@asmlift/core 0.2.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/package.json +1 -1
- package/src/backend/cfamily.ts +30 -4
- package/src/contracts.ts +22 -3
- package/src/declare.ts +225 -0
- package/src/frontend/frontend.ts +12 -2
- 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/rank.ts +215 -76
- package/src/structure/structure.ts +462 -45
- package/src/symbols.ts +426 -0
- package/src/trace.ts +8 -2
package/package.json
CHANGED
package/src/backend/cfamily.ts
CHANGED
|
@@ -47,14 +47,38 @@ export function cType(t: IrType): string {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
/** Declare a name of a given type, C declarator rules: an array puts its length AFTER the name
|
|
50
|
-
* (`u8 _pad[4]`),
|
|
50
|
+
* (`u8 _pad[4]`), a pointer binds its `*` to the declarator (`void *p`), everything else is
|
|
51
|
+
* the prefix `cType name`. */
|
|
51
52
|
function cDeclare(t: IrType, name: string): string {
|
|
52
53
|
if (t.kind === 'array') {
|
|
53
54
|
return `${cType(t.elem)} ${name}[${t.count}]`;
|
|
54
55
|
}
|
|
56
|
+
if (t.kind === 'ptr') {
|
|
57
|
+
return `${cType(t.to)} *${name}`;
|
|
58
|
+
}
|
|
55
59
|
return `${cType(t)} ${name}`;
|
|
56
60
|
}
|
|
57
61
|
|
|
62
|
+
/** One field of a rendered struct declaration — the minimal input shape shared by the two
|
|
63
|
+
* producers of `struct N { ... };` text: the backend's recovered structs (SFn.structs, whose
|
|
64
|
+
* StructType fields already carry pads as real `u8[N]` members) and the cli's map-layout
|
|
65
|
+
* declaration synthesis (declare.ts, which seats fields at exact offsets by interleaving pad
|
|
66
|
+
* fields itself). `volatile` is the MMIO member idiom (`volatile u16 gain;`) — only the
|
|
67
|
+
* map-derived synthesis sets it today; recovered structs never do. */
|
|
68
|
+
export interface StructFieldDecl {
|
|
69
|
+
name: string;
|
|
70
|
+
type: IrType;
|
|
71
|
+
volatile?: boolean;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** THE struct-declaration spelling — every `struct N { ... };` asmlift prints comes from here,
|
|
75
|
+
* so the backend's recovered-struct decls and the scoring layer's synthesized decls cannot
|
|
76
|
+
* drift apart. One line, fields in caller order (the type is self-describing: padding is the
|
|
77
|
+
* caller's discipline, already present as real fields). */
|
|
78
|
+
export function renderStructDecl(name: string, fields: StructFieldDecl[]): string {
|
|
79
|
+
return `struct ${name} { ${fields.map((f) => `${f.volatile ? 'volatile ' : ''}${cDeclare(f.type, f.name)};`).join(' ')} };`;
|
|
80
|
+
}
|
|
81
|
+
|
|
58
82
|
// A LEAF hook lets a C-family backend override how a `var` or `index` node spells WITHOUT
|
|
59
83
|
// re-implementing precedence, parenthesization, or statement structure. It returns the
|
|
60
84
|
// replacement text, or null to fall through to the default C spelling — how the C++ backend
|
|
@@ -121,6 +145,10 @@ function printExpr(e: Expr, parentPrec: number, vt: VarTypes, leaf?: LeafHook):
|
|
|
121
145
|
const baseTxt = hooked ?? `${rec(ix.base, 1)}[${rec(ix.idx, 99)}]`;
|
|
122
146
|
return `${baseTxt}.${e.name}`;
|
|
123
147
|
}
|
|
148
|
+
// explicit dot: a struct-VALUE global's field (`gSym.field`, symbol-map layout spelling)
|
|
149
|
+
if (e.dot) {
|
|
150
|
+
return `${rec(e.base, 1)}.${e.name}`;
|
|
151
|
+
}
|
|
124
152
|
return `${rec(e.base, 1)}->${e.name}`;
|
|
125
153
|
}
|
|
126
154
|
case 'un': {
|
|
@@ -333,9 +361,7 @@ function cFamilyBody(fn0: SFn, leaf?: LeafHook): string[] {
|
|
|
333
361
|
* raise/structs.ts (unaccessed leading/interior gaps) interleave them where natural C alignment
|
|
334
362
|
* does not already cover the offset. This just declares each field in order. */
|
|
335
363
|
function structDecls(fn: SFn): string[] {
|
|
336
|
-
return (fn.structs ?? []).map(
|
|
337
|
-
(s) => `struct ${s.name} { ${s.fields.map((f) => cDeclare(f.type, f.name) + ';').join(' ')} };`,
|
|
338
|
-
);
|
|
364
|
+
return (fn.structs ?? []).map((s) => renderStructDecl(s.name, s.fields));
|
|
339
365
|
}
|
|
340
366
|
|
|
341
367
|
/** Assemble a full C-family function from a caller-supplied signature line and the shared body. */
|
package/src/contracts.ts
CHANGED
|
@@ -73,6 +73,8 @@ export function assertDerefsTyped(sfn: SFn): void {
|
|
|
73
73
|
const bad: string[] = [];
|
|
74
74
|
// Ops C rejects outright on a pointer operand (the additive ops and &&/|| are legal C).
|
|
75
75
|
const NO_PTR_OPS = new Set(['&', '|', '^', '<<', '>>', '*', '/', '%']);
|
|
76
|
+
// The comparison operators — where a bare `&SYM` operand is SIGN-ambiguous, not ill-formed.
|
|
77
|
+
const CMP_OPS = new Set(['<', '<=', '>', '>=', '==', '!=']);
|
|
76
78
|
// 1/2/4 only: the decomp typedef vocabulary (C_TYPEDEFS) has no 64-bit scalar, so a width-8
|
|
77
79
|
// access would print as the nonexistent `(s64 *)` — exactly the three-stages-later failure
|
|
78
80
|
// this rule pre-empts. (If f64 loads ever land they are floats, not a scalar width here.)
|
|
@@ -99,15 +101,32 @@ export function assertDerefsTyped(sfn: SFn): void {
|
|
|
99
101
|
}
|
|
100
102
|
// A bare global ADDRESS `&SYM` under `+`/`-` is an ESCAPING interior pointer: C scales the byte
|
|
101
103
|
// offset by sizeof(SYM), which is unknown for a header-typed global, so `&SYM + N` is byte-
|
|
102
|
-
// inexact.
|
|
103
|
-
// `index`/`field` node whose base is a bare `addr
|
|
104
|
-
//
|
|
104
|
+
// inexact. Nothing emits this shape anymore: a load/store base folds byte-correctly (globalOf
|
|
105
|
+
// turns `&SYM + N` into an `index`/`field` node whose base is a bare `addr`), and the additive
|
|
106
|
+
// lowering intifies every other `addr` operand to `(u32)&SYM` (structure.ts intifyAddr — the
|
|
107
|
+
// cast types int, so it never lands here). A bare `addr` reaching a `+`/`-` operand is therefore
|
|
108
|
+
// a lowering REGRESSION — flag it rather than emit wrong bytes.
|
|
105
109
|
if (e.k === 'bin' && (e.op === '+' || e.op === '-')) {
|
|
106
110
|
const addrSide = e.l.k === 'addr' ? e.l : e.r.k === 'addr' ? e.r : undefined;
|
|
107
111
|
if (addrSide) {
|
|
108
112
|
bad.push(`interior pointer arithmetic on the global address '&${addrSide.name}'`);
|
|
109
113
|
}
|
|
110
114
|
}
|
|
115
|
+
// A bare global address `&SYM` as a COMPARISON operand is the same unspelled escape under a
|
|
116
|
+
// different operator — and worse than ill-formed: the compare's SIGNEDNESS is spelled by the
|
|
117
|
+
// operand TYPES (the structurer maps icmp_ult and icmp_slt to the same '<'), and `&SYM`'s C
|
|
118
|
+
// type is the project's own declaration, unknowable here — so the emitted compare can flip
|
|
119
|
+
// signedness against the asm's, silently. The cmp lowering intifies it signedness-aware
|
|
120
|
+
// (`(u32)`/`(s32)&SYM` — structure.ts intifyAddrCmp; the cast types int, so it never lands
|
|
121
|
+
// here). A bare `addr` reaching a comparison operand is therefore a lowering REGRESSION —
|
|
122
|
+
// flag it rather than emit sign-ambiguous C.
|
|
123
|
+
if (e.k === 'bin' && CMP_OPS.has(e.op)) {
|
|
124
|
+
for (const side of [e.l, e.r]) {
|
|
125
|
+
if (side.k === 'addr') {
|
|
126
|
+
bad.push(`bare global address '&${side.name}' as a comparison operand`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
111
130
|
// `!p` is legal C (pointer truthiness); `-p`/`~p` are not.
|
|
112
131
|
if (e.k === 'un' && e.op !== '!' && ctype(e.e)?.kind === 'ptr') {
|
|
113
132
|
bad.push(`pointer operand under unary '${e.op}'`);
|
package/src/declare.ts
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
// asmlift — declaration SYNTHESIS for self-declaring candidates
|
|
2
|
+
// (research/self-declaring-candidates-2026-07-26.md).
|
|
3
|
+
//
|
|
4
|
+
// A scored candidate that names map-derived symbols must compile WITHOUT the project's headers:
|
|
5
|
+
// this module renders the declaration block for exactly the symbols the candidate's tree
|
|
6
|
+
// references in a value context (Candidate.symbolRefs — derived from the candidate's final
|
|
7
|
+
// tree at enumeration, l3/symbol-refs.ts). It is a SCORING-LAYER
|
|
8
|
+
// concern only — backends never print declarations (a project user compiles asmlift output
|
|
9
|
+
// against their own headers, where a second declaration would collide).
|
|
10
|
+
//
|
|
11
|
+
// LIVES IN CORE (browser-pure, no Node imports) because BOTH scorers prepend it: the cli's
|
|
12
|
+
// Node/objdiff path (which re-exports this module unchanged) and the webapp's wasm scorer
|
|
13
|
+
// (score-wasm.ts) — one renderer, so the two scoring worlds cannot drift.
|
|
14
|
+
//
|
|
15
|
+
// Fidelity rules (each empirically verified against agbcc — see the research doc):
|
|
16
|
+
// • struct decls are rebuilt from the map layout with explicit `u8 pad[]` gap fields — the
|
|
17
|
+
// padded synthesis is byte-identical to the real header declaration;
|
|
18
|
+
// • member/scalar SIGNEDNESS drives u8/s8/u16/s16/u32/s32 — an s8 field read is
|
|
19
|
+
// ldrb+lsl+asr where u8 is ldrb alone, so a guessed signedness is a wrong-bytes decl;
|
|
20
|
+
// • `volatile` is load-bearing (a non-volatile MMIO decl lets the compiler fold/reorder
|
|
21
|
+
// accesses), `const` is the ROM-table spelling;
|
|
22
|
+
// • code symbols get `void Name(void);` ONLY when value-referenced — call targets are never
|
|
23
|
+
// in `symbolRefs` (core excludes them: prototyping a called symbol is C89 poison);
|
|
24
|
+
// • nothing guesses, with TWO documented exceptions: a SHAPED symbol without the facts to
|
|
25
|
+
// declare faithfully is SKIPPED — the candidate then fails to compile LOUDLY and is
|
|
26
|
+
// dropped by rankBy. Exception one is the 4-byte signless-non-pointer cell (see
|
|
27
|
+
// ENUM_IS_SIGNED): spelled s32 on the C89 enum=int rule. For a true enum that is the header
|
|
28
|
+
// truth; the residual mis-spell class (a 4-byte nested-struct member word-read via a dot
|
|
29
|
+
// field) can only LOSE score — the target bytes derive from the truth decls, so a
|
|
30
|
+
// divergent compile can never false-match. Exception two is the NAME-ONLY data symbol
|
|
31
|
+
// (`extern u32 name;` — see the default case): required to reproduce symtab-only map
|
|
32
|
+
// rows outside project headers, justified by the same only-loses-score argument.
|
|
33
|
+
import { type StructFieldDecl, renderStructDecl } from './backend/cfamily';
|
|
34
|
+
import { T } from './ir/types';
|
|
35
|
+
import type { SymbolRef } from './l3/symbol-refs';
|
|
36
|
+
import {
|
|
37
|
+
ENUM_IS_SIGNED,
|
|
38
|
+
type SymbolInfo,
|
|
39
|
+
type SymbolStructField,
|
|
40
|
+
declaredFields,
|
|
41
|
+
pointeeFields,
|
|
42
|
+
symbolFieldType,
|
|
43
|
+
} from './symbols';
|
|
44
|
+
|
|
45
|
+
/** The u8/s8/u16/s16/u32/s32 spelling for a 1/2/4-byte cell, or null (no faithful narrow type). */
|
|
46
|
+
function intType(size: number, signed: boolean): string | null {
|
|
47
|
+
const base = size === 1 ? '8' : size === 2 ? '16' : size === 4 ? '32' : null;
|
|
48
|
+
return base === null ? null : `${signed ? 's' : 'u'}${base}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** `volatile const ` qualifier prefix (either may be absent). */
|
|
52
|
+
function quals(info: SymbolInfo): string {
|
|
53
|
+
return `${info.volatile ? 'volatile ' : ''}${info.const ? 'const ' : ''}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** One struct field's type, seated at its exact offset by the caller's pad discipline — THE shared
|
|
57
|
+
* map-field typing (symbols.ts `symbolFieldType`, which core's own legalization env also reads,
|
|
58
|
+
* so the declaration and the type the emitter reasoned against cannot drift). Member volatility
|
|
59
|
+
* is kept on the field decl here (`vu16 field;` — dropping it lets the compiler fold repeated
|
|
60
|
+
* reads), being a decl-only fact. */
|
|
61
|
+
const fieldType = symbolFieldType;
|
|
62
|
+
|
|
63
|
+
/** The padded `struct Tag { ... };` declaration for a layout: fields seated at exact offsets,
|
|
64
|
+
* gaps as explicit u8 pad arrays, rendered by THE shared struct renderer (core
|
|
65
|
+
* backend/cfamily.ts renderStructDecl — the same spelling the backend's recovered-struct
|
|
66
|
+
* decls use, so the two cannot drift). Returns null when the layout cannot be reproduced
|
|
67
|
+
* faithfully (an unsized member). */
|
|
68
|
+
function structDecl(tag: string, layout: SymbolStructField[] | undefined, size: number | undefined): string | null {
|
|
69
|
+
// THE shared spellability predicate (symbols.ts): which members exist, and whether the layout
|
|
70
|
+
// can be reproduced at all. Core's access rules gate on the SAME call, so a member this
|
|
71
|
+
// declaration omits — an unsizable layout declined whole, a union alias dropped for its first
|
|
72
|
+
// view — is a member no emitted expression can name.
|
|
73
|
+
const members = declaredFields(layout);
|
|
74
|
+
if (members === null) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
const fields: StructFieldDecl[] = [];
|
|
78
|
+
let cursor = 0;
|
|
79
|
+
let pad = 0;
|
|
80
|
+
for (const m of members) {
|
|
81
|
+
if (m.offset > cursor) {
|
|
82
|
+
// asmlift_-prefixed so a REAL member named pad_N (a decomp-header idiom) never collides
|
|
83
|
+
fields.push({ name: `asmlift_pad_${pad++}`, type: T.array(T.u(8), m.offset - cursor) });
|
|
84
|
+
}
|
|
85
|
+
fields.push({
|
|
86
|
+
name: m.name,
|
|
87
|
+
type: fieldType(m),
|
|
88
|
+
...(m.volatile ? { volatile: true } : {}),
|
|
89
|
+
});
|
|
90
|
+
cursor = m.offset + m.size;
|
|
91
|
+
}
|
|
92
|
+
if (size !== undefined && size > cursor) {
|
|
93
|
+
// tail padding to the declared size
|
|
94
|
+
fields.push({ name: `asmlift_pad_${pad}`, type: T.array(T.u(8), size - cursor) });
|
|
95
|
+
}
|
|
96
|
+
return renderStructDecl(tag, fields);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Render the declaration block for a candidate's recorded symbol references. Deterministic
|
|
101
|
+
* (refs arrive name-sorted from core; struct decls dedupe by tag). The block is prepended by
|
|
102
|
+
* the candidate compiler AFTER the typedef prelude — it spells types as u8/s16/… — and only in
|
|
103
|
+
* the self-declared world (the probe in compile-command.ts arbitrates; in the headers world
|
|
104
|
+
* both prelude and declarations are dropped, headers own everything).
|
|
105
|
+
*/
|
|
106
|
+
export function renderDeclarations(refs: SymbolRef[]): string {
|
|
107
|
+
const lines: string[] = [];
|
|
108
|
+
const declaredTags = new Set<string>();
|
|
109
|
+
for (const { name, info, access } of refs) {
|
|
110
|
+
// An address-cast macro declares itself: the header's own body, verbatim. It must NOT become
|
|
111
|
+
// an `extern` — that is the whole point of the fact (an extern emits a relocated pool word
|
|
112
|
+
// where the macro emits the numeric one the target shows).
|
|
113
|
+
if (info.macroBody !== undefined) {
|
|
114
|
+
lines.push(`#define ${name} ${info.macroBody}`);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (info.kind === 'code') {
|
|
118
|
+
// value-referenced code symbol ((u32)Func): any prototype makes the name visible, and
|
|
119
|
+
// the address is arity-independent. Call targets never reach this module (core excludes
|
|
120
|
+
// them from symbolRefs — see collectSymbolRefs).
|
|
121
|
+
lines.push(`void ${name}(void);`);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
switch (info.shape) {
|
|
125
|
+
case 'scalar': {
|
|
126
|
+
// Signedness default: absent + 4 bytes is the enum idiom (int ⇒ s32); absent + narrow
|
|
127
|
+
// has no honest spelling — skip (loud, see module note).
|
|
128
|
+
const t =
|
|
129
|
+
info.size !== undefined
|
|
130
|
+
? intType(info.size, info.signed ?? (info.size === 4 ? ENUM_IS_SIGNED : false))
|
|
131
|
+
: null;
|
|
132
|
+
if (t !== null && (info.signed !== undefined || info.size === 4)) {
|
|
133
|
+
lines.push(`extern ${quals(info)}${t} ${name};`);
|
|
134
|
+
}
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
case 'array': {
|
|
138
|
+
// Element type mirrors core's bare `gSym[i]` env typing exactly (elemSigned ?? false).
|
|
139
|
+
// A non-1/2/4 element width is never bare-indexed by core (only &gSym cast forms), so
|
|
140
|
+
// an unsized u8[] decl is codegen-identical for every spelling core emits.
|
|
141
|
+
const elem = info.elemSize !== undefined ? intType(info.elemSize, info.elemSigned ?? false) : null;
|
|
142
|
+
lines.push(`extern ${quals(info)}${elem ?? 'u8'} ${name}[];`);
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
case 'struct': {
|
|
146
|
+
// With a layout: the padded struct decl + a typed extern (the `gSym.field` spelling
|
|
147
|
+
// compiles against it). Without one, every core spelling is &gSym-based (a struct
|
|
148
|
+
// global never spells bare), so an unsized u8[] extern is codegen-identical.
|
|
149
|
+
const tag = info.structName ?? `Asmlift_${name}`;
|
|
150
|
+
const decl = structDecl(tag, info.layout, info.size);
|
|
151
|
+
if (decl !== null) {
|
|
152
|
+
if (!declaredTags.has(tag)) {
|
|
153
|
+
declaredTags.add(tag);
|
|
154
|
+
lines.push(decl);
|
|
155
|
+
}
|
|
156
|
+
lines.push(`extern ${quals(info)}struct ${tag} ${name};`);
|
|
157
|
+
} else {
|
|
158
|
+
lines.push(`extern ${quals(info)}u8 ${name}[];`);
|
|
159
|
+
}
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
case 'pointer': {
|
|
163
|
+
// With a POINTEE layout the emitter may spell an interior as `gPtr->member`, which only
|
|
164
|
+
// compiles against a pointer to that struct — so the pointee is declared here (the same
|
|
165
|
+
// padded synthesis a struct global gets) and the extern is typed. The declared pointee
|
|
166
|
+
// never changes bytes: the cell is 4 bytes whatever it addresses, and core's own lowering
|
|
167
|
+
// makes every arithmetic stride EXPLICIT (`(u8 *)gPtr + K` / `(u32)gPtr`), so no emitted
|
|
168
|
+
// expression is scaled by this type.
|
|
169
|
+
// Without one, pointee fidelity is unnecessary — load/store/compare of the cell are
|
|
170
|
+
// identical for any object-pointer type, and the output then never derefs through the
|
|
171
|
+
// decl's pointee.
|
|
172
|
+
// THE shared gate (symbols.ts pointeeFields): the typed extern is emitted on exactly
|
|
173
|
+
// the condition under which core may spell `gPtr->member`, so the two cannot disagree.
|
|
174
|
+
const tag = info.pointee?.structName;
|
|
175
|
+
const decl =
|
|
176
|
+
pointeeFields(info.pointee) !== null ? structDecl(tag!, info.pointee!.layout, info.pointee!.size) : null;
|
|
177
|
+
if (decl !== null && !declaredTags.has(tag!)) {
|
|
178
|
+
declaredTags.add(tag!);
|
|
179
|
+
lines.push(decl);
|
|
180
|
+
}
|
|
181
|
+
// The POINTEE's own qualifiers bind to the pointed-at type (`volatile struct S *g`); the
|
|
182
|
+
// cell's bind to the VARIABLE (`struct S *volatile g`). They are independent declarations
|
|
183
|
+
// of two different objects, and the synthesis reproduces each on its own side of the `*`.
|
|
184
|
+
const pointeeQuals = `${info.pointee?.volatile ? 'volatile ' : ''}${info.pointee?.const ? 'const ' : ''}`;
|
|
185
|
+
const pointeeType = decl !== null ? `${pointeeQuals}struct ${tag} *` : `${pointeeQuals}void *`;
|
|
186
|
+
lines.push(`extern ${pointeeType}${info.volatile ? 'volatile ' : ''}${info.const ? 'const ' : ''}${name};`);
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
default: {
|
|
190
|
+
// Name-only (no sidecar shape) — the second documented exception (see the module note;
|
|
191
|
+
// the first is the 4-byte signless enum cell). Skipping here was the original rule, but
|
|
192
|
+
// it made every named-spelling row of a symtab-only map project (marioparty3: names
|
|
193
|
+
// with no DWARF shapes) unreproducible in the self-declared world — the benchmark
|
|
194
|
+
// compiled those candidates inside the project headers, which declare the symbol.
|
|
195
|
+
// The width authority is the candidate's OWN IR (ref.access, rank.ts
|
|
196
|
+
// bareGlobalAccessFacts): a bare `name = v` / `x = name` compiles to the access the
|
|
197
|
+
// tree performed only under a decl of that exact width (`extern u16 g;` is `sh` where
|
|
198
|
+
// a guessed u32 is `sw`). Without a bare off-0 access fact, every core spelling goes
|
|
199
|
+
// through `&name` casts, where any object decl is address-identical — u32 is the
|
|
200
|
+
// fallback cell. A divergent decl can only LOSE score — the target bytes derive from
|
|
201
|
+
// the truth decls, so a mis-declared compile can never false-match (same argument as
|
|
202
|
+
// enumIsSigned).
|
|
203
|
+
const t = access ? intType(access.width, access.signed) : null;
|
|
204
|
+
lines.push(`extern ${quals(info)}${t ?? 'u32'} ${name};`);
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return lines.length ? lines.join('\n') + '\n' : '';
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** The object-like `#define`s out of a rendered declaration block.
|
|
213
|
+
*
|
|
214
|
+
* Address-cast macro defines are the one part of a synthesized block that must survive into the
|
|
215
|
+
* HEADERS world too. Everything else there is owned by the injected headers (a duplicate typedef
|
|
216
|
+
* or struct definition is a C89 hard error), but a duplicate `#define` with an identical body is
|
|
217
|
+
* legal — and a PREPROCESSED project context has no macros left at all, so dropping these turns a
|
|
218
|
+
* macro-named candidate into an `undeclared identifier` rather than a spelling choice. */
|
|
219
|
+
export function macroDefinesOf(declarations: string | undefined): string {
|
|
220
|
+
if (!declarations) {
|
|
221
|
+
return '';
|
|
222
|
+
}
|
|
223
|
+
const lines = declarations.split('\n').filter((l) => l.startsWith('#define '));
|
|
224
|
+
return lines.length ? lines.join('\n') + '\n' : '';
|
|
225
|
+
}
|
package/src/frontend/frontend.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// for target→frontend dispatch.
|
|
5
5
|
import type { Fn } from '../ir/core';
|
|
6
6
|
import type { Prototypes } from '../proto';
|
|
7
|
+
import type { SymbolMap } from '../symbols';
|
|
7
8
|
import type { TargetDescription } from '../target';
|
|
8
9
|
import type { AsmData } from './asmdata';
|
|
9
10
|
import type { AsmTextFormat } from './format';
|
|
@@ -17,6 +18,15 @@ export interface Frontend {
|
|
|
17
18
|
/** decode one function's assembly into an L1 Fn. `prototypes` supplies callee arities
|
|
18
19
|
* (and any other header facts the frontend needs); an empty map is valid. `asmData` is the
|
|
19
20
|
* OPTIONAL Regime-B side-table (data-section jump tables + relocations); absent ⇒ a
|
|
20
|
-
* dense-switch dispatch declines/loud-fails.
|
|
21
|
-
|
|
21
|
+
* dense-switch dispatch declines/loud-fails. `symbols` is the OPTIONAL address→symbol map
|
|
22
|
+
* (symbols.ts); today only the Thumb frontend consumes it (numeric-pool promotion) — the
|
|
23
|
+
* MIPS/PPC objdump dialect already carries symbol names in the asm text. */
|
|
24
|
+
lift(
|
|
25
|
+
name: string,
|
|
26
|
+
asm: string,
|
|
27
|
+
target: TargetDescription,
|
|
28
|
+
prototypes: Prototypes,
|
|
29
|
+
asmData?: AsmData,
|
|
30
|
+
symbols?: SymbolMap,
|
|
31
|
+
): Fn;
|
|
22
32
|
}
|
package/src/frontend/thumb.ts
CHANGED
|
@@ -19,7 +19,9 @@ import type { Opcode } from '../ir/opcodes';
|
|
|
19
19
|
import { T } from '../ir/types';
|
|
20
20
|
import { type Prototypes, protoArity } from '../proto';
|
|
21
21
|
import { RUNTIME_HELPERS } from '../raise/softdiv';
|
|
22
|
+
import { type SymbolMap, lookupInterior, lookupSymbol } from '../symbols';
|
|
22
23
|
import type { TargetDescription } from '../target';
|
|
24
|
+
import type { AsmData } from './asmdata';
|
|
23
25
|
import { pushSwitchBr } from './emit';
|
|
24
26
|
import { FrontendUnsupportedError } from './errors';
|
|
25
27
|
import { assertInputFormat } from './format';
|
|
@@ -171,10 +173,15 @@ function splitOperands(s: string): string[] {
|
|
|
171
173
|
// Parse a Thumb memory addressing operand `[base]` or `[base, #off]` into base register +
|
|
172
174
|
// constant byte offset. (Register-scaled indices like `[base, r1, lsl #2]` are not handled
|
|
173
175
|
// yet — agbcc materialises those as explicit add/lsl before the load in the cases we target.)
|
|
174
|
-
function parseAddr(operand: string): { base: string; off: number } {
|
|
176
|
+
function parseAddr(operand: string): { base: string; off: number; regOff?: string } {
|
|
175
177
|
const inner = operand.replace(/[[\]]/g, '').trim();
|
|
176
178
|
const parts = inner.split(',').map((s) => s.trim());
|
|
177
179
|
const base = parts[0];
|
|
180
|
+
// `[rB, rX]` — REGISTER-offset addressing. Surfaced to the caller so load/store DECLINE
|
|
181
|
+
// loud: silently reading `[rB]` (the old behavior) dropped the index — a silent miscompile.
|
|
182
|
+
if (parts[1] !== undefined && !parts[1].startsWith('#')) {
|
|
183
|
+
return { base, off: 0, regOff: parts[1] };
|
|
184
|
+
}
|
|
178
185
|
const off = parts[1]?.startsWith('#') ? imm(parts[1]) : 0;
|
|
179
186
|
return { base, off };
|
|
180
187
|
}
|
|
@@ -724,6 +731,37 @@ function poolRef(operand: string, dataWords: Map<string, string[]>): PoolRef | n
|
|
|
724
731
|
return { kind: 'unmodelled', why: `pool word '${w}' is a symbol offset or code label` };
|
|
725
732
|
}
|
|
726
733
|
|
|
734
|
+
/** Does this function's literal pool name at least one EXTERNAL symbol?
|
|
735
|
+
*
|
|
736
|
+
* agbcc emits a pool word symbolically exactly when the source expression named a linker symbol,
|
|
737
|
+
* and numerically when it did not (`*(vu16 *)0x4000130`, an address-cast macro). So within one
|
|
738
|
+
* function, a numeric word sitting alongside a symbolic one is numeric *by the source's choice* —
|
|
739
|
+
* which is what lets {@link liftThumb} refuse to invent a name for it.
|
|
740
|
+
*
|
|
741
|
+
* The witness is required because that inference only holds for asm that KEPT its symbols. A
|
|
742
|
+
* linked-ROM disassembly resolves every relocation to a number, and there "numeric" says nothing
|
|
743
|
+
* about the source; vetoing on it would disable the map's naming for the users who need it most.
|
|
744
|
+
* A function whose pool names nothing external is therefore left alone (both spellings still
|
|
745
|
+
* enumerate, and the differ referees) rather than being read as evidence of anything.
|
|
746
|
+
*
|
|
747
|
+
* Labels DEFINED in this same asm — the jump-table pointer word, a pret-style `_08012358` pool
|
|
748
|
+
* label — are not external symbols and never witness: they survive disassembly whether or not
|
|
749
|
+
* relocations did. */
|
|
750
|
+
function poolNamesASymbol(dataWords: Map<string, string[]>, blockLabels: Set<string>): boolean {
|
|
751
|
+
for (const [, words] of dataWords) {
|
|
752
|
+
for (const raw of words) {
|
|
753
|
+
const w = raw.trim();
|
|
754
|
+
if (!/^[A-Za-z_]\w*$/.test(w) || w.startsWith('.L')) {
|
|
755
|
+
continue;
|
|
756
|
+
}
|
|
757
|
+
if (!dataWords.has(w) && !blockLabels.has(w)) {
|
|
758
|
+
return true;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
return false;
|
|
763
|
+
}
|
|
764
|
+
|
|
727
765
|
// Recover an agbcc Thumb jump-table dispatch. Given a dispatch block `disp` ending in `mov pc, rV`
|
|
728
766
|
// and its unique bounds predecessor `bounds` ending in `cmp rX,#(N-1); bhi DEF`, verify the exact
|
|
729
767
|
// idiom and read the inline table — else return null (→ the indirect-jump loud-fail fires). The
|
|
@@ -816,7 +854,14 @@ function recoverJumpTable(
|
|
|
816
854
|
/** Lift decoded asm → an L1 Fn with block-argument SSA. `prototypes` supplies each callee's
|
|
817
855
|
* declared parameter count (from the project's headers); it is authoritative for recovering
|
|
818
856
|
* how many argument registers a `bl` passes (falling back to a heuristic when absent). */
|
|
819
|
-
export function lift(
|
|
857
|
+
export function lift(
|
|
858
|
+
name: string,
|
|
859
|
+
asm: string,
|
|
860
|
+
target: TargetDescription,
|
|
861
|
+
prototypes: Prototypes = {},
|
|
862
|
+
_asmData?: AsmData,
|
|
863
|
+
symbols?: SymbolMap,
|
|
864
|
+
): Fn {
|
|
820
865
|
assertInputFormat('thumb', 'gnu-as', asm);
|
|
821
866
|
const { blocks: rawBlocks, dataWords } = decode(name, asm);
|
|
822
867
|
|
|
@@ -825,6 +870,9 @@ export function lift(name: string, asm: string, target: TargetDescription, proto
|
|
|
825
870
|
// dispatch block is ELIDED from the CFG. A `mov pc` that is NOT a recognised table falls through
|
|
826
871
|
// to the loud-fail below.
|
|
827
872
|
const blockLabels = new Set(rawBlocks.map((b) => b.label));
|
|
873
|
+
// Whether THIS asm preserves symbol names in its literal pools — the witness the numeric-pool
|
|
874
|
+
// naming veto needs (see poolNamesASymbol).
|
|
875
|
+
const poolNamesSymbols = poolNamesASymbol(dataWords, blockLabels);
|
|
828
876
|
// Any label referenced as a branch target (so we can tell if an elided dispatch block has a SECOND
|
|
829
877
|
// predecessor — a `b disp` from elsewhere — which would dangle after elision; decline if so).
|
|
830
878
|
const branchTargets = new Set<string>();
|
|
@@ -1278,6 +1326,54 @@ export function lift(name: string, asm: string, target: TargetDescription, proto
|
|
|
1278
1326
|
if (ins.mnemonic === 'ldr' && b !== undefined) {
|
|
1279
1327
|
const pr = poolRef(b, dataWords);
|
|
1280
1328
|
if (pr?.kind === 'const') {
|
|
1329
|
+
// Numeric-pool PROMOTION (symbols.ts): a pool-loaded word whose value the
|
|
1330
|
+
// project's symbol map knows becomes the NAMED global's address — the same
|
|
1331
|
+
// `gaddr` the symbol-pool path emits, so everything downstream is the existing
|
|
1332
|
+
// named-global machinery. Only pool-loaded words promote (an address built by
|
|
1333
|
+
// arithmetic never reaches here); a promoted `code` symbol carries `code: true`
|
|
1334
|
+
// so the structurer spells it `(u32)Name`, not `&Name`.
|
|
1335
|
+
//
|
|
1336
|
+
// VETOED when this asm's pool names other symbols (poolNamesASymbol): agbcc would
|
|
1337
|
+
// have emitted THIS word symbolically too had the source named it, so promoting it
|
|
1338
|
+
// spells a name the source did not use.
|
|
1339
|
+
//
|
|
1340
|
+
// …but the veto is really about RELOCATION, not about naming. An `extern` name makes
|
|
1341
|
+
// the compiler emit a relocated pool word, which contradicts the numeric word the
|
|
1342
|
+
// target shows. An address-cast MACRO expands to that same numeric literal, so it is
|
|
1343
|
+
// COMPATIBLE with the evidence by construction and is never vetoed — indeed it is
|
|
1344
|
+
// the spelling the numeric word is evidence FOR (klonoa's true source reaches these
|
|
1345
|
+
// cells through exactly such macros). Nothing is guessed in either case: a vetoed
|
|
1346
|
+
// word stays the raw constant the target says it is.
|
|
1347
|
+
const found = symbols ? lookupSymbol(symbols, pr.value) : null;
|
|
1348
|
+
const si = found && (!poolNamesSymbols || found.macroBody !== undefined) ? found : null;
|
|
1349
|
+
if (si) {
|
|
1350
|
+
const res = mkValue(T.unk(32));
|
|
1351
|
+
irb.ops.push(
|
|
1352
|
+
mkOp('gaddr', {
|
|
1353
|
+
results: [res],
|
|
1354
|
+
attrs: { sym: si.name, ...(si.kind === 'code' ? { code: true } : {}) },
|
|
1355
|
+
}),
|
|
1356
|
+
);
|
|
1357
|
+
writeVar(reg(a), bi, res);
|
|
1358
|
+
break;
|
|
1359
|
+
}
|
|
1360
|
+
// INTERIOR attribution: a value strictly inside a sized data symbol becomes
|
|
1361
|
+
// `gaddr sym + offset` — the `&gSym + K` tree structure.ts already lowers (and,
|
|
1362
|
+
// with a struct layout, spells as the named field). Sized symbols only; an
|
|
1363
|
+
// unattributed address stays a raw const — nothing guesses.
|
|
1364
|
+
// Interior attribution is always an `&gSym + K` spelling — extern-shaped, hence
|
|
1365
|
+
// relocated — so the veto applies to it without the macro exemption above.
|
|
1366
|
+
const interior = symbols && !poolNamesSymbols ? lookupInterior(symbols, pr.value) : null;
|
|
1367
|
+
if (interior) {
|
|
1368
|
+
const g = mkValue(T.unk(32));
|
|
1369
|
+
const k = mkValue(T.unk(32));
|
|
1370
|
+
const res = mkValue(T.unk(32));
|
|
1371
|
+
irb.ops.push(mkOp('gaddr', { results: [g], attrs: { sym: interior.info.name } }));
|
|
1372
|
+
irb.ops.push(mkOp('const', { results: [k], attrs: { value: interior.offset } }));
|
|
1373
|
+
irb.ops.push(mkOp('add', { operands: [g, k], results: [res] }));
|
|
1374
|
+
writeVar(reg(a), bi, res);
|
|
1375
|
+
break;
|
|
1376
|
+
}
|
|
1281
1377
|
const res = mkValue(T.unk(32));
|
|
1282
1378
|
irb.ops.push(mkOp('const', { results: [res], attrs: { value: pr.value } }));
|
|
1283
1379
|
writeVar(reg(a), bi, res);
|
|
@@ -1303,9 +1399,19 @@ export function lift(name: string, asm: string, target: TargetDescription, proto
|
|
|
1303
1399
|
}
|
|
1304
1400
|
const width = /b/.test(ins.mnemonic) ? 1 : /h/.test(ins.mnemonic) ? 2 : 4;
|
|
1305
1401
|
const signed = ins.mnemonic === 'ldr' || /s/.test(ins.mnemonic.slice(3));
|
|
1306
|
-
const { base, off } = parseAddr(b);
|
|
1402
|
+
const { base, off, regOff } = parseAddr(b);
|
|
1403
|
+
// `[rB, rX]` register-offset: lower EXACTLY as `rB + rX` then a load at offset 0 —
|
|
1404
|
+
// the same address arithmetic the encoding performs. (parseAddr used to silently
|
|
1405
|
+
// read `[rB]`, dropping the index — a silent miscompile; ldrsh exists ONLY in this
|
|
1406
|
+
// form in Thumb-1, so every ldrsh went through here.)
|
|
1407
|
+
let baseVal = readData(base, bi);
|
|
1408
|
+
if (regOff !== undefined) {
|
|
1409
|
+
const sum = mkValue(T.unk(32));
|
|
1410
|
+
irb.ops.push(mkOp('add', { operands: [baseVal, readData(regOff, bi)], results: [sum] }));
|
|
1411
|
+
baseVal = sum;
|
|
1412
|
+
}
|
|
1307
1413
|
const res = mkValue(T.unk(32));
|
|
1308
|
-
irb.ops.push(mkOp('load', { operands: [
|
|
1414
|
+
irb.ops.push(mkOp('load', { operands: [baseVal], results: [res], attrs: { off, width, signed } }));
|
|
1309
1415
|
writeVar(reg(a), bi, res);
|
|
1310
1416
|
break;
|
|
1311
1417
|
}
|
|
@@ -1318,8 +1424,15 @@ export function lift(name: string, asm: string, target: TargetDescription, proto
|
|
|
1318
1424
|
break;
|
|
1319
1425
|
}
|
|
1320
1426
|
const width = /b/.test(ins.mnemonic) ? 1 : /h/.test(ins.mnemonic) ? 2 : 4;
|
|
1321
|
-
const { base, off } = parseAddr(b);
|
|
1322
|
-
|
|
1427
|
+
const { base, off, regOff } = parseAddr(b);
|
|
1428
|
+
let storeBase = readData(base, bi);
|
|
1429
|
+
if (regOff !== undefined) {
|
|
1430
|
+
// register-offset store: same exact `rB + rX` lowering as the load path above
|
|
1431
|
+
const sum = mkValue(T.unk(32));
|
|
1432
|
+
irb.ops.push(mkOp('add', { operands: [storeBase, readData(regOff, bi)], results: [sum] }));
|
|
1433
|
+
storeBase = sum;
|
|
1434
|
+
}
|
|
1435
|
+
irb.ops.push(mkOp('store', { operands: [storeBase, readData(reg(a), bi)], attrs: { off, width } }));
|
|
1323
1436
|
break;
|
|
1324
1437
|
}
|
|
1325
1438
|
case 'bl':
|
package/src/l3/ast.ts
CHANGED
|
@@ -43,7 +43,7 @@ export type Expr =
|
|
|
43
43
|
// pointer, so the byte offset resolves to a named field instead of a scaled array index).
|
|
44
44
|
// Unlike `index`, this carries the field NAME (which encodes the byte offset, `field_<off>`),
|
|
45
45
|
// not a width-scaled number — the byte-offset-carrying member access cpp.ts's sub-word guard needs.
|
|
46
|
-
| { k: 'field'; base: Expr; name: string }
|
|
46
|
+
| { k: 'field'; base: Expr; name: string; dot?: true }
|
|
47
47
|
// A GAP MARKER — the annotate-mode (`onGap: "annotate"`) spelling of a value asmlift could not
|
|
48
48
|
// faithfully lift (an unmodelled instruction's `opaque` result, an unlowered transient op, a
|
|
49
49
|
// dropped def). Every backend spells it as a call to the UNDEFINED symbol `ASMLIFT_ERROR("reason",
|
|
@@ -111,6 +111,10 @@ export interface SFn {
|
|
|
111
111
|
name: string;
|
|
112
112
|
params: { name: string; type: IrType }[];
|
|
113
113
|
locals: { name: string; type: IrType }[]; // recovered locals, declared at function top
|
|
114
|
+
/** project globals referenced with a known declaration shape (symbol map) — typed for the
|
|
115
|
+
* legalization env (exprCType) but NEVER declared by a backend: the project's own headers
|
|
116
|
+
* declare them, exactly like every other global name asmlift emits. */
|
|
117
|
+
globals?: { name: string; type: IrType }[];
|
|
114
118
|
retType: IrType;
|
|
115
119
|
body: Stmt[];
|
|
116
120
|
/** Struct types this function's fields reference, declared above it by the backend. Empty
|
|
@@ -148,7 +152,9 @@ export function dotBase(f: Extract<Expr, { k: 'field' }>): Extract<Expr, { k: 'i
|
|
|
148
152
|
|
|
149
153
|
/** Boolean projection of `dotBase` for conditions that need no narrowing. */
|
|
150
154
|
export function fieldSpellsDot(f: Extract<Expr, { k: 'field' }>): boolean {
|
|
151
|
-
|
|
155
|
+
// dot also spells a STRUCT-VALUE global's field (`gSym.field`, the symbol-map layout path) —
|
|
156
|
+
// marked explicitly by the structurer via `dot: true` since the base is a `var`, not an index.
|
|
157
|
+
return dotBase(f) !== undefined || f.dot === true;
|
|
152
158
|
}
|
|
153
159
|
|
|
154
160
|
/** Structural equality of two expression trees. THE one copy of Expr deep-equal (like
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// asmlift — SELF-DECLARING CANDIDATES: the pure map-reference query
|
|
2
|
+
// (research/self-declaring-candidates-2026-07-26.md).
|
|
3
|
+
//
|
|
4
|
+
// `collectSymbolRefs` derives, from a FINAL structured tree, every map-derived symbol the body
|
|
5
|
+
// references in a VALUE context — the input to the scoring layer's declaration synthesis
|
|
6
|
+
// (@asmlift/cli declare.ts). It is a pure tree query with no pipeline state: the enumeration
|
|
7
|
+
// layer (rank.ts) calls it exactly once per candidate, on the tree the candidate's source was
|
|
8
|
+
// emitted from, at the moment the candidate is finalized. There is deliberately NO cached
|
|
9
|
+
// `symbolRefs` field on `SFn` — a carried field would oblige every future l3 pass to remember
|
|
10
|
+
// to recompute it (a dead-store DCE that drops a tree's only reference would otherwise leave a
|
|
11
|
+
// stale ref, transitively reintroducing the hazards the collector excludes). Deriving at the
|
|
12
|
+
// consumption point makes staleness impossible by construction.
|
|
13
|
+
import type { SymbolInfo } from '../symbols';
|
|
14
|
+
import { Expr, Stmt, exprChildren, stmtChildren, stmtExprs } from './ast';
|
|
15
|
+
|
|
16
|
+
/** One recorded map-symbol VALUE reference — a name the tree references plus its map facts. */
|
|
17
|
+
export interface SymbolRef {
|
|
18
|
+
name: string;
|
|
19
|
+
info: SymbolInfo;
|
|
20
|
+
/** NAME-ONLY symbols (no map shape): the bare off-0 access facts observed in the candidate's
|
|
21
|
+
* own IR — attached by the enumeration (rank.ts bareGlobalAccessFacts), consumed by the
|
|
22
|
+
* declaration synthesis (declare.ts) as the width/signedness authority for `extern T name;`. */
|
|
23
|
+
access?: { width: number; signed: boolean };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The map-derived symbols a structured body references in a VALUE context — the input to the
|
|
27
|
+
* scoring layer's declaration synthesis. A name counts when it appears as a `var`/`addr` leaf
|
|
28
|
+
* and the map knows it (bare `gSym`, `&gSym`, `(u32)Func`, a `field` base — all reduce to
|
|
29
|
+
* those leaves). A name that is ANY call's target is excluded entirely, even if also
|
|
30
|
+
* value-referenced: prototyping a called symbol `void F(void);` hard-errors under gcc-2.9
|
|
31
|
+
* when the call passes args, while leaving it undeclared keeps today's implicit-declaration
|
|
32
|
+
* behavior (the one honest option without arity knowledge). The function's OWN name
|
|
33
|
+
* (`selfName`) is excluded too — the candidate's definition IS its declaration, and a
|
|
34
|
+
* synthesized `void F(void);` above `s32 F(...)` is a conflicting-types hard error (a
|
|
35
|
+
* self-address reference resolves against the definition itself). */
|
|
36
|
+
export function collectSymbolRefs(body: Stmt[], symbols: Map<string, SymbolInfo>, selfName: string): SymbolRef[] {
|
|
37
|
+
const called = new Set<string>();
|
|
38
|
+
const valueRefs = new Set<string>();
|
|
39
|
+
const visitExpr = (e: Expr): void => {
|
|
40
|
+
if (e.k === 'call') {
|
|
41
|
+
called.add(e.fn);
|
|
42
|
+
} else if ((e.k === 'var' || e.k === 'addr') && symbols.has(e.name)) {
|
|
43
|
+
valueRefs.add(e.name);
|
|
44
|
+
}
|
|
45
|
+
exprChildren(e).forEach(visitExpr);
|
|
46
|
+
};
|
|
47
|
+
const visitStmt = (s: Stmt): void => {
|
|
48
|
+
// an `assign` carries its target as a NAME, not an Expr — a scalar global WRITE
|
|
49
|
+
// (`gSym = x;`) references the symbol every bit as much as a read does
|
|
50
|
+
if (s.k === 'assign' && symbols.has(s.name)) {
|
|
51
|
+
valueRefs.add(s.name);
|
|
52
|
+
}
|
|
53
|
+
stmtExprs(s).forEach(visitExpr);
|
|
54
|
+
stmtChildren(s).forEach(visitStmt);
|
|
55
|
+
};
|
|
56
|
+
body.forEach(visitStmt);
|
|
57
|
+
return [...valueRefs]
|
|
58
|
+
.filter((n) => !called.has(n) && n !== selfName)
|
|
59
|
+
.sort()
|
|
60
|
+
.map((n) => ({ name: n, info: symbols.get(n)! }));
|
|
61
|
+
}
|