@asmlift/core 0.1.0 → 0.3.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/README.md +18 -23
- package/package.json +1 -1
- package/src/backend/cfamily.ts +30 -4
- package/src/contracts.ts +30 -0
- package/src/declare.ts +225 -0
- package/src/detect.ts +5 -2
- package/src/frontend/format.ts +11 -3
- package/src/frontend/frontend.ts +12 -2
- package/src/frontend/mips.ts +206 -2
- package/src/frontend/splat.ts +305 -0
- package/src/frontend/thumb.ts +119 -6
- package/src/l3/ast.ts +8 -2
- package/src/l3/symbol-refs.ts +61 -0
- package/src/l3/typing.ts +4 -0
- package/src/macros.ts +126 -0
- package/src/pipeline.ts +15 -4
- package/src/proto.ts +55 -0
- package/src/raise/magicdiv.ts +1 -1
- package/src/rank.ts +215 -76
- package/src/structure/structure.ts +462 -45
- package/src/symbols.ts +426 -0
- package/src/trace.ts +8 -2
|
@@ -37,11 +37,19 @@
|
|
|
37
37
|
// whose exit copies would clobber, switch fall-through, and mixed-entry self-loops (a guarded
|
|
38
38
|
// header also entered by a plain br).
|
|
39
39
|
import { Block, Fn, Op, Value, defOpMap, successorsOf } from '../ir/core';
|
|
40
|
-
import { type IrType, T } from '../ir/types';
|
|
40
|
+
import { type IrType, T, scalarTypeForAccess, typeEquals } from '../ir/types';
|
|
41
41
|
import { BinOp, Expr, SFn, Stmt, SwitchCase, exprChildren, mapExprChildren } from '../l3/ast';
|
|
42
42
|
import { exprCType, ptrElemBytes } from '../l3/typing';
|
|
43
43
|
import { returnType } from '../raise/recover';
|
|
44
44
|
import { collectStructs } from '../raise/structs';
|
|
45
|
+
import {
|
|
46
|
+
type DeclaredField,
|
|
47
|
+
type SymbolInfo,
|
|
48
|
+
type SymbolStructField,
|
|
49
|
+
declaredFields,
|
|
50
|
+
isArrayField,
|
|
51
|
+
pointeeFields,
|
|
52
|
+
} from '../symbols';
|
|
45
53
|
import { analyze } from './analysis';
|
|
46
54
|
import { makeLoopHazards, updateWriteSet } from './hazards';
|
|
47
55
|
import { analyzeLoops, dominators } from './loops';
|
|
@@ -59,44 +67,267 @@ import { makeSwitchRecovery } from './switch-recover';
|
|
|
59
67
|
// TODAY — carrying the struct name (resolved against SFn.structs) is the same move as width and
|
|
60
68
|
// the named follow-up; until then no backend pays a tax for the tree cast (Pascal loud-fails
|
|
61
69
|
// `field` regardless, C++ falls through its leaf hook to the shared C spelling).
|
|
70
|
+
// `&gSym`, possibly wearing the value-context integer cast the additive lowering adds
|
|
71
|
+
// (`(u32)&gSym` — see lowerDef's addr-intify): both spell the same link-time constant, so the
|
|
72
|
+
// fold rules match through the cast and every access that CAN spell a named element still does.
|
|
73
|
+
// WIDTH 32 ONLY — a NARROWING cast (`(u8)&gSym`, from a zext/sext lowering) is a different
|
|
74
|
+
// VALUE (`addr & 0xFF`), and folding through it would read the named global at a wrong address
|
|
75
|
+
// (the adversarial round's probe: `*(u8*)(u8)&gSym` must keep its truncation, never become
|
|
76
|
+
// `*(u8*)&gSym` — let alone a confidently-named `gSym.field`).
|
|
77
|
+
function addrIn(e: Expr): Extract<Expr, { k: 'addr' }> | null {
|
|
78
|
+
if (e.k === 'addr') {
|
|
79
|
+
return e;
|
|
80
|
+
}
|
|
81
|
+
if (e.k === 'cast' && e.to.kind === 'int' && e.to.width === 32 && e.e.k === 'addr') {
|
|
82
|
+
return e.e;
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
62
87
|
// If `e` is a global address `&gSym` (optionally `+ index`), return the global name and the
|
|
63
88
|
// element index (byte residual divided by the access width). `&gSym` alone → idx const 0;
|
|
64
89
|
// `&gSym + i` → idx `i / width` (exact division only — a non-multiple residual is a mid-element
|
|
65
90
|
// access this whole-global spelling can't express, so it declines to null and the caller casts).
|
|
66
91
|
function globalOf(e: Expr, width: number): { name: string; idx: Expr } | null {
|
|
67
|
-
|
|
68
|
-
|
|
92
|
+
const top = addrIn(e);
|
|
93
|
+
if (top) {
|
|
94
|
+
return { name: top.name, idx: { k: 'const', value: 0 } };
|
|
69
95
|
}
|
|
70
96
|
if (e.k === 'bin' && e.op === '+') {
|
|
71
|
-
for (const [
|
|
97
|
+
for (const [side, other] of [
|
|
72
98
|
[e.l, e.r],
|
|
73
99
|
[e.r, e.l],
|
|
74
100
|
] as const) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
101
|
+
const addrSide = addrIn(side);
|
|
102
|
+
if (addrSide) {
|
|
103
|
+
const idx = elementIndex(other, width);
|
|
104
|
+
return idx ? { name: addrSide.name, idx } : null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// A BYTE residual read as an ELEMENT index of `elemSize`-wide elements, or null when it is not one
|
|
112
|
+
// — the residual then addresses mid-element and no whole-element spelling can express it, so the
|
|
113
|
+
// caller falls through to the honest cast forms. THE one copy of the rule, indexing the
|
|
114
|
+
// `&gSym`-based array spelling: width 1 → the byte residual IS the index; wider → a constant
|
|
115
|
+
// residual must divide exactly, and a non-constant one must already be element-scaled
|
|
116
|
+
// (`i * elemSize` / `i << log2(elemSize)`), which is exactly what the asm's own index scaling
|
|
117
|
+
// produced.
|
|
118
|
+
function elementIndex(residual: Expr, elemSize: number): Expr | null {
|
|
119
|
+
if (elemSize === 1) {
|
|
120
|
+
return residual;
|
|
121
|
+
}
|
|
122
|
+
if (residual.k === 'const') {
|
|
123
|
+
return residual.value % elemSize === 0 ? { k: 'const', value: residual.value / elemSize } : null;
|
|
124
|
+
}
|
|
125
|
+
if (residual.k === 'bin' && (residual.op === '*' || residual.op === '<<')) {
|
|
126
|
+
const factor =
|
|
127
|
+
residual.op === '<<'
|
|
128
|
+
? residual.r.k === 'const'
|
|
129
|
+
? 1 << residual.r.value
|
|
130
|
+
: 0
|
|
131
|
+
: residual.r.k === 'const'
|
|
132
|
+
? residual.r.value
|
|
133
|
+
: 0;
|
|
134
|
+
if (factor === elemSize) {
|
|
135
|
+
return residual.l;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** The symbol-map rendering context threaded into memAccess/arrayAccess: shape facts per
|
|
142
|
+
* global name, plus a callback registering a global's env type (so the bare `gSym[i]` spelling,
|
|
143
|
+
* which must pass the stride check uncast, does). Absent ⇒ today's spellings. */
|
|
144
|
+
interface SymRenderCtx {
|
|
145
|
+
info(name: string): SymbolInfo | undefined;
|
|
146
|
+
noteGlobal(name: string, type: IrType): void;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ── interior spelling through a POINTER-shaped global ────────────────────────────────────────
|
|
150
|
+
// A pointer global's VALUE is the address of the object the project's header says it points at.
|
|
151
|
+
// With that pointee's layout in the map, an access at a known offset is a NAMED member of it —
|
|
152
|
+
// `gPtr->member` — which is the source spelling; without it the access is byte arithmetic on the
|
|
153
|
+
// loaded cell (`((u8 *)gPtr + i)[16]`), honest but opaque. The address computed is IDENTICAL
|
|
154
|
+
// either way: `->member` adds the member's DWARF offset, which is the same constant the arithmetic
|
|
155
|
+
// added, and an index into an array member scales by the member's own element size, which the
|
|
156
|
+
// rules below require to equal the access width. Everything not provably that — a partial overlap,
|
|
157
|
+
// a width mismatch, an unnamed offset, a member whose declared signedness differs from the
|
|
158
|
+
// access's (an s8 read is ldrb+lsl+asr where u8 is ldrb alone), a missing layout — falls THROUGH
|
|
159
|
+
// to the cast forms. A member is never guessed.
|
|
160
|
+
|
|
161
|
+
/** The global named by a pointer global's VALUE as the additive lowering spells it: the bare
|
|
162
|
+
* `gPtr`, or that value wearing the byte-pointer / u32 cast that lowering adds (cast-then-add,
|
|
163
|
+
* see the `needsIntSpelling` / pointer-global arithmetic rules below). Both denote the same
|
|
164
|
+
* address and add BYTES to it, so both fold here; a cast to any other pointer type is NOT looked
|
|
165
|
+
* through — a `(u16 *)` base would re-scale everything added after it. */
|
|
166
|
+
function ptrGlobalValueName(x: Expr): string | null {
|
|
167
|
+
if (x.k === 'var') {
|
|
168
|
+
return x.name;
|
|
169
|
+
}
|
|
170
|
+
if (x.k === 'cast' && x.e.k === 'var') {
|
|
171
|
+
const t = x.to;
|
|
172
|
+
const bytePtr = t.kind === 'ptr' && t.to.kind === 'int' && t.to.width === 8;
|
|
173
|
+
return bytePtr || (t.kind === 'int' && t.width === 32) ? x.e.name : null;
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** A pointer global's value, the constant bytes added to it, and the at-most-one variable term. */
|
|
179
|
+
interface PtrGlobalBase {
|
|
180
|
+
name: string;
|
|
181
|
+
byte: number;
|
|
182
|
+
idx: Expr | null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Decompose an access base into "the VALUE of a map-declared POINTER global + a constant byte
|
|
186
|
+
* offset + at most ONE variable term": `gPtr`, `gPtr + K`, `(u8 *)gPtr + i`, `(u8 *)gPtr + (i <<
|
|
187
|
+
* 2) + K`. Null for anything else — two variable terms, no such global, a non-`+` operator —
|
|
188
|
+
* because only a single residual can be read as one member's index. */
|
|
189
|
+
function ptrGlobalBase(e: Expr, isPtrGlobal: (n: string) => boolean): PtrGlobalBase | null {
|
|
190
|
+
let name: string | null = null;
|
|
191
|
+
let byte = 0;
|
|
192
|
+
let idx: Expr | null = null;
|
|
193
|
+
let ok = true;
|
|
194
|
+
const visit = (x: Expr): void => {
|
|
195
|
+
if (!ok) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (x.k === 'bin' && x.op === '+') {
|
|
199
|
+
visit(x.l);
|
|
200
|
+
visit(x.r);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const global = ptrGlobalValueName(x);
|
|
204
|
+
if (global !== null && name === null && isPtrGlobal(global)) {
|
|
205
|
+
name = global;
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (x.k === 'const') {
|
|
209
|
+
byte += x.value;
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (idx !== null) {
|
|
213
|
+
ok = false;
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
idx = x;
|
|
217
|
+
};
|
|
218
|
+
visit(e);
|
|
219
|
+
return ok && name !== null ? { name, byte, idx } : null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Does a member declared at signedness `declared` read as EXACTLY the type the cast spelling this
|
|
223
|
+
* replaces would have produced — `scalarTypeForAccess(width, signed)`? That is the whole
|
|
224
|
+
* byte-exactness argument for naming a member instead of casting: same address, same read width,
|
|
225
|
+
* same C type, so every operator downstream compiles identically. A 4-byte access renders s32
|
|
226
|
+
* whatever the load said (the ISA has one word load), so only a SIGNED member may take its place;
|
|
227
|
+
* narrower accesses carry their own signedness and must match it. An undeclared signedness (the
|
|
228
|
+
* member is not a base type — a nested struct, an enum, a pointer) is never assumed to match.
|
|
229
|
+
* Compared against THE one copy of that rule (ir/types.ts) rather than restating it. */
|
|
230
|
+
function spellsAccessType(declared: boolean | undefined, width: number, signed: boolean): boolean {
|
|
231
|
+
if (declared === undefined) {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
return typeEquals(T.int(width * 8, declared), scalarTypeForAccess(width, signed));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** May a member be NAMED by an access of this direction, given the qualifiers on its declaration?
|
|
238
|
+
* The named spelling REPLACES a cast through `(u8 *)`, which carries no qualifier at all, so a
|
|
239
|
+
* qualifier the name reintroduces changes what the compiler emits:
|
|
240
|
+
* • `volatile` makes the access observable — the load may no longer be folded or reordered,
|
|
241
|
+
* which is a different instruction sequence (measured: 6 insns where the cast form was 5);
|
|
242
|
+
* • `const` under a STORE is a hard error, where the cast form merely cast the qualifier away.
|
|
243
|
+
* Either way the honest spelling is the cast form, so the member simply is not nameable here. */
|
|
244
|
+
function memberQualsAllow(f: SymbolStructField, containerConst: boolean | undefined, isStore: boolean): boolean {
|
|
245
|
+
if (f.volatile) {
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
return !(isStore && (f.const || containerConst));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// WHY THERE IS NO INDEXED `gPtr->arr[i]` SPELLING.
|
|
252
|
+
//
|
|
253
|
+
// Naming a member is only allowed where it is byte-identical to the cast form it replaces, and
|
|
254
|
+
// for the INDEXED form that was measured to be false. Against agbcc, `gPtr->arr[i]` and
|
|
255
|
+
// `((u8 *)gPtr + i)[K]` differ at EVERY nonzero K and for every width and direction — agbcc does
|
|
256
|
+
// not reassociate `(base + K) + i` into `(base + i) + K`, so it materialises the offset instead of
|
|
257
|
+
// folding it into the load (`adds r1, #16`; +2 code bytes at width 1, +4 at widths 2 and 4). At
|
|
258
|
+
// K = 0 the two still differ for widths 2 and 4, where the commutative `adds` picks a different
|
|
259
|
+
// destination register. The single agbcc case that did measure identical — width 1 at K = 0 —
|
|
260
|
+
// survives only a BARE index in a function with ONE such access: `i & 255`, `i + 1`, `i >> 2` and
|
|
261
|
+
// a second access that lets the cast side CSE its base all break it. A spelling rule decides one
|
|
262
|
+
// expression at a time and cannot see the neighbouring access that changes the answer, so there is
|
|
263
|
+
// no local gate that makes this form safe. (Both MIPS targets accept it freely, but core is
|
|
264
|
+
// target-agnostic — it cannot condition on the compiler it is emitting for.)
|
|
265
|
+
//
|
|
266
|
+
// The CONSTANT-offset form below is the opposite case, and is emitted unconditionally: measured
|
|
267
|
+
// identical on all three targets for widths 1/2/4, loads and stores, at every offset tested up to
|
|
268
|
+
// 4096 — including offsets past Thumb's immediate range, and under multi-member, across-a-call
|
|
269
|
+
// and in-a-loop shapes. It is one load whose member offset becomes the same immediate the cast
|
|
270
|
+
// form used, and unlike the indexed form it composes.
|
|
271
|
+
|
|
272
|
+
/** The pointee a global's value may be spelled through: the members the declaration synthesis
|
|
273
|
+
* DECLARES. Null when nothing may be named through it — THE shared gate (symbols.ts
|
|
274
|
+
* pointeeFields), so core never names a member that synthesis would not declare, and never sees a
|
|
275
|
+
* member synthesis drops (a union alias behind the first view at that offset). A VOLATILE pointee
|
|
276
|
+
* declines outright: every named access through it would be a volatile access where the cast form
|
|
277
|
+
* it replaces was plain. */
|
|
278
|
+
function spellablePointee(
|
|
279
|
+
name: string,
|
|
280
|
+
sym: SymRenderCtx,
|
|
281
|
+
): { fields: DeclaredField[]; const: boolean | undefined } | null {
|
|
282
|
+
const pointee = sym.info(name)?.pointee;
|
|
283
|
+
const fields = pointeeFields(pointee);
|
|
284
|
+
if (fields === null || pointee!.volatile) {
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
return { fields, const: pointee!.const };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** `gPtr->member` for an access through a pointer global's value, or null when the offset is not
|
|
291
|
+
* provably ONE member's (see the block comment above). A VARIABLE index declines whatever it
|
|
292
|
+
* lands on — the indexed form is not byte-neutral and has no spelling here. */
|
|
293
|
+
function pointeeAccess(
|
|
294
|
+
pg: PtrGlobalBase,
|
|
295
|
+
off: number,
|
|
296
|
+
width: number,
|
|
297
|
+
signed: boolean,
|
|
298
|
+
isStore: boolean,
|
|
299
|
+
sym: SymRenderCtx,
|
|
300
|
+
): Expr | null {
|
|
301
|
+
if (pg.idx !== null) {
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
const total = pg.byte + off;
|
|
305
|
+
// Constant offset: the member must match EXACTLY — offset, read width, and the SPELLED type
|
|
306
|
+
// (spellsAccessType). An ARRAY member is excluded whatever its size: `u8 x[1]` would match a
|
|
307
|
+
// byte access by (offset, size) and spell `->x`, which is not an lvalue of that width at all.
|
|
308
|
+
const p = spellablePointee(pg.name, sym);
|
|
309
|
+
const f = p?.fields.find((m) => m.offset === total && m.size === width && !isArrayField(m));
|
|
310
|
+
return p && f && spellsAccessType(f.signed, width, signed) && memberQualsAllow(f, p.const, isStore)
|
|
311
|
+
? { k: 'field', base: { k: 'var', name: pg.name }, name: f.name }
|
|
312
|
+
: null;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// The (name, byte offset) of a global access with a CONSTANT total offset — `&gSym` → off,
|
|
316
|
+
// `&gSym + K` → K + off. The exact byte is what a struct-layout field lookup needs; a variable
|
|
317
|
+
// residual returns null (no field spelling — falls through to the index/cast forms).
|
|
318
|
+
function globalConstByte(baseExpr: Expr, off: number): { name: string; byte: number } | null {
|
|
319
|
+
const top = addrIn(baseExpr);
|
|
320
|
+
if (top) {
|
|
321
|
+
return { name: top.name, byte: off };
|
|
322
|
+
}
|
|
323
|
+
if (baseExpr.k === 'bin' && baseExpr.op === '+') {
|
|
324
|
+
for (const [a, b] of [
|
|
325
|
+
[baseExpr.l, baseExpr.r],
|
|
326
|
+
[baseExpr.r, baseExpr.l],
|
|
327
|
+
] as const) {
|
|
328
|
+
const a2 = addrIn(a);
|
|
329
|
+
if (a2 && b.k === 'const') {
|
|
330
|
+
return { name: a2.name, byte: b.value + off };
|
|
100
331
|
}
|
|
101
332
|
}
|
|
102
333
|
}
|
|
@@ -111,6 +342,8 @@ function memAccess(
|
|
|
111
342
|
signed: boolean,
|
|
112
343
|
ctype: (e: Expr) => IrType | undefined,
|
|
113
344
|
scalarGlobals: Set<string>,
|
|
345
|
+
sym?: SymRenderCtx,
|
|
346
|
+
isStore = false,
|
|
114
347
|
): Expr {
|
|
115
348
|
// A deref of a global's address collapses to the bare global: `*(&gSym)` at off 0 is `gSym`;
|
|
116
349
|
// at off N the global is an array — `gSym[N/width]` (a C global name decays to a pointer, so
|
|
@@ -118,6 +351,35 @@ function memAccess(
|
|
|
118
351
|
// `*(&gSym + i)` → `gSym[i + off/width]` (byte offset `i` peeled from the tree; for a u8 global
|
|
119
352
|
// the residual IS the index). This is what makes an agbcc `.word gSym` pool access a named
|
|
120
353
|
// global read/element rather than a phantom-pointer deref.
|
|
354
|
+
// Declaration-shape spellings (symbol map): a STRUCT global's constant-offset access is the
|
|
355
|
+
// named field (`gSym.field` — the source spelling a folded literal can never match); an ARRAY
|
|
356
|
+
// global indexes its BARE name (`gSym[i]`, see below). Exact field match only (offset AND
|
|
357
|
+
// width) — anything else falls through to the honest cast forms, never a guessed field.
|
|
358
|
+
if (sym) {
|
|
359
|
+
const gb = globalConstByte(baseExpr, off);
|
|
360
|
+
const si = gb ? sym.info(gb.name) : undefined;
|
|
361
|
+
if (gb && si?.shape === 'struct') {
|
|
362
|
+
// THE shared spellability predicate (symbols.ts), the same call declare.ts gates its struct
|
|
363
|
+
// declaration on: a layout it declines whole is a layout with no nameable members, and a
|
|
364
|
+
// union alias it drops for the first view at that offset is a name no declaration carries.
|
|
365
|
+
// An ARRAY member is excluded for the same reason as in pointeeAccess: `u8 x[1]` would match
|
|
366
|
+
// a byte access by (offset, size) and spell `.x`, which is not an lvalue of that width.
|
|
367
|
+
const fld = declaredFields(si.layout)?.find((f) => f.offset === gb.byte && f.size === width && !isArrayField(f));
|
|
368
|
+
if (fld && memberQualsAllow(fld, si.const, isStore)) {
|
|
369
|
+
return { k: 'field', base: { k: 'var', name: gb.name }, name: fld.name, dot: true };
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
// …and the same idea one indirection down: an access at a CONSTANT offset through a POINTER
|
|
373
|
+
// global's VALUE is a named member of what it points at (`gPtr->member`) when the map knows
|
|
374
|
+
// the pointee's layout — see pointeeAccess for the guards.
|
|
375
|
+
const pg = ptrGlobalBase(baseExpr, (n) => sym.info(n)?.shape === 'pointer');
|
|
376
|
+
if (pg) {
|
|
377
|
+
const spelled = pointeeAccess(pg, off, width, signed, isStore, sym);
|
|
378
|
+
if (spelled) {
|
|
379
|
+
return spelled;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
121
383
|
const g = globalOf(baseExpr, width);
|
|
122
384
|
if (g) {
|
|
123
385
|
const idxVal = g.idx;
|
|
@@ -136,6 +398,14 @@ function memAccess(
|
|
|
136
398
|
: idxVal.k === 'const'
|
|
137
399
|
? { k: 'const', value: idxVal.value + off / width }
|
|
138
400
|
: { k: 'bin', op: '+', l: idxVal, r: { k: 'const', value: off / width } };
|
|
401
|
+
// ARRAY-declared global (symbol map): index the bare name — `gSym[i]`, the spelling the
|
|
402
|
+
// dogfood proved agbcc needs for ROM tables — with the element type registered in the env
|
|
403
|
+
// so the stride check passes and no cast is added. Element-width match only.
|
|
404
|
+
const siArr = sym?.info(g.name);
|
|
405
|
+
if (siArr?.shape === 'array' && siArr.elemSize === width) {
|
|
406
|
+
sym!.noteGlobal(g.name, T.ptr(T.int(width * 8, siArr.elemSigned ?? false)));
|
|
407
|
+
return { k: 'index', base: { k: 'var', name: g.name }, idx, width, signed };
|
|
408
|
+
}
|
|
139
409
|
return { k: 'index', base: { k: 'addr', name: g.name }, idx, width, signed };
|
|
140
410
|
}
|
|
141
411
|
const bt = base.type;
|
|
@@ -169,11 +439,18 @@ function arrayAccess(
|
|
|
169
439
|
elemSize: number,
|
|
170
440
|
signed: boolean,
|
|
171
441
|
ctype: (e: Expr) => IrType | undefined,
|
|
442
|
+
sym?: SymRenderCtx,
|
|
172
443
|
): Expr {
|
|
173
444
|
// A variable-index access off a global's address indexes the ADDRESS `&gSym` (the cast form
|
|
174
445
|
// `((T *)&gSym)[i]` — valid for a struct global too, unlike casting the bare value). A
|
|
175
446
|
// struct-array-of-globals (fieldOff) through `&gSym` is out of scope — fall through.
|
|
176
447
|
if (baseExpr.k === 'addr' && fieldOff === undefined) {
|
|
448
|
+
// ARRAY-declared global (symbol map): the bare-name spelling, same rule as memAccess.
|
|
449
|
+
const si = sym?.info(baseExpr.name);
|
|
450
|
+
if (si?.shape === 'array' && si.elemSize === elemSize) {
|
|
451
|
+
sym!.noteGlobal(baseExpr.name, T.ptr(T.int(elemSize * 8, si.elemSigned ?? false)));
|
|
452
|
+
return { k: 'index', base: { k: 'var', name: baseExpr.name }, idx: idxExpr, width: elemSize, signed };
|
|
453
|
+
}
|
|
177
454
|
return { k: 'index', base: baseExpr, idx: idxExpr, width: elemSize, signed };
|
|
178
455
|
}
|
|
179
456
|
const bt = base.type;
|
|
@@ -306,6 +583,11 @@ export interface StructureOptions {
|
|
|
306
583
|
// "annotate" — a `marker` node that spells as the undefined ASMLIFT_ERROR(...) symbol (loud in
|
|
307
584
|
// the ARTIFACT: the function emits complete, but cannot compile un-acknowledged).
|
|
308
585
|
onGap?: 'strict' | 'annotate';
|
|
586
|
+
/** NAME-keyed project symbol facts (symbols.ts `symbolsByName`) — drives the byte-sensitive
|
|
587
|
+
* declaration-shape spellings: `shape:'array'` forces the aggregate classification and the
|
|
588
|
+
* bare `gSym[i]` form; `shape:'struct'`+layout spells interiors as `gSym.field`. Absent (or
|
|
589
|
+
* a symbol not in the map) ⇒ today's usage-inferred behavior, byte-identical. */
|
|
590
|
+
symbols?: Map<string, SymbolInfo>;
|
|
309
591
|
}
|
|
310
592
|
|
|
311
593
|
export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
@@ -316,6 +598,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
316
598
|
orderArgCopiesByComputation = true,
|
|
317
599
|
switchAllowsNeqCase = true,
|
|
318
600
|
onGap = 'strict',
|
|
601
|
+
symbols,
|
|
319
602
|
} = opts;
|
|
320
603
|
const defs = defOpMap(fn);
|
|
321
604
|
const preds = predecessorBlocks(fn);
|
|
@@ -347,14 +630,16 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
347
630
|
if (s) {
|
|
348
631
|
(offsets.get(s) ?? offsets.set(s, new Set()).get(s)!).add(op.attrs.off as number);
|
|
349
632
|
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
633
|
+
} else if (op.opcode === 'add' || op.opcode === 'sub') {
|
|
634
|
+
// ANY arithmetic on the symbol's address is interior addressing ⇒ aggregate — even when
|
|
635
|
+
// the sum only reaches memory through a copy/phi (a pointer-walk loop `p = &g + 2;
|
|
636
|
+
// do { *p++ … }` never makes the add a DIRECT load/store base, which is all the old
|
|
637
|
+
// check saw; the symbol then classified scalar and emitted the bare `g = 0` spelling,
|
|
638
|
+
// which a project declaring `extern u16 g[]` rejects as an incomplete-type assignment).
|
|
639
|
+
for (const o of op.operands) {
|
|
640
|
+
const s2 = gaddrSym(o);
|
|
641
|
+
if (s2) {
|
|
642
|
+
bumpAgg(s2);
|
|
358
643
|
}
|
|
359
644
|
}
|
|
360
645
|
} else if (op.opcode === 'aload' || op.opcode === 'astore') {
|
|
@@ -370,8 +655,38 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
370
655
|
scalarGlobals.add(sym);
|
|
371
656
|
}
|
|
372
657
|
}
|
|
658
|
+
// Declaration-shape OVERRIDE (symbol map): a project-declared array/struct global is an
|
|
659
|
+
// AGGREGATE whatever the usage inference saw — a lone off-0 access to `extern u16 tbl[]`
|
|
660
|
+
// must still spell through the aggregate/array forms, never the bare scalar `tbl`.
|
|
661
|
+
if (symbols) {
|
|
662
|
+
for (const [n, si] of symbols) {
|
|
663
|
+
if (si.shape === 'array' || si.shape === 'struct') {
|
|
664
|
+
scalarGlobals.delete(n);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
373
668
|
}
|
|
374
669
|
|
|
670
|
+
// Symbol-map rendering context (memAccess/arrayAccess): shape lookups + the env registry for
|
|
671
|
+
// array-shaped globals actually referenced (they surface as SFn.globals — typed, undeclared).
|
|
672
|
+
const shapedGlobalTypes = new Map<string, IrType>();
|
|
673
|
+
const symCtx: SymRenderCtx | undefined = symbols
|
|
674
|
+
? { info: (n) => symbols.get(n), noteGlobal: (n, t) => shapedGlobalTypes.set(n, t) }
|
|
675
|
+
: undefined;
|
|
676
|
+
|
|
677
|
+
/** A bare `gSym` naming a map-declared POINTER global — the VALUE of a pointer cell. Load,
|
|
678
|
+
* store and compare of that 4-byte cell are identical for any object-pointer type, so the
|
|
679
|
+
* declared pointee never matters to THEM; arithmetic on the loaded value is the opposite case,
|
|
680
|
+
* where the pointee's size scales what is added and every stride must therefore be made
|
|
681
|
+
* explicit (`(u8 *)gPtr + K`). `ctype` cannot see any of this: it types only params/locals, so
|
|
682
|
+
* a pointer global renders `undefined` there. */
|
|
683
|
+
const isPtrGlobal = (x: Expr): boolean => x.k === 'var' && symCtx?.info(x.name)?.shape === 'pointer';
|
|
684
|
+
|
|
685
|
+
/** Operands `-`/`~` cannot take as spelled: a rendered pointer, a bare `&gSym`, a pointer
|
|
686
|
+
* global's value. All three are ill-formed C under a unary arithmetic operator — the asm did
|
|
687
|
+
* 32-bit integer math on the address, so that is what gets spelled. */
|
|
688
|
+
const needsIntSpelling = (x: Expr): boolean => ctype(x)?.kind === 'ptr' || x.k === 'addr' || isPtrGlobal(x);
|
|
689
|
+
|
|
375
690
|
// --- loop discovery (loops.ts): natural loops via dominator back-edges + the nesting forest ---
|
|
376
691
|
const forest = analyzeLoops(fn, dom);
|
|
377
692
|
|
|
@@ -786,11 +1101,20 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
786
1101
|
}
|
|
787
1102
|
}
|
|
788
1103
|
|
|
789
|
-
// An unresolvable value: strict mode keeps the `"?"` sentinel
|
|
790
|
-
//
|
|
791
|
-
//
|
|
792
|
-
|
|
793
|
-
|
|
1104
|
+
// An unresolvable value: strict mode keeps the `"?"` sentinel AND records the reason — the
|
|
1105
|
+
// decline thrown below names the actual gaps ("unmodelled instruction 'adde'"), the same
|
|
1106
|
+
// reasons annotate mode's markers carry, instead of the anonymous `?` that assertResolved
|
|
1107
|
+
// would report at the boundary (assertResolved stays as the backstop for any other producer).
|
|
1108
|
+
// Annotate mode emits a marker (the undefined ASMLIFT_ERROR symbol — loud in the ARTIFACT,
|
|
1109
|
+
// function still complete).
|
|
1110
|
+
const strictGaps: string[] = [];
|
|
1111
|
+
const mkGap = (reason: string, args: Expr[]): Expr => {
|
|
1112
|
+
if (onGap === 'annotate') {
|
|
1113
|
+
return { k: 'marker', reason, args };
|
|
1114
|
+
}
|
|
1115
|
+
strictGaps.push(reason);
|
|
1116
|
+
return { k: 'var', name: '?' };
|
|
1117
|
+
};
|
|
794
1118
|
|
|
795
1119
|
// Lower ONE def's operation to an Expr, rendering operands through `e`. Shared between the
|
|
796
1120
|
// inline-at-use path (exprWith) and the materialized-temp path (sideEffects), so both spell a
|
|
@@ -800,7 +1124,30 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
800
1124
|
return { k: 'const', value: d.attrs.value as number };
|
|
801
1125
|
}
|
|
802
1126
|
if (CMP_TO_BIN[d.opcode]) {
|
|
803
|
-
|
|
1127
|
+
// A bare global address `&gSym` as a COMPARISON operand is the same unspelled escape as the
|
|
1128
|
+
// arithmetic case below (see intifyAddr): its C type comes from the PROJECT's own
|
|
1129
|
+
// declaration, unknowable here. Worse, the compare's SIGNEDNESS lives in the operand types
|
|
1130
|
+
// (CMP_TO_BIN maps icmp_ult and icmp_slt to the same '<'), so leaving `&gSym` untyped lets
|
|
1131
|
+
// the project's declaration pick the compare the compiler emits — silently byte-inexact
|
|
1132
|
+
// whenever it disagrees with the asm. The honest spelling is integer math on the address
|
|
1133
|
+
// with the cast AGREEING with the opcode's signedness: unsigned compares (and the
|
|
1134
|
+
// sign-agnostic ==/!=) spell `(u32)&gSym`, signed compares `(s32)&gSym` — exactly the
|
|
1135
|
+
// compare the asm did. The deref folds never see a compare operand, so no named spelling is
|
|
1136
|
+
// lost; a NARROWING cast (`(u8)&gSym`) is not a bare `addr` and keeps its truncation.
|
|
1137
|
+
// SCOPE (adversarial review): this closes the hole for BARE addr operands only. An
|
|
1138
|
+
// addr-carrying arithmetic tree (`(u32)&gSym + 4`, spelled by intifyAddr below) under an
|
|
1139
|
+
// icmp_s* still compares unsigned in C (u32 wins the usual-arithmetic-conversions) — the
|
|
1140
|
+
// same pre-existing wrongness the old ptr-vs-int spelling had, surfacing as a scoring
|
|
1141
|
+
// nonmatch, never a silent regression of a formerly-correct compare. Rare shape; an outer
|
|
1142
|
+
// signed cast on addr-carrying trees is the follow-up if it ever costs a row.
|
|
1143
|
+
const t = /^icmp_s/.test(d.opcode) ? T.s(32) : T.u(32);
|
|
1144
|
+
const intifyAddrCmp = (x: Expr): Expr => (x.k === 'addr' ? { k: 'cast', to: t, e: x } : x);
|
|
1145
|
+
return {
|
|
1146
|
+
k: 'bin',
|
|
1147
|
+
op: CMP_TO_BIN[d.opcode],
|
|
1148
|
+
l: intifyAddrCmp(e(d.operands[0])),
|
|
1149
|
+
r: intifyAddrCmp(e(d.operands[1])),
|
|
1150
|
+
};
|
|
804
1151
|
}
|
|
805
1152
|
if (ARITH_TO_BIN[d.opcode]) {
|
|
806
1153
|
let l = e(d.operands[0]);
|
|
@@ -844,6 +1191,50 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
844
1191
|
} else if (op === '-' && ctype(l)?.kind !== 'ptr' && ctype(r)?.kind === 'ptr') {
|
|
845
1192
|
r = intify(r); // int - ptr is not C
|
|
846
1193
|
}
|
|
1194
|
+
// A bare global address `&gSym` under ANY of these operators is never emitted as-is: its C
|
|
1195
|
+
// type comes from the PROJECT's own declaration (unknowable here — exprCType types `addr`
|
|
1196
|
+
// undefined, so the ptr-keyed intify above never fires on it), which makes `&gSym + K`
|
|
1197
|
+
// byte-INEXACT (C scales K by sizeof(gSym)) and `&gSym & K` ill-formed. The honest spelling
|
|
1198
|
+
// is integer math on the address — `(u32)&gSym + K`, exactly the arithmetic the asm did.
|
|
1199
|
+
// The deref folds (globalOf / globalConstByte, via addrIn) look through this cast, so every
|
|
1200
|
+
// access that CAN spell a named element/field still does; only a genuine value-context
|
|
1201
|
+
// escape (a call argument, a stored address, a compare) keeps it — previously such an
|
|
1202
|
+
// escape tripped assertDerefsTyped's interior-pointer rule and declined the whole function.
|
|
1203
|
+
const intifyAddr = (x: Expr): Expr => (x.k === 'addr' ? { k: 'cast', to: T.u(32), e: x } : x);
|
|
1204
|
+
l = intifyAddr(l);
|
|
1205
|
+
r = intifyAddr(r);
|
|
1206
|
+
// The SAME hazard one level down, for a POINTER-shaped global's VALUE (`gPtr`, isPtrGlobal):
|
|
1207
|
+
// C scales `gPtr + K` by sizeof(*gPtr) — 1 under the map's synthesized `void *`, but
|
|
1208
|
+
// whatever the PROJECT's header declares (a 0x5C-byte struct, say) in the world a user
|
|
1209
|
+
// actually recompiles in. The asm added BYTES, so the honest spelling makes the stride
|
|
1210
|
+
// explicit: CAST-THEN-ADD, `(u8 *)gPtr + K`, the same address in EVERY world. Add-then-cast
|
|
1211
|
+
// (`(u8 *)(gPtr + K)`, what the backend's deref legalization would otherwise produce) is
|
|
1212
|
+
// byte-correct in exactly one of them — a silent wrongness, the class this project refuses.
|
|
1213
|
+
// NOT foldable into the deref index either: `((u8 *)gPtr)[K + off]` re-scales K by the
|
|
1214
|
+
// ACCESS width, a different address whenever that width is not 1.
|
|
1215
|
+
// Under the non-additive operators C rejects a pointer outright, so there the honest
|
|
1216
|
+
// spelling is integer math on the cell — exactly intifyAddr's `(u32)&gSym` rule.
|
|
1217
|
+
const bytePtr = (x: Expr): Expr => ({ k: 'cast', to: T.ptr(T.u(8)), e: x });
|
|
1218
|
+
const intifyPtrGlobal = (x: Expr): Expr => ({ k: 'cast', to: T.u(32), e: x });
|
|
1219
|
+
if (op === '+' || op === '-') {
|
|
1220
|
+
// `ptr ± int` and `ptr - ptr` are byte arithmetic once both sides are byte pointers;
|
|
1221
|
+
// `ptr + ptr` and `int - ptr` are not C at all, so the second pointer goes integer.
|
|
1222
|
+
const bothPtr = isPtrGlobal(l) && isPtrGlobal(r);
|
|
1223
|
+
if (isPtrGlobal(l)) {
|
|
1224
|
+
l = bytePtr(l);
|
|
1225
|
+
}
|
|
1226
|
+
if (isPtrGlobal(r)) {
|
|
1227
|
+
r = bothPtr && op === '-' ? bytePtr(r) : op === '+' && !bothPtr ? bytePtr(r) : intifyPtrGlobal(r);
|
|
1228
|
+
}
|
|
1229
|
+
} else if (op !== '&&' && op !== '||') {
|
|
1230
|
+
// (`&&`/`||` take a pointer operand legally — a truth test, no arithmetic.)
|
|
1231
|
+
l = isPtrGlobal(l) ? intifyPtrGlobal(l) : l;
|
|
1232
|
+
r = isPtrGlobal(r) ? intifyPtrGlobal(r) : r;
|
|
1233
|
+
}
|
|
1234
|
+
// SCOPE: this and intifyAddr cover the ARITHMETIC escapes. A pointer global under a
|
|
1235
|
+
// COMPARISON (`gPtr < K` — C compares unsigned whatever the asm's icmp_s* said) is the same
|
|
1236
|
+
// class as intifyAddrCmp's `addr` rule and is deliberately left alone here: it is valid C
|
|
1237
|
+
// today, so closing it would churn spellings for a signedness case no row exercises.
|
|
847
1238
|
return { k: 'bin', op, l, r };
|
|
848
1239
|
}
|
|
849
1240
|
// `-`/`~` on a pointer rendering is equally not C — same honest integer cast as above.
|
|
@@ -879,11 +1270,11 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
879
1270
|
}
|
|
880
1271
|
if (d.opcode === 'neg') {
|
|
881
1272
|
const x = e(d.operands[0]);
|
|
882
|
-
return { k: 'un', op: '-', e:
|
|
1273
|
+
return { k: 'un', op: '-', e: needsIntSpelling(x) ? { k: 'cast', to: T.s(32), e: x } : x };
|
|
883
1274
|
}
|
|
884
1275
|
if (d.opcode === 'not') {
|
|
885
1276
|
const x = e(d.operands[0]);
|
|
886
|
-
return { k: 'un', op: '~', e:
|
|
1277
|
+
return { k: 'un', op: '~', e: needsIntSpelling(x) ? { k: 'cast', to: T.s(32), e: x } : x };
|
|
887
1278
|
}
|
|
888
1279
|
// Width-narrowing casts: `zext`/`sext` widen a `width`-bit value back to 32 → C `(u8)e`/`(s8)e`.
|
|
889
1280
|
if (d.opcode === 'zext') {
|
|
@@ -896,6 +1287,12 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
896
1287
|
return { k: 'call', fn: d.attrs.target as string, args: d.operands.map(e) };
|
|
897
1288
|
}
|
|
898
1289
|
if (d.opcode === 'gaddr') {
|
|
1290
|
+
// A promoted CODE symbol (frontend `code: true`) is a function pointer stored as an
|
|
1291
|
+
// integer: spelled `(u32)Name` — the source idiom — never `&Name` (defect G of the
|
|
1292
|
+
// dogfood report; the & form compiles but is a different, non-matching spelling).
|
|
1293
|
+
if (d.attrs.code === true) {
|
|
1294
|
+
return { k: 'cast', to: T.int(32, false), e: { k: 'var', name: d.attrs.sym as string } };
|
|
1295
|
+
}
|
|
899
1296
|
return { k: 'addr', name: d.attrs.sym as string };
|
|
900
1297
|
}
|
|
901
1298
|
if (d.opcode === 'load') {
|
|
@@ -907,6 +1304,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
907
1304
|
(d.attrs.signed as boolean) ?? false,
|
|
908
1305
|
ctype,
|
|
909
1306
|
scalarGlobals,
|
|
1307
|
+
symCtx,
|
|
910
1308
|
);
|
|
911
1309
|
}
|
|
912
1310
|
// aload carries a runtime index operand (variable-index array access) — `base[index]`, or
|
|
@@ -920,6 +1318,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
920
1318
|
d.attrs.elemSize as number,
|
|
921
1319
|
(d.attrs.signed as boolean) ?? false,
|
|
922
1320
|
ctype,
|
|
1321
|
+
symCtx,
|
|
923
1322
|
);
|
|
924
1323
|
}
|
|
925
1324
|
return d.opcode === 'opaque'
|
|
@@ -1035,6 +1434,8 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1035
1434
|
width === 4,
|
|
1036
1435
|
ctype,
|
|
1037
1436
|
scalarGlobals,
|
|
1437
|
+
symCtx,
|
|
1438
|
+
true, // an lvalue: a member whose declaration is const cannot be NAMED as the target
|
|
1038
1439
|
);
|
|
1039
1440
|
if (lval0.k === 'var') {
|
|
1040
1441
|
globalNames.add(lval0.name);
|
|
@@ -1056,6 +1457,7 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1056
1457
|
elemSize,
|
|
1057
1458
|
elemSize === 4,
|
|
1058
1459
|
ctype,
|
|
1460
|
+
symCtx,
|
|
1059
1461
|
),
|
|
1060
1462
|
value: expr(op.operands[2]),
|
|
1061
1463
|
});
|
|
@@ -1471,6 +1873,14 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1471
1873
|
};
|
|
1472
1874
|
|
|
1473
1875
|
const body = recognizeForLoops(structureRegion(entry, null));
|
|
1876
|
+
// Strict-mode gaps decline HERE, naming the reasons — the same text annotate's markers
|
|
1877
|
+
// carry, so the two mode surfaces report the same decline (the reproduction scripts run
|
|
1878
|
+
// strict; the benchmark rows store annotate markers — fidelity holds them against each
|
|
1879
|
+
// other). Without this, the `?` sentinels reach assertResolved and decline anonymously.
|
|
1880
|
+
if (strictGaps.length > 0) {
|
|
1881
|
+
const reasons = [...new Set(strictGaps)].join('; ');
|
|
1882
|
+
throw new StructureError(`${strictGaps.length} unresolvable value(s) in '${fn.name}' — ${reasons}`);
|
|
1883
|
+
}
|
|
1474
1884
|
// v* = coalesced/materialized locals; t* = sequentialize's swap-cycle temps (varType-only —
|
|
1475
1885
|
// they have no Value, so they are collected from varType, not varName).
|
|
1476
1886
|
const localNames = [...new Set([...varName.values(), ...[...varType.keys()].filter((n) => /^t\d+$/.test(n))])].filter(
|
|
@@ -1481,6 +1891,13 @@ export function structure(fn: Fn, opts: StructureOptions = {}): SFn {
|
|
|
1481
1891
|
name: fn.name,
|
|
1482
1892
|
params: entry.params.map((p, i) => ({ name: `a${i}`, type: p.type })),
|
|
1483
1893
|
locals: localNames.map((n) => ({ name: n, type: varType.get(n)! })),
|
|
1894
|
+
...(shapedGlobalTypes.size
|
|
1895
|
+
? {
|
|
1896
|
+
globals: [...shapedGlobalTypes]
|
|
1897
|
+
.map(([name, type]) => ({ name, type }))
|
|
1898
|
+
.sort((a, b) => a.name.localeCompare(b.name)),
|
|
1899
|
+
}
|
|
1900
|
+
: {}),
|
|
1484
1901
|
retType: returnsVoid ? T.void() : returnType(fn),
|
|
1485
1902
|
body,
|
|
1486
1903
|
...(structs.length ? { structs } : {}),
|