@asmlift/core 0.8.0 → 0.9.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 (105) hide show
  1. package/README.md +34 -30
  2. package/package.json +1 -1
  3. package/src/aggregate.ts +248 -0
  4. package/src/backend/c.ts +5 -2
  5. package/src/backend/cfamily.ts +236 -37
  6. package/src/backend/cpp.ts +114 -9
  7. package/src/backend/pascal.ts +39 -0
  8. package/src/cdecl/constant.ts +59 -0
  9. package/src/cdecl/lex.ts +209 -0
  10. package/src/cdecl/parse.ts +975 -0
  11. package/src/cdecl/spell.ts +69 -0
  12. package/src/codegen-flags.ts +38 -6
  13. package/src/compiler-diagnostics.ts +194 -0
  14. package/src/contracts.ts +615 -79
  15. package/src/declare.ts +75 -5
  16. package/src/frontend/asmdata.ts +19 -5
  17. package/src/frontend/call-plan.ts +686 -0
  18. package/src/frontend/device-pins.ts +277 -0
  19. package/src/frontend/errors.ts +5 -0
  20. package/src/frontend/flags-edge.ts +101 -0
  21. package/src/frontend/fpu.ts +167 -0
  22. package/src/frontend/frame-objects.ts +2131 -0
  23. package/src/frontend/high-half.ts +2 -1
  24. package/src/frontend/local-object.ts +490 -0
  25. package/src/frontend/mips.ts +363 -40
  26. package/src/frontend/opaque.ts +97 -18
  27. package/src/frontend/ppc.ts +520 -129
  28. package/src/frontend/reloc-symbol.ts +94 -13
  29. package/src/frontend/splat.ts +108 -5
  30. package/src/frontend/ssa.ts +669 -76
  31. package/src/frontend/stackargs.ts +120 -39
  32. package/src/frontend/thumb.ts +2764 -2018
  33. package/src/ir/alias.ts +29 -0
  34. package/src/ir/core.ts +101 -3
  35. package/src/ir/discipline.ts +248 -0
  36. package/src/ir/float-bits.ts +47 -0
  37. package/src/ir/opcodes.ts +84 -112
  38. package/src/ir/parse.ts +1 -0
  39. package/src/ir/types.ts +118 -2
  40. package/src/ir/verify.ts +150 -3
  41. package/src/l3/address.ts +5 -0
  42. package/src/l3/argcopy.ts +38 -6
  43. package/src/l3/ast.ts +186 -14
  44. package/src/l3/coalesce.ts +21 -13
  45. package/src/l3/dce.ts +32 -10
  46. package/src/l3/gates.ts +9 -3
  47. package/src/l3/hoist.ts +8 -7
  48. package/src/l3/initfirst.ts +27 -9
  49. package/src/l3/intcell.ts +97 -0
  50. package/src/l3/mentions.ts +10 -4
  51. package/src/l3/narrowdecl.ts +218 -0
  52. package/src/l3/nearbase.ts +10 -5
  53. package/src/l3/parkfirst.ts +8 -5
  54. package/src/l3/pollguard.ts +3 -2
  55. package/src/l3/ptrcell.ts +357 -0
  56. package/src/l3/regspell.ts +17 -3
  57. package/src/l3/reindex.ts +10 -7
  58. package/src/l3/symbol-refs.ts +104 -23
  59. package/src/l3/tailmerge.ts +6 -19
  60. package/src/l3/typing.ts +137 -25
  61. package/src/l3/unreduce.ts +4 -4
  62. package/src/l3/volstore.ts +96 -7
  63. package/src/mangle.ts +11 -1
  64. package/src/pattern/engine.ts +19 -16
  65. package/src/pipeline.ts +57 -12
  66. package/src/proto-context.ts +788 -0
  67. package/src/proto.ts +689 -50
  68. package/src/raise/arrays.ts +4 -3
  69. package/src/raise/declared-volatile.ts +131 -0
  70. package/src/raise/divpow2.ts +2 -2
  71. package/src/raise/errors.ts +17 -0
  72. package/src/raise/floathelpers.ts +243 -0
  73. package/src/raise/globalshape.ts +11 -5
  74. package/src/raise/gvn.ts +26 -5
  75. package/src/raise/narrow.ts +29 -2
  76. package/src/raise/narrowlocal.ts +11 -6
  77. package/src/raise/offsetnames.ts +510 -0
  78. package/src/raise/pairparams.ts +81 -0
  79. package/src/raise/paramwidth.ts +7 -0
  80. package/src/raise/pre-recovery.ts +78 -4
  81. package/src/raise/recover.ts +14 -10
  82. package/src/raise/retsink.ts +1 -1
  83. package/src/raise/shortcircuit.ts +41 -19
  84. package/src/raise/softdiv.ts +21 -23
  85. package/src/raise/struct-arrays.ts +5 -4
  86. package/src/raise/structs.ts +311 -98
  87. package/src/raise/truncload.ts +366 -0
  88. package/src/raise/widehelpers.ts +106 -0
  89. package/src/rank-declare.ts +30 -5
  90. package/src/rank-variations.ts +36 -8
  91. package/src/rank.ts +276 -80
  92. package/src/runtime-helpers.ts +287 -0
  93. package/src/stillborn.ts +289 -0
  94. package/src/structure/analysis.ts +1285 -448
  95. package/src/structure/hazards.ts +633 -58
  96. package/src/structure/local-statics.ts +381 -0
  97. package/src/structure/namecoalesce.ts +1 -1
  98. package/src/structure/redundant-test.ts +99 -0
  99. package/src/structure/structure.ts +1772 -395
  100. package/src/structure/switch-recover.ts +159 -78
  101. package/src/symbols.ts +38 -3
  102. package/src/target.ts +662 -59
  103. package/src/trace.ts +44 -24
  104. package/src/variation-definitions.ts +119 -1
  105. package/src/variation-tokens.ts +20 -6
