@asmlift/core 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/backend/c.ts +1 -0
- package/src/backend/cfamily.ts +33 -12
- package/src/backend/cpp.ts +1 -0
- package/src/backend/lines.ts +10 -0
- package/src/backend/pascal.ts +17 -5
- package/src/cdecl/parse.ts +21 -0
- package/src/contracts.ts +73 -20
- package/src/frontend/arm-body.ts +155 -0
- package/src/frontend/asmdata.ts +2 -1
- package/src/frontend/call-plan.ts +197 -45
- package/src/frontend/callee-body.ts +370 -0
- package/src/frontend/device-pins.ts +18 -8
- package/src/frontend/frame-objects.ts +504 -61
- package/src/frontend/frontend.ts +20 -0
- package/src/frontend/jump-table.ts +35 -20
- package/src/frontend/mips.ts +226 -8
- package/src/frontend/ppc.ts +26 -2
- package/src/frontend/register-struct-call.ts +1 -86
- package/src/frontend/ssa.ts +263 -1
- package/src/frontend/stackargs.ts +95 -17
- package/src/frontend/thumb-entry.ts +340 -0
- package/src/frontend/thumb.ts +410 -61
- package/src/ir/alias.ts +29 -8
- package/src/ir/bits.ts +1081 -66
- package/src/ir/core.ts +154 -4
- package/src/ir/entry.ts +3 -5
- package/src/ir/opcodes.ts +28 -0
- package/src/ir/parse.ts +1 -0
- package/src/ir/simplify.ts +61 -10
- package/src/ir/verify.ts +16 -0
- package/src/l3/argbase.ts +2 -0
- package/src/l3/ast.ts +294 -2
- package/src/l3/basecse.ts +35 -10
- package/src/l3/caseendgoto.ts +223 -0
- package/src/l3/coalesce.ts +42 -14
- package/src/l3/constinit.ts +34 -6
- package/src/l3/dce.ts +52 -8
- package/src/l3/ptrcell.ts +2 -0
- package/src/l3/regspell.ts +2 -0
- package/src/l3/reindex.ts +13 -4
- package/src/l3/scopebase.ts +2 -0
- package/src/l3/tailmerge.ts +2 -0
- package/src/pipeline.ts +507 -23
- package/src/proto-context.ts +135 -1
- package/src/raise/const.ts +25 -0
- package/src/raise/declared-volatile.ts +1 -1
- package/src/raise/divpow2.ts +192 -0
- package/src/raise/offsetnames.ts +412 -10
- package/src/raise/pre-recovery.ts +8 -5
- package/src/raise/pre-structure.ts +24 -5
- package/src/raise/reroll.ts +4 -24
- package/src/raise/shortcircuit.ts +192 -23
- package/src/raise/unpre.ts +594 -0
- package/src/rank-variations.ts +97 -4
- package/src/rank.ts +258 -43
- package/src/structure/analysis.ts +56 -10
- package/src/structure/cross-jumped-tail.ts +216 -0
- package/src/structure/globalaccess.ts +46 -1
- package/src/structure/structure.ts +4221 -368
- package/src/structure/switch-recover.ts +345 -26
- package/src/structure/unfolded-branch-constant.ts +90 -0
- package/src/symbols.ts +52 -15
- package/src/target.ts +269 -8
- package/src/trace.ts +28 -14
- package/src/variation-definitions.ts +207 -6
- package/src/variation-gates.ts +2 -0
- package/src/variation-tokens.ts +63 -2
package/src/l3/ast.ts
CHANGED
|
@@ -229,7 +229,7 @@ export type Expr =
|
|
|
229
229
|
* *u.half;` returns stale (`mov r0, #0x1`) where the subscript reloads. Set on a view of a
|
|
230
230
|
* frame union (structure.ts `frameUnionAccess`), the one access that rests on that set,
|
|
231
231
|
* and on an element of a frame array, where it only keeps the element spelling at index 0
|
|
232
|
-
* (structure.ts `
|
|
232
|
+
* (structure.ts `bareArrayIndex`, l3/constinit.ts `asElement`). */
|
|
233
233
|
subscript?: true;
|
|
234
234
|
}
|
|
235
235
|
// A named struct-field access `base->name` (raise/structs.ts recovered `base` as a struct
|
|
@@ -352,8 +352,36 @@ export type Stmt =
|
|
|
352
352
|
// would bind it to the other kind — a plain one between `case` labels, this one in a loop body
|
|
353
353
|
// (backend/cfamily.ts `refuseMisboundBreak`). A pass that reads `break` as a loop exit must skip
|
|
354
354
|
// this one (l3/reindex.ts `hasLoopExit`).
|
|
355
|
-
|
|
355
|
+
//
|
|
356
|
+
// `label` names a switch-scoped `break` a case-end `goto` lands on (`L: break;`, see `goto`).
|
|
357
|
+
| { k: 'break'; ofSwitch?: true; label?: string }
|
|
356
358
|
| { k: 'continue' }
|
|
359
|
+
// `goto <label>;` — L3 HOLDS TWO GOTOS, both narrow (docs/asmlift-101.md §2.7).
|
|
360
|
+
//
|
|
361
|
+
// A CASE-LABEL goto jumps to a case label of the switch it sits in, written where two or more arms
|
|
362
|
+
// enter one sibling arm's body and only one of them can reach it by falling through (`case 1: if
|
|
363
|
+
// (c) continue; goto L; case 2: … case 3: L: …`). `label` names the target arm's
|
|
364
|
+
// `SwitchCase.label`. It is the LAST statement of a case or default body, at that body's top
|
|
365
|
+
// level, and its label is a case of THAT switch — so no loop or nested switch stands between the
|
|
366
|
+
// jump and its target, and the jump runs exactly what a fall-through into that arm would run.
|
|
367
|
+
//
|
|
368
|
+
// A CASE-END goto is the next kind, `caseEndGoto`. Both are checked by `caseGotoViolation`
|
|
369
|
+
// (contracts.ts `assertCaseGotosNarrow`, which every structured and respelled tree passes). Any
|
|
370
|
+
// other goto is not representable. A goto ends its path like `break` and `continue`. SGI/IDO
|
|
371
|
+
// Pascal has no goto into a `case-of` arm, so its backend loud-fails it.
|
|
372
|
+
| { k: 'goto'; label: string }
|
|
373
|
+
// `goto <label>;` — THE CASE-END GOTO, the second goto L3 holds, a jump to the end of the arm it
|
|
374
|
+
// sits in: `label` names a labelled switch-scoped `break` (`L: break;`) that is an element of a
|
|
375
|
+
// statement list enclosing the goto, later in that list than the statement holding it, with no
|
|
376
|
+
// switch between the two and no loop around the label inside the arm — so the jump leaves every
|
|
377
|
+
// loop it sits in and then the switch, and runs nothing on the way. It is the source's `for (j…)
|
|
378
|
+
// { if (hit) { …; goto L; } } k(i); L: break;`, which agbcc threads past the loop around the
|
|
379
|
+
// switch (jump.c `follow_jumps`). Its own kind, not a flag on `goto`, so every pass that reads a
|
|
380
|
+
// `goto` as the jump into a case arm (`switchArmSuccessors`, `switchJumpsBack`) never meets one,
|
|
381
|
+
// and every exhaustive switch over `Stmt` decides it. It ends its path, and leaves the innermost
|
|
382
|
+
// switch around it. Written only by l3/caseendgoto.ts. SGI/IDO Pascal has no `break`, so its
|
|
383
|
+
// backend loud-fails it.
|
|
384
|
+
| { k: 'caseEndGoto'; label: string }
|
|
357
385
|
// A multi-way `switch` over an integer scrutinee (recovered from a comparison tree — Regime A — or
|
|
358
386
|
// a jump-table `switch_br` — Regime B). `cases` are emitted IN ARRAY ORDER; `default` (if present)
|
|
359
387
|
// is emitted after `defaultAt` of them, or after all of them when that is absent.
|
|
@@ -398,6 +426,10 @@ export interface SwitchCase {
|
|
|
398
426
|
values: number[];
|
|
399
427
|
body: Stmt[];
|
|
400
428
|
fallsThrough: boolean;
|
|
429
|
+
/** The C label printed after this arm's `case` labels, present only where a `goto` of this same
|
|
430
|
+
* switch names it (see the `goto` statement). A pass that merges, drops or reorders arms keeps it
|
|
431
|
+
* on the arm it labels. */
|
|
432
|
+
label?: string;
|
|
401
433
|
}
|
|
402
434
|
|
|
403
435
|
export interface SFn {
|
|
@@ -579,6 +611,12 @@ export interface LanguageBackend {
|
|
|
579
611
|
* spells without one, by copying the code the arm meets at into each path, which Pascal prints.
|
|
580
612
|
* The structurer reads it through `StructureOptions.spellSwitchBreak`. */
|
|
581
613
|
readonly spellsSwitchBreak: boolean;
|
|
614
|
+
/** Can this language spell a `return` before the end of the function? C and C++ can; Pascal
|
|
615
|
+
* cannot, and its backend loud-fails one. Asked for the same reason again: the join past early
|
|
616
|
+
* returns ends each arm that leaves early in a `return`, and the same branch also spells by
|
|
617
|
+
* copying the code below it into each arm (the return-tail copies), which Pascal prints. The
|
|
618
|
+
* structurer reads it through `StructureOptions.spellEarlyReturns`. */
|
|
619
|
+
readonly spellsEarlyReturn: boolean;
|
|
582
620
|
emit(fn: SFn): string;
|
|
583
621
|
// Spell ONE LINE of text as a comment in this language (C block comments, Pascal `(* … *)`).
|
|
584
622
|
// Used by the annotate-mode stub path to carry the failure reason + the original asm
|
|
@@ -896,6 +934,8 @@ export function stmtExprs(s: Stmt): Expr[] {
|
|
|
896
934
|
return [s.scrutinee];
|
|
897
935
|
case 'break':
|
|
898
936
|
case 'continue':
|
|
937
|
+
case 'goto':
|
|
938
|
+
case 'caseEndGoto':
|
|
899
939
|
return [];
|
|
900
940
|
}
|
|
901
941
|
}
|
|
@@ -935,6 +975,8 @@ export function mapStmtExprs(s: Stmt, f: (e: Expr) => Expr): Stmt {
|
|
|
935
975
|
};
|
|
936
976
|
case 'break':
|
|
937
977
|
case 'continue':
|
|
978
|
+
case 'goto':
|
|
979
|
+
case 'caseEndGoto':
|
|
938
980
|
return s;
|
|
939
981
|
}
|
|
940
982
|
}
|
|
@@ -1091,6 +1133,8 @@ export function stmtChildren(s: Stmt): Stmt[] {
|
|
|
1091
1133
|
case 'return':
|
|
1092
1134
|
case 'break':
|
|
1093
1135
|
case 'continue':
|
|
1136
|
+
case 'goto':
|
|
1137
|
+
case 'caseEndGoto':
|
|
1094
1138
|
return [];
|
|
1095
1139
|
case 'if':
|
|
1096
1140
|
return [...s.then, ...s.else];
|
|
@@ -1141,10 +1185,254 @@ export function isLoop(s: Stmt): s is Loop {
|
|
|
1141
1185
|
case 'return':
|
|
1142
1186
|
case 'break':
|
|
1143
1187
|
case 'continue':
|
|
1188
|
+
case 'goto':
|
|
1189
|
+
case 'caseEndGoto':
|
|
1144
1190
|
return false;
|
|
1145
1191
|
}
|
|
1146
1192
|
}
|
|
1147
1193
|
|
|
1194
|
+
/** Why `body` breaks the two forms a `goto` may take in L3 (the `goto` statement's contract), or
|
|
1195
|
+
* null when every `goto` in it keeps one. A case-label goto is the LAST statement of a case or
|
|
1196
|
+
* default body, at that body's top level, naming a label one case of THAT switch carries. A
|
|
1197
|
+
* case-end goto (`caseEndGoto`) names a labelled switch-scoped `break` later in a statement list that
|
|
1198
|
+
* encloses it, inside the same switch arm, with no switch between them; ifs and loops may stand
|
|
1199
|
+
* between, but no loop may stand between the labelled `break` and its arm. Labels are unique
|
|
1200
|
+
* across the function, case labels and `break` labels together. Any other placement is refused:
|
|
1201
|
+
* a case-label goto under an `if`, in a loop body, in a nested switch's arm aimed at the outer
|
|
1202
|
+
* switch or before another statement; a case-end goto in a nested switch, in another arm, outside
|
|
1203
|
+
* the switch, or after its label; a labelled `break` that is not the switch's or sits in a loop.
|
|
1204
|
+
*
|
|
1205
|
+
* Shared by contracts.ts `assertCaseGotosNarrow`, which every structured and respelled tree
|
|
1206
|
+
* passes, and the C-family printer, which asks it of every function it prints: a pass that moves
|
|
1207
|
+
* a statement across an arm boundary is caught at the one place the program is spelled. */
|
|
1208
|
+
export function caseGotoViolation(body: readonly Stmt[]): string | null {
|
|
1209
|
+
const seen = new Set<string>();
|
|
1210
|
+
const claim = (label: string): string | null => {
|
|
1211
|
+
if (seen.has(label)) {
|
|
1212
|
+
return `the label \`${label}\` is carried by two switch arms`;
|
|
1213
|
+
}
|
|
1214
|
+
seen.add(label);
|
|
1215
|
+
return null;
|
|
1216
|
+
};
|
|
1217
|
+
/** `labels`: the case labels a case-label goto here may name (null outside an arm's top level's
|
|
1218
|
+
* reach); `ends`: the `break` labels a case-end goto here may name; `inArm`: inside a switch arm
|
|
1219
|
+
* with no loop entered since it began, where a labelled `break` may sit. */
|
|
1220
|
+
type Scope = { labels: ReadonlySet<string> | null; armTop: boolean; ends: ReadonlySet<string>; inArm: boolean };
|
|
1221
|
+
const walk = (list: readonly Stmt[], scope: Scope): string | null => {
|
|
1222
|
+
// `after[i]`: the `break` labels later in this list than `i`, beside the enclosing ones — built
|
|
1223
|
+
// only for a list holding one, so the walk allocates nothing per statement on a goto-free tree
|
|
1224
|
+
const after = endsAfter(list, scope.ends);
|
|
1225
|
+
for (const [i, s] of list.entries()) {
|
|
1226
|
+
if (s.k === 'caseEndGoto') {
|
|
1227
|
+
if (!scope.ends.has(s.label)) {
|
|
1228
|
+
return `\`goto ${s.label};\` names no labelled \`break;\` that ends the switch arm it sits in`;
|
|
1229
|
+
}
|
|
1230
|
+
continue;
|
|
1231
|
+
}
|
|
1232
|
+
if (s.k === 'goto') {
|
|
1233
|
+
if (scope.labels === null || !scope.armTop || i !== list.length - 1) {
|
|
1234
|
+
return `\`goto ${s.label};\` is not the last statement of a switch arm's own body`;
|
|
1235
|
+
}
|
|
1236
|
+
if (!scope.labels.has(s.label)) {
|
|
1237
|
+
return `\`goto ${s.label};\` names no case label of the switch it sits in`;
|
|
1238
|
+
}
|
|
1239
|
+
continue;
|
|
1240
|
+
}
|
|
1241
|
+
if (s.k === 'break' && s.label !== undefined) {
|
|
1242
|
+
if (s.ofSwitch !== true || !scope.inArm) {
|
|
1243
|
+
return `the label \`${s.label}\` is not on a \`break;\` that leaves a switch arm`;
|
|
1244
|
+
}
|
|
1245
|
+
const why = claim(s.label);
|
|
1246
|
+
if (why !== null) {
|
|
1247
|
+
return why;
|
|
1248
|
+
}
|
|
1249
|
+
continue;
|
|
1250
|
+
}
|
|
1251
|
+
if (s.k === 'switch') {
|
|
1252
|
+
const own = new Set<string>();
|
|
1253
|
+
for (const c of s.cases) {
|
|
1254
|
+
if (c.label !== undefined) {
|
|
1255
|
+
const why = claim(c.label);
|
|
1256
|
+
if (why !== null) {
|
|
1257
|
+
return why;
|
|
1258
|
+
}
|
|
1259
|
+
own.add(c.label);
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
for (const arm of [...s.cases.map((c) => c.body), s.default ?? []]) {
|
|
1263
|
+
const why = walk(arm, { labels: own, armTop: true, ends: NO_ENDS, inArm: true });
|
|
1264
|
+
if (why !== null) {
|
|
1265
|
+
return why;
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
continue;
|
|
1269
|
+
}
|
|
1270
|
+
// The `break` labels later in this list are the ends a goto inside `s` may reach. Inside a
|
|
1271
|
+
// loop the switch's case labels are out of reach (a case-label goto there would leave the
|
|
1272
|
+
// loop) and no `break;` leaves the arm.
|
|
1273
|
+
const ends = after?.[i] ?? scope.ends;
|
|
1274
|
+
const loop = isLoop(s);
|
|
1275
|
+
for (const inner of stmtLists(s)) {
|
|
1276
|
+
const why = walk(inner, {
|
|
1277
|
+
labels: loop ? null : scope.labels,
|
|
1278
|
+
armTop: false,
|
|
1279
|
+
ends,
|
|
1280
|
+
inArm: scope.inArm && !loop,
|
|
1281
|
+
});
|
|
1282
|
+
if (why !== null) {
|
|
1283
|
+
return why;
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
if (s.k === 'for') {
|
|
1287
|
+
const why = walk([s.init, s.inc], { labels: null, armTop: false, ends: NO_ENDS, inArm: false });
|
|
1288
|
+
if (why !== null) {
|
|
1289
|
+
return why;
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
return null;
|
|
1294
|
+
};
|
|
1295
|
+
return walk(body, { labels: null, armTop: false, ends: NO_ENDS, inArm: false });
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
const NO_ENDS: ReadonlySet<string> = new Set();
|
|
1299
|
+
|
|
1300
|
+
/** For each index of `list`, `outer` and the labels of the labelled `break`s that are elements of
|
|
1301
|
+
* `list` after it; undefined where `list` holds none, so every index reads `outer`. */
|
|
1302
|
+
function endsAfter(list: readonly Stmt[], outer: ReadonlySet<string>): ReadonlySet<string>[] | undefined {
|
|
1303
|
+
if (!list.some((s) => s.k === 'break' && s.label !== undefined)) {
|
|
1304
|
+
return undefined;
|
|
1305
|
+
}
|
|
1306
|
+
const out: ReadonlySet<string>[] = new Array(list.length);
|
|
1307
|
+
let later: ReadonlySet<string> = outer;
|
|
1308
|
+
for (let i = list.length - 1; i >= 0; i--) {
|
|
1309
|
+
out[i] = later;
|
|
1310
|
+
const s = list[i];
|
|
1311
|
+
if (s.k === 'break' && s.label !== undefined) {
|
|
1312
|
+
later = new Set([...later, s.label]);
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
return out;
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
/** Does a case-end `goto` in `list`, at any depth, name a labelled `break` that `list` does not
|
|
1319
|
+
* hold — so the jump leaves `list`, and every loop of it the goto sits in? A pass asking whether a
|
|
1320
|
+
* loop body leaves its loop asks this beside its `break`s (l3/reindex.ts `hasLoopExit`). */
|
|
1321
|
+
export function caseEndGotoLeaves(list: readonly Stmt[]): boolean {
|
|
1322
|
+
const held = new Set<string>();
|
|
1323
|
+
const named: string[] = [];
|
|
1324
|
+
const visit = (s: Stmt): void => {
|
|
1325
|
+
if (s.k === 'break' && s.label !== undefined) {
|
|
1326
|
+
held.add(s.label);
|
|
1327
|
+
} else if (s.k === 'caseEndGoto') {
|
|
1328
|
+
named.push(s.label);
|
|
1329
|
+
}
|
|
1330
|
+
stmtChildren(s).forEach(visit);
|
|
1331
|
+
};
|
|
1332
|
+
list.forEach(visit);
|
|
1333
|
+
return named.some((l) => !held.has(l));
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
/** Can control fall off the END of `list`? False only where every path through it is provably
|
|
1337
|
+
* left first — by `return`, `continue`, a loop's `break`, a `goto`, a switch-scoped `break` or a
|
|
1338
|
+
* case-end `goto` (which leave the arm they sit in), or an `if` or `switch` none of whose paths
|
|
1339
|
+
* reaches its own end. A loop is read as reaching its end (a decided exit is not looked for), so
|
|
1340
|
+
* the answer errs toward "reaches", which is the side a caller appending a jump after the list
|
|
1341
|
+
* can afford: the jump it then writes is one no path runs, dead code and never a path cut. */
|
|
1342
|
+
export function reachesEnd(list: readonly Stmt[]): boolean {
|
|
1343
|
+
// `end`: some path falls off the list's last statement; `leaves`: some path takes a
|
|
1344
|
+
// switch-scoped `break` out of it, which ends the innermost switch around it.
|
|
1345
|
+
const flow = (ss: readonly Stmt[]): { end: boolean; leaves: boolean } => {
|
|
1346
|
+
let leaves = false;
|
|
1347
|
+
for (const s of ss) {
|
|
1348
|
+
switch (s.k) {
|
|
1349
|
+
case 'return':
|
|
1350
|
+
case 'continue':
|
|
1351
|
+
case 'goto':
|
|
1352
|
+
return { end: false, leaves };
|
|
1353
|
+
// a case-end goto leaves the innermost switch around it, as its labelled `break;` does
|
|
1354
|
+
case 'caseEndGoto':
|
|
1355
|
+
return { end: false, leaves: true };
|
|
1356
|
+
case 'break':
|
|
1357
|
+
return { end: false, leaves: leaves || s.ofSwitch === true };
|
|
1358
|
+
case 'if': {
|
|
1359
|
+
const t = flow(s.then);
|
|
1360
|
+
const e = flow(s.else);
|
|
1361
|
+
leaves ||= t.leaves || e.leaves;
|
|
1362
|
+
if (!t.end && !e.end) {
|
|
1363
|
+
return { end: false, leaves };
|
|
1364
|
+
}
|
|
1365
|
+
break;
|
|
1366
|
+
}
|
|
1367
|
+
case 'switch': {
|
|
1368
|
+
// Its end is reached by a `break` out of an arm, by an arm that neither falls nor jumps
|
|
1369
|
+
// on and reaches its own end, or — with no `default` — by a value no case names.
|
|
1370
|
+
const next = switchArmSuccessors(s);
|
|
1371
|
+
const ends =
|
|
1372
|
+
s.default === undefined ||
|
|
1373
|
+
[...s.cases.map((c) => c.body), s.default].some((body, i) => {
|
|
1374
|
+
const f = flow(body);
|
|
1375
|
+
return f.leaves || (f.end && next[i] === undefined);
|
|
1376
|
+
});
|
|
1377
|
+
if (!ends) {
|
|
1378
|
+
return { end: false, leaves };
|
|
1379
|
+
}
|
|
1380
|
+
break;
|
|
1381
|
+
}
|
|
1382
|
+
case 'while':
|
|
1383
|
+
case 'dowhile':
|
|
1384
|
+
case 'for':
|
|
1385
|
+
case 'assign':
|
|
1386
|
+
case 'store':
|
|
1387
|
+
case 'exprstmt':
|
|
1388
|
+
break;
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
return { end: true, leaves };
|
|
1392
|
+
};
|
|
1393
|
+
return flow(list).end;
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
/** For each arm of a `switch` — its cases in order, then its `default` at index `cases.length` —
|
|
1397
|
+
* the arm control runs on into from that arm's end, or undefined where it leaves the switch: the
|
|
1398
|
+
* arm a trailing `goto` names (its last top-level statement, `caseGotoViolation`), else the next
|
|
1399
|
+
* arm where the case falls through (the last case into the `default`, or out where there is
|
|
1400
|
+
* none). The one reading of "arm `i` runs on into arm `j`" that contracts.ts's effect count and
|
|
1401
|
+
* l3/coalesce.ts's arm-disjointness both follow, transitively. */
|
|
1402
|
+
export function switchArmSuccessors(sw: Extract<Stmt, { k: 'switch' }>): (number | undefined)[] {
|
|
1403
|
+
const labelled = new Map(sw.cases.flatMap((c, i) => (c.label === undefined ? [] : [[c.label, i] as const])));
|
|
1404
|
+
const jumpOf = (body: readonly Stmt[]): number | undefined => {
|
|
1405
|
+
const last = body[body.length - 1];
|
|
1406
|
+
return last?.k === 'goto' ? labelled.get(last.label) : undefined;
|
|
1407
|
+
};
|
|
1408
|
+
const last = sw.cases.length;
|
|
1409
|
+
return [
|
|
1410
|
+
...sw.cases.map(
|
|
1411
|
+
(c, i) => jumpOf(c.body) ?? (c.fallsThrough && (i + 1 < last || sw.default !== undefined) ? i + 1 : undefined),
|
|
1412
|
+
),
|
|
1413
|
+
sw.default === undefined ? undefined : jumpOf(sw.default),
|
|
1414
|
+
];
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
/** Does a case-label `goto` of this switch land on an arm LAID OUT AT OR ABOVE the arm it ends —
|
|
1418
|
+
* a BACK EDGE with no loop statement around it? Then the arm it lands on runs AFTER the jumping
|
|
1419
|
+
* arm, so preorder stops being execution order inside the switch's own range, exactly as it does
|
|
1420
|
+
* inside a loop body; a pass that reads preorder as liveness must count such a switch with the
|
|
1421
|
+
* loops (l3/coalesce.ts `spans`). Positions are the layout {@link stmtChildren} walks: the
|
|
1422
|
+
* `default` sits at `defaultAt`. A fall-through only ever runs on DOWNWARD, so only the jumps are
|
|
1423
|
+
* asked. */
|
|
1424
|
+
export function switchJumpsBack(sw: Extract<Stmt, { k: 'switch' }>): boolean {
|
|
1425
|
+
const at = sw.defaultAt ?? sw.cases.length;
|
|
1426
|
+
const laidOut = (arm: number): number => (arm === sw.cases.length ? at : arm < at ? arm : arm + 1);
|
|
1427
|
+
const labelled = new Map(sw.cases.flatMap((c, i) => (c.label === undefined ? [] : [[c.label, i] as const])));
|
|
1428
|
+
const bodies = [...sw.cases.map((c) => c.body), sw.default ?? []];
|
|
1429
|
+
return bodies.some((body, i) => {
|
|
1430
|
+
const last = body[body.length - 1];
|
|
1431
|
+
const to = last?.k === 'goto' ? labelled.get(last.label) : undefined;
|
|
1432
|
+
return to !== undefined && laidOut(to) <= laidOut(i);
|
|
1433
|
+
});
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1148
1436
|
/** The nested statement LISTS of a statement — the SCOPES it opens.
|
|
1149
1437
|
*
|
|
1150
1438
|
* Deliberately not `stmtChildren`, which flattens a `for`'s `init`/`inc` in with its body: those
|
|
@@ -1172,6 +1460,8 @@ export function stmtLists(s: Stmt): Stmt[][] {
|
|
|
1172
1460
|
case 'return':
|
|
1173
1461
|
case 'break':
|
|
1174
1462
|
case 'continue':
|
|
1463
|
+
case 'goto':
|
|
1464
|
+
case 'caseEndGoto':
|
|
1175
1465
|
return [];
|
|
1176
1466
|
}
|
|
1177
1467
|
}
|
|
@@ -1201,6 +1491,8 @@ export function mapStmtLists(s: Stmt, f: (list: Stmt[]) => Stmt[]): Stmt {
|
|
|
1201
1491
|
case 'return':
|
|
1202
1492
|
case 'break':
|
|
1203
1493
|
case 'continue':
|
|
1494
|
+
case 'goto':
|
|
1495
|
+
case 'caseEndGoto':
|
|
1204
1496
|
return s;
|
|
1205
1497
|
}
|
|
1206
1498
|
}
|
package/src/l3/basecse.ts
CHANGED
|
@@ -203,13 +203,16 @@ interface Collected {
|
|
|
203
203
|
* pass has rebuilt one node and dropped the stamp, and requiring all of them makes that a
|
|
204
204
|
* refusal rather than a half-homed base. */
|
|
205
205
|
ordered: Map<string, boolean>;
|
|
206
|
+
/** per key, whether EVERY access so far sits under some connective's right operand — `false`
|
|
207
|
+
* once one access is under none (the input to `guardedOperand`) */
|
|
208
|
+
guard: Map<string, boolean>;
|
|
206
209
|
}
|
|
207
210
|
|
|
208
211
|
/** Every `index` node whose base is a hoistable leaf, tallied by key (the gates' use count) and in
|
|
209
212
|
* first-appearance order (so the hoisted assignments emit in the order the bases are first used,
|
|
210
213
|
* matching the compiler's pool-load order). `loop` marks uses nested in a while/do-while/for. */
|
|
211
214
|
function collect(stmts: Stmt[], c: Collected, loop: boolean): void {
|
|
212
|
-
const visitExpr = (e: Expr, inLoop: boolean): void => {
|
|
215
|
+
const visitExpr = (e: Expr, inLoop: boolean, guard: boolean): void => {
|
|
213
216
|
if (e.k === 'index' && isHoistableBase(e.base)) {
|
|
214
217
|
const k = keyOf(e.base, e.width, e.signed);
|
|
215
218
|
if (!c.count.has(k)) {
|
|
@@ -232,9 +235,11 @@ function collect(stmts: Stmt[], c: Collected, loop: boolean): void {
|
|
|
232
235
|
c.operandOff.add(k);
|
|
233
236
|
}
|
|
234
237
|
c.ordered.set(k, (c.ordered.get(k) ?? true) && e.baseOrdered === true);
|
|
238
|
+
c.guard.set(k, (c.guard.get(k) ?? true) && guard);
|
|
235
239
|
}
|
|
240
|
+
const rhs = e.k === 'bin' && (e.op === '&&' || e.op === '||') ? e.r : null;
|
|
236
241
|
for (const ch of exprChildren(e)) {
|
|
237
|
-
visitExpr(ch, inLoop);
|
|
242
|
+
visitExpr(ch, inLoop, guard || ch === rhs);
|
|
238
243
|
}
|
|
239
244
|
};
|
|
240
245
|
for (const s of stmts) {
|
|
@@ -242,7 +247,7 @@ function collect(stmts: Stmt[], c: Collected, loop: boolean): void {
|
|
|
242
247
|
// there is loop-invariant just like a body use — visit it with `nested`, not the outer flag.
|
|
243
248
|
const nested = loop || isLoop(s);
|
|
244
249
|
for (const e of stmtExprs(s)) {
|
|
245
|
-
visitExpr(e, nested);
|
|
250
|
+
visitExpr(e, nested, false);
|
|
246
251
|
}
|
|
247
252
|
collect(stmtChildren(s), c, nested);
|
|
248
253
|
}
|
|
@@ -380,6 +385,14 @@ export interface BaseKey {
|
|
|
380
385
|
* false for every base a compiler that has not opted in produced, which is what keeps the variation
|
|
381
386
|
* off those targets. */
|
|
382
387
|
orderLicensed: boolean;
|
|
388
|
+
/** EVERY access sits in the right operand of some `&&`/`||` — the half C evaluates only when the
|
|
389
|
+
* left one did not decide. A head hoist puts the base's load above that guard, where the compiler
|
|
390
|
+
* loaded it inside the operand: `if (gY != 5 || (gS.a | gS.d | gS.c) != 0)` reads `&gS` into a
|
|
391
|
+
* register after the `gY` test, while `p0 = &gS;` ahead of the `if` loads it first. Not one
|
|
392
|
+
* operand: accesses under two different guards (`(gY != 5 || gS.a) && (gA != 1 || gS.c)`, or the
|
|
393
|
+
* two in separate `if`s) are each loaded under their own, so a hoist above both moves every load
|
|
394
|
+
* (agbcc 15/39 and 19/40 hoisted, 9/39 inline). */
|
|
395
|
+
guardedOperand: boolean;
|
|
383
396
|
}
|
|
384
397
|
|
|
385
398
|
/** The admission rules. NONE is sound, and that is a property of the pass rather than an oversight:
|
|
@@ -441,6 +454,16 @@ export const BASECSE_GATES: readonly Gate<BaseKey>[] = [
|
|
|
441
454
|
sound: false,
|
|
442
455
|
rejects: (c) => c.repeatedConstOffset,
|
|
443
456
|
},
|
|
457
|
+
{
|
|
458
|
+
// `if (gY != 5 || (gS.a | gS.d | gS.c) != 0) { … }` on agbcc: the inline spelling MATCHES and
|
|
459
|
+
// `p0 = (u8 *)&gS;` above the `if` does not (7 points over 44), because the literal load moves
|
|
460
|
+
// ahead of the first test. A placement prediction like the two above, ablated with them for
|
|
461
|
+
// `/livebase`, so a hoisted base before a connective stays a candidate the differ can pick.
|
|
462
|
+
id: 'guarded-operand',
|
|
463
|
+
why: 'a base read only under the second half of a condition is loaded there, after the first half',
|
|
464
|
+
sound: false,
|
|
465
|
+
rejects: (c) => c.guardedOperand,
|
|
466
|
+
},
|
|
444
467
|
];
|
|
445
468
|
|
|
446
469
|
/** `/basefold`'s admission (rank.ts): the default rules with `single-use` EXEMPTING a base whose
|
|
@@ -473,15 +496,15 @@ export const BASEFOLD_GATES: readonly Gate<BaseKey>[] = [
|
|
|
473
496
|
...ablateHeuristic(BASECSE_GATES, 'single-use'),
|
|
474
497
|
];
|
|
475
498
|
|
|
476
|
-
/** The `/livebase` variation's admission (rank.ts): the default rules with
|
|
477
|
-
* ablated, keeping only `single-use`. `loop
|
|
478
|
-
* the compiler chose, and
|
|
479
|
-
* while (p[2] & BUSY) {}`) stores and re-reads a fixed offset through ONE register
|
|
480
|
-
* time.
|
|
499
|
+
/** The `/livebase` variation's admission (rank.ts): the default rules with the PLACEMENT heuristics
|
|
500
|
+
* ablated, keeping only `single-use`. `loop`, `repeated-const-offset` and `guarded-operand` predict
|
|
501
|
+
* which spelling the compiler chose, and the first two have a counterexample — an MMIO poll
|
|
502
|
+
* (`p[2] = go; while (p[2] & BUSY) {}`) stores and re-reads a fixed offset through ONE register
|
|
503
|
+
* the whole time. None of the three is `sound`, so ablating them can only change which spelling wins, never
|
|
481
504
|
* what a candidate means; the differ referees. */
|
|
482
505
|
export const LIVEBASE_GATES: readonly Gate<BaseKey>[] = ablateHeuristic(
|
|
483
|
-
ablateHeuristic(BASECSE_GATES, 'loop'),
|
|
484
|
-
'
|
|
506
|
+
ablateHeuristic(ablateHeuristic(BASECSE_GATES, 'loop'), 'repeated-const-offset'),
|
|
507
|
+
'guarded-operand',
|
|
485
508
|
);
|
|
486
509
|
|
|
487
510
|
/** `/livebase-block`'s admission (rank.ts): `/livebase` plus `single-cell`. The two tables differ
|
|
@@ -725,6 +748,7 @@ function collectFrom(sfn: SFn): Collected {
|
|
|
725
748
|
varIndexed: new Set(),
|
|
726
749
|
operandOff: new Set(),
|
|
727
750
|
ordered: new Map(),
|
|
751
|
+
guard: new Map(),
|
|
728
752
|
};
|
|
729
753
|
collect(sfn.body, c, false);
|
|
730
754
|
return c;
|
|
@@ -745,6 +769,7 @@ function admit(sfn: SFn, gates: readonly Gate<BaseKey>[]): { c: Collected; keys:
|
|
|
745
769
|
unfoldedOffset: c.operandOff.has(k),
|
|
746
770
|
castBase: c.meta.get(k)!.base.k === 'cast',
|
|
747
771
|
orderLicensed: c.ordered.get(k) === true,
|
|
772
|
+
guardedOperand: c.guard.get(k) === true,
|
|
748
773
|
}) === null
|
|
749
774
|
);
|
|
750
775
|
});
|