Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions examples/switchFallthrough.dfy
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,21 @@ method collect(items: seq<Item>) returns (res: seq<int>)
}
return out;
}

method score(tag: Tag) returns (res: int)
ensures (res >= 0)
{
var r := 0;
match tag {
case a =>
r := (r + 1);
r := (r + 10);
case b =>
r := (r + 10);
case c =>
r := (r + 100);
case skip =>

}
return r;
}
18 changes: 18 additions & 0 deletions examples/switchFallthrough.dfy.gen
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,21 @@ method collect(items: seq<Item>) returns (res: seq<int>)
}
return out;
}

method score(tag: Tag) returns (res: int)
ensures (res >= 0)
{
var r := 0;
match tag {
case a =>
r := (r + 1);
r := (r + 10);
case b =>
r := (r + 10);
case c =>
r := (r + 100);
case skip =>

}
return r;
}
24 changes: 24 additions & 0 deletions examples/switchFallthrough.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
* - Stacked `case "a": case "b": case "c": body` — the leading labels have no
* statements and fall through, so all three share the body (the `match` gets
* an arm per label).
* - A *non-empty* case with no trailing break/return also falls through: its
* statements run, then control continues into the next case (`score`).
* - The body is a `{ … break }` block; that `break` is the *switch* exit and is
* dropped, NOT emitted as a `break` of the enclosing `while`. A `break` inside
* a nested loop would be kept. See the switch handling in extract.ts.
Expand Down Expand Up @@ -39,3 +41,25 @@ export function collect(items: Item[]): number[] {
}
return out;
}

// Non-empty fall-through: "a" runs `r += 1`, then continues into "b" (`r += 10`);
// "b" stops at its break. Mirrors partial-json-parser's `strip`, where a complete
// "number" token falls into the "string" handling.
export function score(tag: Tag): number {
//@ verify
//@ ensures \result >= 0
let r = 0;
switch (tag) {
case "a":
r = r + 1;
case "b":
r = r + 10;
break;
case "c":
r = r + 100;
break;
case "skip":
break;
}
return r;
}
44 changes: 27 additions & 17 deletions tools/src/extract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1581,25 +1581,35 @@ function extractStmts(stmts: Node[]): RawStmt[] {
// nested loop stays put.
const stripExitBreaks = (b: RawStmt[]) => b.filter(st => st.kind !== "break");
const isExit = (st: RawStmt | undefined) => !!st && ["break", "return", "throw", "continue"].includes(st.kind);
let fallthrough: string[] = [];
for (const clause of s.getClauses()) {
if (Node.isCaseClause(clause)) {
const label = clause.getExpression().getText().replace(/^["']|["']$/g, "");
if (clause.getStatements().length === 0) { fallthrough.push(label); continue; }
const raw = extractStmts(clause.getStatements());
if (!isExit(raw[raw.length - 1]))
throw new Error(`switch case "${label}" at line ${line}: a non-empty case must end with break/return/throw; fall-through into the next case is not supported`);
const body = stripExitBreaks(raw);
for (const l of fallthrough) cases.push({ label: l, body });
cases.push({ label, body });
fallthrough = [];
} else {
defaultBody = stripExitBreaks(extractStmts(clause.getStatements()));
for (const l of fallthrough) cases.push({ label: l, body: defaultBody });
fallthrough = [];
// Resolve JS fall-through positionally: a clause's effective body is its
// own statements concatenated with each following clause's statements up
// to and including the first clause that ends in break/return/throw (or
// the switch end). This covers both empty stacked labels (`case A: case
// B: body`) and a *non-empty* case that falls through (`case A: sA; case
// B: ...`) — the stripped breaks are the switch exits.
const clauseInfos = s.getClauses().map(clause => {
const stmts = extractStmts(clause.getStatements());
return {
label: Node.isCaseClause(clause)
? clause.getExpression().getText().replace(/^["']|["']$/g, "")
: null,
stmts,
exits: isExit(stmts[stmts.length - 1]),
};
});
const fallThroughBody = (start: number): RawStmt[] => {
let body: RawStmt[] = [];
for (let j = start; j < clauseInfos.length; j++) {
body = body.concat(clauseInfos[j]!.stmts);
if (clauseInfos[j]!.exits) break;
}
return stripExitBreaks(body);
};
for (let i = 0; i < clauseInfos.length; i++) {
const c = clauseInfos[i]!;
if (c.label === null) defaultBody = fallThroughBody(i);
else cases.push({ label: c.label, body: fallThroughBody(i) });
}
for (const l of fallthrough) cases.push({ label: l, body: [] });
result.push({ kind: "switch", expr: switchExpr, discriminant, cases, defaultBody, line });
continue;
}
Expand Down
Loading