package/src/ir/alias.ts CHANGED
@@ -86,6 +86,35 @@ export function globalCellOf(defs: Map<Value, Op>, addr: Value, off: number): Gl
86
86
  return null;
87
87
  }
88
88
 
89
+ /**
90
+ * The named global an address value is DERIVED FROM, through defs alone — `gaddr`, an `add` where
91
+ * exactly one side reaches a name, and the LEFT side of a `sub`, whose right side is a distance and
92
+ * never a second object. Where {@link globalCellOf} asks which CELL an
93
+ * address denotes and answers null the moment a runtime term enters, this asks only which OBJECT
94
+ * it reaches, which a runtime index does not change: `gVolArr[i]` names no cell and one object.
95
+ *
96
+ * So it answers a DECLARATION question — volatile, const, the qualifiers that hold for every byte
97
+ * of the object — and never an aliasing one: two accesses this gives the same name are not thereby
98
+ * the same cell. Null when nothing reaches a name, and when two sides do (a sum of two `gaddr`s is
99
+ * a base the defs cannot tell apart); null is UNKNOWN rather than "no global", so a caller that
100
+ * refuses on a declared property must read it as "the map cannot say".
101
+ */
102
+ export function globalBaseOf(defs: Map<Value, Op>, addr: Value): string | null {
103
+ const d = defs.get(addr);
104
+ if (d?.opcode === 'gaddr') {
105
+ return d.attrs.sym as string;
106
+ }
107
+ if (d?.opcode === 'add' && d.operands.length === 2) {
108
+ const a = globalBaseOf(defs, d.operands[0]);
109
+ const b = globalBaseOf(defs, d.operands[1]);
110
+ return a !== null && b !== null ? null : (a ?? b);
111
+ }
112
+ if (d?.opcode === 'sub' && d.operands.length === 2) {
113
+ return globalBaseOf(defs, d.operands[0]);
114
+ }
115
+ return null;
116
+ }
117
+
89
118
  /**
90
119
  * The NUMERIC address an access denotes, resolved through defs alone — a literal base, or the sum
91
120
  * of two literals — plus the access's own `off`. Null when the address
package/src/ir/core.ts CHANGED
@@ -56,8 +56,73 @@ export interface Fn {
56
56
  /** L1 SIDE DATA (see {@link ParamEvidence}); set by the SSA builder, `undefined` on parsed IR.
57
57
  * REQUIRED-but-possibly-undefined for exactly the reason `writeOrder` and `slotHomes` are. */
58
58
  paramEvidence: ParamEvidence | undefined;
59
+ /** L1 SIDE DATA (see {@link LocalObjects}); set by a frontend that recovered one, `undefined` on
60
+ * every other `Fn`. REQUIRED-but-possibly-undefined for exactly the reason `writeOrder` is. */
61
+ localObjects: LocalObjects | undefined;
59
62
  }
