@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.
Files changed (68) hide show
  1. package/package.json +1 -1
  2. package/src/backend/c.ts +1 -0
  3. package/src/backend/cfamily.ts +33 -12
  4. package/src/backend/cpp.ts +1 -0
  5. package/src/backend/lines.ts +10 -0
  6. package/src/backend/pascal.ts +17 -5
  7. package/src/cdecl/parse.ts +21 -0
  8. package/src/contracts.ts +73 -20
  9. package/src/frontend/arm-body.ts +155 -0
  10. package/src/frontend/asmdata.ts +2 -1
  11. package/src/frontend/call-plan.ts +197 -45
  12. package/src/frontend/callee-body.ts +370 -0
  13. package/src/frontend/device-pins.ts +18 -8
  14. package/src/frontend/frame-objects.ts +504 -61
  15. package/src/frontend/frontend.ts +20 -0
  16. package/src/frontend/jump-table.ts +35 -20
  17. package/src/frontend/mips.ts +226 -8
  18. package/src/frontend/ppc.ts +26 -2
  19. package/src/frontend/register-struct-call.ts +1 -86
  20. package/src/frontend/ssa.ts +263 -1
  21. package/src/frontend/stackargs.ts +95 -17
  22. package/src/frontend/thumb-entry.ts +340 -0
  23. package/src/frontend/thumb.ts +410 -61
  24. package/src/ir/alias.ts +29 -8
  25. package/src/ir/bits.ts +1081 -66
  26. package/src/ir/core.ts +154 -4
  27. package/src/ir/entry.ts +3 -5
  28. package/src/ir/opcodes.ts +28 -0
  29. package/src/ir/parse.ts +1 -0
  30. package/src/ir/simplify.ts +61 -10
  31. package/src/ir/verify.ts +16 -0
  32. package/src/l3/argbase.ts +2 -0
  33. package/src/l3/ast.ts +294 -2
  34. package/src/l3/basecse.ts +35 -10
  35. package/src/l3/caseendgoto.ts +223 -0
  36. package/src/l3/coalesce.ts +42 -14
  37. package/src/l3/constinit.ts +34 -6
  38. package/src/l3/dce.ts +52 -8
  39. package/src/l3/ptrcell.ts +2 -0
  40. package/src/l3/regspell.ts +2 -0
  41. package/src/l3/reindex.ts +13 -4
  42. package/src/l3/scopebase.ts +2 -0
  43. package/src/l3/tailmerge.ts +2 -0
  44. package/src/pipeline.ts +507 -23
  45. package/src/proto-context.ts +135 -1
  46. package/src/raise/const.ts +25 -0
  47. package/src/raise/declared-volatile.ts +1 -1
  48. package/src/raise/divpow2.ts +192 -0
  49. package/src/raise/offsetnames.ts +412 -10
  50. package/src/raise/pre-recovery.ts +8 -5
  51. package/src/raise/pre-structure.ts +24 -5
  52. package/src/raise/reroll.ts +4 -24
  53. package/src/raise/shortcircuit.ts +192 -23
  54. package/src/raise/unpre.ts +594 -0
  55. package/src/rank-variations.ts +97 -4
  56. package/src/rank.ts +258 -43
  57. package/src/structure/analysis.ts +56 -10
  58. package/src/structure/cross-jumped-tail.ts +216 -0
  59. package/src/structure/globalaccess.ts +46 -1
  60. package/src/structure/structure.ts +4221 -368
  61. package/src/structure/switch-recover.ts +345 -26
  62. package/src/structure/unfolded-branch-constant.ts +90 -0
  63. package/src/symbols.ts +52 -15
  64. package/src/target.ts +269 -8
  65. package/src/trace.ts +28 -14
  66. package/src/variation-definitions.ts +207 -6
  67. package/src/variation-gates.ts +2 -0
  68. 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 `frameArrayElement`, l3/constinit.ts `asElement`). */
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
- | { k: 'break'; ofSwitch?: true }
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 both PLACEMENT heuristics
477
- * ablated, keeping only `single-use`. `loop` and `repeated-const-offset` predict which spelling
478
- * the compiler chose, and both predictions have a counterexample — an MMIO poll (`p[2] = go;
479
- * while (p[2] & BUSY) {}`) stores and re-reads a fixed offset through ONE register the whole
480
- * time. Neither gate is `sound`, so ablating them can only change which spelling wins, never
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
- 'repeated-const-offset',
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
  });