60
63
 
64
+ /** Where a function-scope static lives, which decides how its definition is spelled: `rodata` is
65
+ * `const` with an initializer, `data` has an initializer, `bss` has none. */
66
+ export type LocalObjectSection = 'rodata' | 'data' | 'bss';
67
+
68
+ /** One function-scope static, as the target defines it (frontend/local-object.ts). */
69
+ export interface LocalObject {
70
+ /** the name the source wrote — the linker name without the compiler's counter */
71
+ name: string;
72
+ /** the linker name the target carries (`tide.3`, `sprHideTbl$797`) */
73
+ symbol: string;
74
+ /** the counter in that name, which is the order the source declared the function's statics in
75
+ * (frontend/reloc-symbol.ts `localStaticName`) */
76
+ order: number;
77
+ section: LocalObjectSection;
78
+ /** bytes */
79
+ size: number;
80
+ /** the initial contents in target byte order; absent exactly for a `bss` object */
81
+ bytes?: Uint8Array;
82
+ bigEndian: boolean;
83
+ /** What an assembler LISTING shows of an initialized definition beyond its bytes, absent where
84
+ * the target is an object file, which keeps no directives. */
85
+ directives?: LocalObjectDirectives;
86
+ /** What an object FILE records of where the object sits, absent for an assembler listing, whose
87
+ * `.align` is among its directives. */
88
+ placement?: LocalObjectPlacement;
89
+ }
90
+
91
+ /** What an object file records of where a static sits. */
92
+ export interface LocalObjectPlacement {
93
+ /** the alignment the compiler gave the object (mwcc's `.comment` record for its symbol) */
94
+ align: number;
95
+ /** the object file's own section (`.sbss`, where {@link LocalObject.section} says `bss`) */
96
+ section: string;
97
+ /** its offset in that section */
98
+ offset: number;
99
+ }
100
+
101
+ /** An initialized static's data directives, as the compiler wrote them. The compiler writes each
102
+ * scalar of the initializer with the directive of its width, and aligns the object to its type's
103
+ * alignment; both are facts about the source's TYPE that the bytes alone have lost. */
104
+ export interface LocalObjectDirectives {
105
+ /** the alignment the object was placed at, in bytes (`.align N` ahead of the label: 2^N; none: 1) */
106
+ align: number;
107
+ /** the width every value directive writes, when they all agree (`.word` → 4); padding does not count */
108
+ unit?: number;
109
+ /** the directives share a `unit` narrower than a word and one wrote a negative value, which the
110
+ * compiler only does for a signed element type */
111
+ negative: boolean;
112
+ /** every value directive writes a string (`.ascii`), as the compiler writes a string literal —
113
+ * whole, or as the elements of an array or struct of strings */
114
+ string: boolean;
115
+ }
116
+
117
+ /** The function-scope statics a function DEFINES, keyed by {@link LocalObject.symbol} — the object a
118
+ * `gaddr` of that name denotes. The IR keeps the linker name because it carries a `.` or `$` that
119
+ * no C identifier and no declared symbol has, so a static can never be one name with a global of
120
+ * its source name (a symbol map's `tide` beside `tide.3`); structure spells it by its source name.
121
+ * A `gaddr` names a symbol some declaration elsewhere describes; these have none, because the
122
+ * source defined them inside the function, so the definition travels with the function to the
123
+ * emitter, which writes it in the body (structure/local-statics.ts). */
124
+ export type LocalObjects = ReadonlyMap<string, LocalObject>;
125
+
61
126
  /** What the machine's own object shows about each ENTRY PARAMETER, beyond the value graph — two
62
127
  * observations the lift destroys, recorded so raise/paramwidth.ts can read them. Every entry
63
128
  * parameter of a lifted function has an entry; a `Fn` with no map is one nobody measured.
@@ -67,9 +132,11 @@ export interface Fn {
67
132
  * `compilerBehaviors.narrowParamWitness` decides what the pair means, and the disassemblies for
68
133
  * each value live there.
69
134
  *
70
- * `deadHome` — THE STORE DOES NOT REACH THE IR. Both slot-modelling frontends spell a word
135
+ * `deadHome` — THE STORE DOES NOT REACH THE IR. Every slot-modelling frontend spells a word
71
136
  * sp-relative store as a write to the SSA key `sp@k` instead of a `store` op (`stackSlotKey`), so
72
- * a slot nothing reloads has no reader, no op and no value: it simply is not there by L1. A
137
+ * a slot nothing reloads has no reader, no op and no value: it simply is not there by L1. MIPS and
138
+ * Thumb lift such a store. PowerPC refuses it, because an unread store may be a call's outgoing
139
+ * stack argument (frontend/ppc.ts), so a PowerPC lift never reports a dead home. A
73
140
  * parameter stored to two slots, one of them reloaded, still counts — the dead store happened —
74
141
  * and the reader's own gates decide what a parameter with that much traffic may become.
75
142
  *
@@ -230,6 +297,37 @@ export function mkOp(opcode: Opcode, o: Partial<Op> = {}): Op {
230
297
  };
231
298
  }
232
299
 
300
+ // ── a call's callee: a NAME (`attrs.target`) or, for a call through a register, a VALUE ─────────
301
+ //
302
+ // An INDIRECT call carries `indirect: true` and no `target`, and calls the function whose address
303
+ // is its LAST operand. The arguments come first on both forms, so `operands[k]` is argument k on
304
+ // either and a reader that indexes an argument needs no case; a reader that takes the WHOLE operand
305
+ // list as the arguments must go through `callArgs`.
306
+
307
+ /** The function an indirect call calls, or undefined for a call that names its callee. */
308
+ export function calleeValue(op: Op): Value | undefined {
309
+ return op.opcode === 'call' && op.attrs.indirect === true ? op.operands[op.operands.length - 1] : undefined;
310
+ }
311
+
312
+ /** The callee a call names, or undefined for an indirect one. */
313
+ export function calleeName(op: Op): string | undefined {
314
+ return op.opcode === 'call' && typeof op.attrs.target === 'string' ? op.attrs.target : undefined;
315
+ }
316
+
317
+ /** A call's arguments, the callee value of an indirect call aside. */
318
+ export function callArgs(op: Op): Value[] {
319
+ return calleeValue(op) === undefined ? op.operands : op.operands.slice(0, -1);
320
+ }
321
+
322
+ /** Cut a call down to its first `n` arguments, keeping an indirect call's callee. */
323
+ export function truncateCallArgs(op: Op, n: number): void {
324
+ const callee = calleeValue(op);
325
+ op.operands.length = n;
326
+ if (callee !== undefined) {
327
+ op.operands.push(callee);
328
+ }
329
+ }
330
+
233
331
  /** A block's last op — its terminator on well-formed IR, `undefined` on a block with no ops.
234
332
  *
235
333
  * `ir/verify.ts` rejects an empty block and every entry path verifies before raising, so the
@@ -412,7 +510,7 @@ export function forwardingTarget(b: Block): Block {
412
510
  * a CFG fact a parameter for one caller's extra edges — so it stays where it is, and what this
413
511
  * paragraph buys is that the next reader looking for a fourth copy knows which of the two the
414
512
  * third one is. */
415
- export function mergeClasses(fn: Fn): Map<Value, readonly Value[]> {
513
+ export function mergeClasses(fn: Pick<Fn, 'blocks'>): Map<Value, readonly Value[]> {
416
514
  const parent = new Map<Value, Value>();
417
515
  const find = (v: Value): Value => {
418
516
  let r = v;
@@ -0,0 +1,248 @@
1
+ // asmlift IR — the access discipline: which ops the recompile must run where, and as often as, the
2
+ // asm ran them, and the questions every pass asks about that.
3
+ //
4
+ // Two kinds of fact meet here. The registry's flags (ir/opcodes.ts `OpSig`: `effects`, `reads`,
5
+ // `traps`) are facts about an OPCODE. A PLACEMENT is a fact about one OP, decided once by the pass
6
+ // that knew it: `call` is its own opcode; `helper` is the stamp `runtime-helpers.ts` `helperOp` puts
7
+ // on a value op the asm computed by calling a runtime routine; `device` is the `volatile` the
8
+ // frontend's device-pin pass (frontend/device-pins.ts) puts on a memory access; `declared` is the
9
+ // stamp raise/declared-volatile.ts puts on an access of an object the symbol map declares volatile —
10
+ // a read only where the compiler could have made it of a `volatile` — as the lift is made and again
11
+ // when structuring starts. Every question below takes the op, so a pass
12
+ // asks one question instead of re-deriving the combination, and `PLACEMENT_ANSWERS` is the one
13
+ // place a placement's answers are written, the attrs a rebuilt op carries it by among them.
14
+ //
15
+ // A new placement is three edits: its `Placement` member with a `PLACEMENT_ANSWERS` row, the
16
+ // `placedAt` clause that reads its stamp, and its key in contracts.ts `assertEffectsPreserved`,
17
+ // whose switch over `placedAt` does not compile until the member is keyed or answered as uncounted.
18
+ //
19
+ // NAMED RESIDUE. The sites below ask a registry question where a placement could change the answer,
20
+ // and keep the registry's answer. DELIBERATE: the placement does not bear on what the site asks.
21
+ // GAP: it does, and some input gets a wrong answer.
22
+ //
23
+ // `speculationUnsafe` answers no for a plain read — DELIBERATE, argued at the function — and for
24
+ // a trapping divide — GAP: raise/shortcircuit.ts hoists one the structurer may name above its guard.
25
+ // A QUALIFIED read is one `qualified` answers yes for.
26
+ // Two qualified reads commuting (`orderSensitive` without `effectful` on either side):
27
+ // pattern/engine.ts `reordersUnsequenced` — DELIBERATE: the structurer's barrier scan names the
28
+ // first of two qualified reads the fold would join in one expression.
29
+ // structure/hazards.ts `movesPast` — DELIBERATE: the analysis names a qualified read another
30
+ // qualified read stands between it and its render, so the scan never weighs two.
31
+ // `effectful` as the barrier a read may not cross, without `isBarrier`'s helper and qualified-read
32
+ // clauses (structure/analysis.ts):
33
+ // structure/analysis.ts `writeBetween`, `standsOnMovableRead`, the multi-render rule — DELIBERATE:
34
+ // each asks about a memory write, and a qualified read writes nothing.
35
+ // The `call` placement alone, in structure/analysis.ts:
36
+ // `callPos` — DELIBERATE: it asks which values live across the registers a call clobbers.
37
+ // `readCone`, `isHomeableDef`, `copyInterdependentValues` — DELIBERATE: a qualified read is a
38
+ // `load` there, which each already stops at.
39
+ // `effectful` as block purity — structure.ts's exit ownership, preheader claim, header purity,
40
+ // copied `ret` target, `ends` and latch effect roots; structure/hazards.ts `testSkipsAnEffect`;
41
+ // structure/redundant-test.ts `testRereadsOnly` — DELIBERATE: a qualified read renders with its
42
+ // block, once on each path that runs the block, or declines upstream.
43
+ // raise/divpow2.ts's bias arm — GAP: the arm is deleted, and a dead qualified read in it with it.
44
+ // And three accesses the deciders leave unplaced, where a placement would change an answer — GAP:
45
+ // a device store a pinned read of the same cell re-reads in a loop, under the per-object policy
46
+ // (frontend/device-pins.ts's header argues it);
47
+ // a read of a map-declared volatile object through a base that reaches no name — a pointer
48
+ // walked along the object in a loop, a join of two different addresses in it
49
+ // (raise/declared-volatile.ts);
50
+ // a read the stamp cannot place by byte — a runtime index, an `aload` — of an object the map
51
+ // qualifies only some members of, which may reach a plain member (symbols.ts
52
+ // `declaresVolatile`).
53
+ import type { Op } from './core';
54
+ import { MEM_BASE_OPS, type OpSig, opSig } from './opcodes';
55
+
56
+ /** Why an op must run where the asm ran it. */
57
+ export type Placement = 'call' | 'helper' | 'device' | 'declared';
58
+
59
+ /** Where a qualified access's spelling carries its `volatile`: on a cast of its own address, or
60
+ * through the declaration of the object it names. */
61
+ export type Qualifier = 'cast' | 'declaration';
62
+
63
+ /** The attr raise/declared-volatile.ts stamps a `declared` access with. */
64
+ export const DECLARED_VOLATILE = 'declaredVolatile';
65
+
66
+ interface PlacementAnswers {
67
+ /** spelled even when nothing reads the result */
68
+ keptWhenDead: boolean;
69
+ /** never run on a path that did not run it */
70
+ speculationUnsafe: boolean;
71
+ /** each render is one more execution, so it renders once on every path the asm ran it on */
72
+ counted: boolean;
73
+ /** an access whose spelling must carry `volatile`, and where it carries it */
74
+ qualifiedBy: Qualifier | null;
75
+ /** the attrs that put this placement on an op a pass builds to stand for `from` */
76
+ carried: (from: Op) => Op['attrs'];
77
+ }
78
+
79
+ const PLACEMENT_ANSWERS: Readonly<Record<Placement, PlacementAnswers>> = {
80
+ // The registry's `effects` flag gives a call the first two answers on its own, and its opcode is
81
+ // its placement, which an op built to stand for it does not inherit.
82
+ call: { keptWhenDead: true, speculationUnsafe: true, counted: true, qualifiedBy: null, carried: () => ({}) },
83
+ // The value the helper computes is pure: nothing observes a divide nobody reads, a fold that
84
+ // moves one under a guard drops the stamp (`forgetHelperPlacement`), and agbcc computes one
85
+ // spelled twice once (`a / n + a / n` is one `bl __divsi3`).
86
+ helper: {
87
+ keptWhenDead: false,
88
+ speculationUnsafe: false,
89
+ counted: false,
90
+ qualifiedBy: null,
91
+ carried: (from) => ({ helper: from.attrs.helper }),
92
+ },
93
+ // The access is what is observable, not the value it yields, so its spelling is qualified.
94
+ device: {
95
+ keptWhenDead: true,
96
+ speculationUnsafe: true,
97
+ counted: true,
98
+ qualifiedBy: 'cast',
99
+ carried: () => ({ volatile: true }),
100
+ },
101
+ declared: {
102
+ keptWhenDead: true,
103
+ speculationUnsafe: true,
104
+ counted: true,
105
+ qualifiedBy: 'declaration',
106
+ carried: () => ({ [DECLARED_VOLATILE]: true }),
107
+ },
108
+ };
109
+
110
+ /** The placement an op carries, or null for an op that may render wherever its value is used. */
111
+ export function placedAt(op: Op): Placement | null {
112
+ if (op.opcode === 'call') {
113
+ return 'call';
114
+ }
115
+ if (MEM_BASE_OPS.has(op.opcode) && op.attrs.volatile === true) {
116
+ return 'device';
117
+ }
118
+ if (MEM_BASE_OPS.has(op.opcode) && op.attrs[DECLARED_VOLATILE] === true) {
119
+ return 'declared';
120
+ }
121
+ // An `opaque` carries `helper` too, naming the call nothing could fold (`refuseUnmodelledHelpers`);
122
+ // it is no value op, and its `effects` flag already places it.
123
+ if (op.opcode !== 'opaque' && typeof op.attrs.helper === 'string') {
124
+ return 'helper';
125
+ }
126
+ return null;
127
+ }
128
+
129
+ const answered = (op: Op, q: 'keptWhenDead' | 'speculationUnsafe' | 'counted'): boolean => {
130
+ const p = placedAt(op);
131
+ return p !== null && PLACEMENT_ANSWERS[p][q];
132
+ };
133
+
134
+ /** Where this access's spelling carries the `volatile` the recompile needs to make it where, and as
135
+ * often as, the asm did — or null for an op whose spelling needs none. A spelling through the
136
+ * declaration needs a cast's qualifier too wherever it reaches the object through a cast, since a
137
+ * cast's pointee drops the declaration's qualifiers (l3/volstore.ts `castOffDeclaration`). */
138
+ export function qualifiedBy(op: Op): Qualifier | null {
139
+ const p = placedAt(op);
140
+ return p === null ? null : PLACEMENT_ANSWERS[p].qualifiedBy;
141
+ }
142
+
143
+ /** Must this access's spelling carry `volatile`? */
144
+ export function qualified(op: Op): boolean {
145
+ return qualifiedBy(op) !== null;
146
+ }
147
+
148
+ const sigOf = (op: Op): OpSig | undefined => opSig(op.opcode);
149
+
150
+ /** Does each render of this op execute it — a call, or a qualified access? Such an op is spelled
151
+ * once on every path the asm ran it on, at the position it ran: named where a second use, an edge
152
+ * or another block would render it again or elsewhere. */
153
+ export function counted(op: Op): boolean {
154
+ return answered(op, 'counted');
155
+ }
156
+
157
+ /** Has an observable side effect: a memory write, a call, an unmodelled instruction. The question a
158
+ * block-purity test asks, and the write a memory read may not cross. */
159
+ export function effectful(op: Op): boolean {
160
+ return sigOf(op)?.effects === true;
161
+ }
162
+
163
+ /** May a dead result be deleted? Registered, no effect, not control flow, and not an op whose
164
+ * placement keeps it. */
165
+ export function deletableWhenDead(op: Op): boolean {
166
+ const sig = sigOf(op);
167
+ return !!sig && !sig.effects && !sig.terminator && !answered(op, 'keptWhenDead');
168
+ }
169
+
170
+ /** Must the structurer still SPELL this op when nothing consumes its result — an effect, or a
171
+ * memory read?
172
+ *
173
+ * The read half points the opposite way from `deletableWhenDead`, and both are right. That one is
174
+ * the C claim: nothing observes a load nobody reads. This one is the COMPILER claim: an optimizing
175
+ * compiler deletes every dead read it is allowed to delete, so one still in the target is evidence
176
+ * the source's access was `volatile`. A yes here only says the op may not be dropped silently;
177
+ * whether a `volatile` can reach the access is an ADDRESS-level question the caller asks
178
+ * (structure.ts `volatileQualifiable`), and a read it answers no to is dropped.
179
+ *
180
+ * Its own function and not `orderSensitive`, though the two agree on every registered opcode: they
181
+ * ask different questions, and a registry change that splits them is a decision to make at each. */
182
+ export function spelledWhenDead(op: Op): boolean {
183
+ const sig = sigOf(op);
184
+ return !!sig && (!!sig.effects || !!sig.reads);
185
+ }
186
+
187
+ /** May this op NOT be speculated — run on a path that did not run it? An effect, or an op whose
188
+ * placement forbids it.
189
+ *
190
+ * A plain memory read answers no, and that is the answer worth arguing. The consumer is
191
+ * raise/shortcircuit.ts, which hoists an arm's body into the block above, and the structurer
192
+ * inlines an unnamed value back into the `&&`/`||` right-hand side, where C's own short circuit
193
+ * re-guards it. Answering yes for plain reads refuses every connective whose arm reads memory,
194
+ * which branch-shortcircuit.test.ts's `refusals` pin. A qualified read is the read the re-guard
195
+ * argument does not cover — it moves an access, not a value — so its placement answers yes.
196
+ *
197
+ * The trapping divides answer no as well, and there the re-guard argument does NOT carry: a hoisted
198
+ * `sdiv` the structurer NAMES becomes an unconditional statement. `reevalUnsafe` answers yes for
199
+ * them, so the pre-update sink is not exposed to it.
200
+ *
201
+ * THE EXEMPTION IS NOT TRANSFERABLE. A consumer asking "would gcc have speculated this arm above a
202
+ * compare", where nothing re-guards anything, must refuse single-load arms (`gcc/jump.c:483`'s
203
+ * `! may_trap_p`); `raise/narrowlocal.ts` asks `reevalUnsafe`, and asking this instead loses
204
+ * `synthetic:mergeldcast:agbcc` its byte match. Ask this only where a C-level re-guard at the new
205
+ * point actually holds. */
206
+ export function speculationUnsafe(op: Op): boolean {
207
+ return effectful(op) || answered(op, 'speculationUnsafe');
208
+ }
209
+
210
+ /** Does this op's answer depend on WHERE it runs on one path? An effect (its order against other
211
+ * effects is observable) or a memory read (it answers whichever stores ran before it). */
212
+ export function orderSensitive(op: Op): boolean {
213
+ const sig = sigOf(op);
214
+ return !!sig && (!!sig.effects || !!sig.reads);
215
+ }
216
+
217
+ /** May this op NOT be re-evaluated at another program point — order-sensitive, or trapping? The trap
218
+ * half separates it from `orderSensitive`: it matters only when the new point can be reached on a
219
+ * path the old one was not. A collapsed switch re-renders a test block's ops at their uses, each
220
+ * dominated by the def, so on a subset of the original paths — a narrowing, never a speculation —
221
+ * and asks `orderSensitive` instead. */
222
+ export function reevalUnsafe(op: Op): boolean {
223
+ const sig = sigOf(op);
224
+ return !!sig && (!!sig.effects || !!sig.reads || !!sig.traps);
225
+ }
226
+
227
+ /** Drop the `helper` placement from an op a pass moves out of the block the asm called it in, and
228
+ * return the op. The stamp is read as WHERE the call ran, so an op moved elsewhere must stop
229
+ * claiming it and renders as the pure value it computes. raise/shortcircuit.ts hoists a `&&`/`||`
230
+ * guarded arm's body above the branch, and there C's short circuit re-guards the op at its use:
231
+ * agbcc's `if (a > 0 && k / n != 0)` runs the `bl __divsi3` under the first compare, and named at
232
+ * its hoisted def it would divide on the path the `&&` skips. */
233
+ export function forgetHelperPlacement(op: Op): Op {
234
+ if (placedAt(op) === 'helper') {
235
+ const { helper: _, ...rest } = op.attrs;
236
+ op.attrs = rest;
237
+ }
238
+ return op;
239
+ }
240
+
241
+ /** `attrs`, plus the placement of `from`, for an op a pass builds to stand for `from`: a memory
242
+ * access keeps its `volatile` or its `declared` stamp, a value op its `helper`. Every op rebuilder
243
+ * carries the stamps through here, so a placement decided once is not lost to a rebuild that
244
+ * forgot to copy it. */
245
+ export function carryDiscipline(from: Op, attrs: Op['attrs']): Op['attrs'] {
246
+ const p = placedAt(from);
247
+ return p === null ? attrs : { ...attrs, ...PLACEMENT_ANSWERS[p].carried(from) };
248
+ }
@@ -0,0 +1,47 @@
1
+ // asmlift IR — a double literal: the bit pattern the IR carries it as, and the C that spells it.
2
+ //
3
+ // THE IR CARRIES THE BITS, NOT A NUMBER, because a number has one value that is two doubles:
4
+ // `-0` prints and serializes as `0`, so a literal keyed or dumped by its number would be the
5
+ // other zero. Sixteen hex digits name exactly one double, NaN payloads included.
6
+
7
+ const view = new DataView(new ArrayBuffer(8));
8
+
9
+ /** The bit pattern, as sixteen hex digits, of the double whose high word (sign and exponent) is
10
+ * `hi` and low word is `lo`. */
11
+ export function doubleBits(hi: number, lo: number): string {
12
+ return (hi >>> 0).toString(16).padStart(8, '0') + (lo >>> 0).toString(16).padStart(8, '0');
13
+ }
14
+
15
+ /** Whether `bits` is a double's bit pattern: sixteen hex digits, as `doubleBits` spells one. A
16
+ * single's eight would name another number read as a double, so nothing here takes one. */
17
+ export const isDoubleBits = (bits: unknown): bits is string => typeof bits === 'string' && /^[0-9a-f]{16}$/.test(bits);
18
+
19
+ /** The double a bit pattern names. */
20
+ export function doubleOf(bits: string): number {
21
+ if (!isDoubleBits(bits)) {
22
+ throw new Error(`'${bits}' is not a double's bit pattern`);
23
+ }
24
+ view.setUint32(0, parseInt(bits.slice(0, 8), 16));
25
+ view.setUint32(4, parseInt(bits.slice(8, 16), 16));
26
+ return view.getFloat64(0);
27
+ }
28
+
29
+ /** The C spelling of a finite double: the shortest decimal that reads back as the same double
30
+ * (ECMAScript's Number::toString, whose digits are the fewest that do), with a `.0` where it has
31
+ * neither a point nor an exponent, so C reads a `double` and not an `int`, and `-0.0` for the
32
+ * negative zero.
33
+ *
34
+ * THAT IT READS BACK IS A PREMISE ABOUT THE COMPILER, and the one producer of a literal
35
+ * (`raise/floathelpers.ts`) runs only where the target states it
36
+ * (`compilerBehaviors.roundTripsDoubleLiterals`, which cites agbcc's source for it). */
37
+ export function doubleLiteral(bits: string): string {
38
+ const x = doubleOf(bits);
39
+ if (!Number.isFinite(x)) {
40
+ throw new Error(`the double ${bits} is not finite and has no C literal`);
41
+ }
42
+ if (Object.is(x, -0)) {
43
+ return '-0.0';
44
+ }
45
+ const s = String(x);
46
+ return /[.e]/.test(s) ? s : `${s}.0`;
47
+ }