@asmlift/core 0.8.1 → 0.10.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 +36 -31
- package/package.json +1 -1
- package/src/aggregate.ts +397 -0
- package/src/backend/c.ts +6 -2
- package/src/backend/cfamily.ts +370 -56
- package/src/backend/cpp.ts +116 -10
- package/src/backend/pascal.ts +69 -11
- package/src/cdecl/constant.ts +59 -0
- package/src/cdecl/lex.ts +209 -0
- package/src/cdecl/parse.ts +975 -0
- package/src/cdecl/spell.ts +69 -0
- package/src/codegen-flags.ts +38 -6
- package/src/compiler-diagnostics.ts +194 -0
- package/src/contracts.ts +777 -84
- package/src/declare.ts +132 -13
- package/src/frontend/asmdata.ts +26 -6
- package/src/frontend/call-plan.ts +1590 -0
- package/src/frontend/dead-reads.ts +628 -0
- package/src/frontend/device-pins.ts +296 -0
- package/src/frontend/emit.ts +4 -3
- package/src/frontend/errors.ts +5 -0
- package/src/frontend/flags-edge.ts +101 -0
- package/src/frontend/fpu.ts +374 -0
- package/src/frontend/frame-objects.ts +3171 -0
- package/src/frontend/frontend.ts +64 -3
- package/src/frontend/high-half.ts +55 -5
- package/src/frontend/jump-table.ts +530 -0
- package/src/frontend/local-object.ts +616 -0
- package/src/frontend/mips.ts +2392 -208
- package/src/frontend/opaque.ts +97 -18
- package/src/frontend/own-struct-return.ts +500 -0
- package/src/frontend/ppc.ts +3137 -267
- package/src/frontend/register-struct-call.ts +484 -0
- package/src/frontend/reloc-symbol.ts +109 -13
- package/src/frontend/splat.ts +108 -5
- package/src/frontend/ssa.ts +2098 -89
- package/src/frontend/stackargs.ts +172 -42
- package/src/frontend/thumb.ts +4563 -2091
- package/src/ir/alias.ts +71 -0
- package/src/ir/bits.ts +681 -1
- package/src/ir/core.ts +199 -6
- package/src/ir/discipline.ts +313 -0
- package/src/ir/entry.ts +159 -0
- package/src/ir/float-bits.ts +47 -0
- package/src/{structure → ir}/loops.ts +6 -2
- package/src/ir/opcodes.ts +193 -121
- package/src/ir/parse.ts +1 -0
- package/src/ir/types.ts +202 -2
- package/src/ir/verify.ts +219 -3
- package/src/l3/address.ts +5 -0
- package/src/l3/argcopy.ts +38 -6
- package/src/l3/ast.ts +467 -23
- package/src/l3/basecse.ts +33 -5
- package/src/l3/coalesce.ts +21 -13
- package/src/l3/constinit.ts +382 -0
- package/src/l3/dce.ts +34 -12
- package/src/l3/gates.ts +9 -3
- package/src/l3/hoist.ts +8 -7
- package/src/l3/initfirst.ts +68 -19
- package/src/l3/intcell.ts +98 -0
- package/src/l3/mentions.ts +10 -4
- package/src/l3/narrowdecl.ts +259 -0
- package/src/l3/nearbase.ts +10 -5
- package/src/l3/parkfirst.ts +8 -5
- package/src/l3/pollguard.ts +11 -4
- package/src/l3/ptrcell.ts +354 -0
- package/src/l3/ptrfield.ts +123 -8
- package/src/l3/regspell.ts +26 -7
- package/src/l3/reindex.ts +293 -103
- package/src/l3/storage.ts +98 -1
- package/src/l3/symbol-refs.ts +182 -23
- package/src/l3/tailmerge.ts +9 -20
- package/src/l3/typing.ts +237 -32
- package/src/l3/unreduce.ts +10 -9
- package/src/l3/volatileptr.ts +17 -54
- package/src/l3/volstore.ts +96 -7
- package/src/mangle.ts +41 -1
- package/src/pattern/engine.ts +373 -82
- package/src/pipeline.ts +589 -27
- package/src/proto-context.ts +1648 -0
- package/src/proto.ts +1203 -49
- package/src/raise/aggmembers.ts +290 -0
- package/src/raise/arrays.ts +8 -3
- package/src/raise/declared-volatile.ts +307 -0
- package/src/raise/divpow2.ts +2 -2
- package/src/raise/errors.ts +17 -0
- package/src/raise/floathelpers.ts +464 -0
- package/src/raise/globalshape.ts +11 -5
- package/src/raise/gvn.ts +29 -8
- package/src/raise/latch.ts +162 -6
- package/src/raise/memberarrays.ts +10 -4
- package/src/raise/narrow.ts +29 -2
- package/src/raise/narrowlocal.ts +11 -6
- package/src/raise/offsetnames.ts +494 -0
- package/src/raise/pairparams.ts +114 -0
- package/src/raise/paramwidth.ts +203 -6
- package/src/raise/pre-recovery.ts +174 -17
- package/src/raise/pre-structure.ts +54 -0
- package/src/raise/recover.ts +26 -15
- package/src/raise/reroll.ts +694 -0
- package/src/raise/retsink.ts +1 -1
- package/src/raise/shortcircuit.ts +66 -30
- package/src/raise/softdiv.ts +21 -23
- package/src/raise/storesign.ts +153 -0
- package/src/raise/struct-arrays.ts +34 -14
- package/src/raise/structs.ts +674 -98
- package/src/raise/truncload.ts +367 -0
- package/src/raise/unthread.ts +400 -0
- package/src/raise/widehelpers.ts +106 -0
- package/src/rank-declare.ts +143 -12
- package/src/rank-variations.ts +117 -31
- package/src/rank.ts +563 -122
- package/src/runtime-helpers.ts +405 -0
- package/src/stillborn.ts +289 -0
- package/src/structure/analysis.ts +1449 -449
- package/src/structure/arith-ops.ts +23 -0
- package/src/structure/globalaccess.ts +111 -9
- package/src/structure/hazards.ts +641 -114
- package/src/structure/local-statics.ts +517 -0
- package/src/structure/namecoalesce.ts +2 -2
- package/src/structure/pointer-spelling.ts +1106 -0
- package/src/structure/redundant-test.ts +99 -0
- package/src/structure/structure.ts +3478 -975
- package/src/structure/switch-recover.ts +403 -178
- package/src/symbols.ts +44 -3
- package/src/target.ts +1387 -71
- package/src/trace.ts +66 -28
- package/src/variation-definitions.ts +243 -13
- package/src/variation-gates.ts +2 -0
- package/src/variation-tokens.ts +25 -7
package/src/contracts.ts
CHANGED
|
@@ -3,21 +3,30 @@
|
|
|
3
3
|
// decompileRanked / decompileWithReport).
|
|
4
4
|
// A pass that regresses fails AT its boundary with a diagnostic, not three stages later as
|
|
5
5
|
// wrong C.
|
|
6
|
-
import {
|
|
7
|
-
import { type
|
|
8
|
-
import
|
|
6
|
+
import { constAddressOf, globalBaseOf } from './ir/alias';
|
|
7
|
+
import { type Fn, type Op, type Value, calleeName, reachableBlocks } from './ir/core';
|
|
8
|
+
import { placedAt } from './ir/discipline';
|
|
9
|
+
import { MEM_BASE_OPS } from './ir/opcodes';
|
|
10
|
+
import { type IrType, memberOf, typeToString } from './ir/types';
|
|
11
|
+
import { cellAddress, qualifiedBase } from './l3/address';
|
|
12
|
+
import { type BinOp, type Expr, type SFn, type Stmt, calleeOf } from './l3/ast';
|
|
9
13
|
import {
|
|
14
|
+
assignsIn,
|
|
10
15
|
exprChildren,
|
|
11
16
|
fieldSpellsDot,
|
|
12
17
|
gapReasonFor,
|
|
13
18
|
mapExprChildren,
|
|
19
|
+
mentionedName,
|
|
14
20
|
stmtChildren,
|
|
15
21
|
stmtExprs,
|
|
16
22
|
stmtLists,
|
|
23
|
+
taintOverAssigns,
|
|
17
24
|
walkExprs,
|
|
18
25
|
} from './l3/ast';
|
|
19
26
|
import { mentionedLocals } from './l3/mentions';
|
|
27
|
+
import { localAddresses } from './l3/storage';
|
|
20
28
|
import { declaredTypes, exprCType } from './l3/typing';
|
|
29
|
+
import { castOffDeclaration } from './l3/volstore';
|
|
21
30
|
|
|
22
31
|
export class ContractError extends Error {
|
|
23
32
|
constructor(message: string) {
|
|
@@ -68,9 +77,14 @@ export function assertResolved(sfn: SFn): void {
|
|
|
68
77
|
// Every Expr kind that CARRIES a name, not just `var` — each is read through the same
|
|
69
78
|
// `map.get(d)!` / `attrs.x as string` and prints straight into the source. `carriesName` is asked
|
|
70
79
|
// separately because an ABSENT name is the case being caught: keying off `nameOf` alone refuses nothing.
|
|
71
|
-
const carriesName = (e: Expr): boolean =>
|
|
80
|
+
const carriesName = (e: Expr): boolean =>
|
|
81
|
+
e.k === 'var' ||
|
|
82
|
+
e.k === 'addr' ||
|
|
83
|
+
e.k === 'field' ||
|
|
84
|
+
(e.k === 'call' && typeof e.fn === 'string') ||
|
|
85
|
+
e.k === 'postincr';
|
|
72
86
|
const nameOf = (e: Expr): string | undefined =>
|
|
73
|
-
e.k === 'call' ? e
|
|
87
|
+
e.k === 'call' ? calleeOf(e) : e.k === 'marker' ? undefined : (e as { name?: string }).name;
|
|
74
88
|
const badExpr = (e: Expr): boolean => (carriesName(e) && badName(nameOf(e))) || exprChildren(e).some(badExpr);
|
|
75
89
|
// An `assign`'s DESTINATION is a bare string field, so the expression walk never reaches it.
|
|
76
90
|
const badStmt = (s: Stmt): boolean =>
|
|
@@ -86,10 +100,10 @@ export function assertResolved(sfn: SFn): void {
|
|
|
86
100
|
// ── effects: executed once, never dropped ──────────────────────────────────────────────────
|
|
87
101
|
//
|
|
88
102
|
// The three contracts around this one are about TYPING and SPELLABILITY. Nothing checked the
|
|
89
|
-
// property the structurer's materialization model exists to preserve:
|
|
90
|
-
// exactly as often in the emitted source. Its two failure modes are the two that hurt
|
|
91
|
-
// asmlift's first rule is that a loud failure beats a silently wrong answer, and both of
|
|
92
|
-
// silent:
|
|
103
|
+
// property the structurer's materialization model exists to preserve: an execution the asm makes
|
|
104
|
+
// must happen exactly as often in the emitted source. Its two failure modes are the two that hurt
|
|
105
|
+
// most — asmlift's first rule is that a loud failure beats a silently wrong answer, and both of
|
|
106
|
+
// these are silent:
|
|
93
107
|
//
|
|
94
108
|
// • DROPPED — a call the asm makes has no counterpart in the tree at all;
|
|
95
109
|
// • RE-RUN — inlining a call's value at more than one render position (or a structuring copy
|
|
@@ -97,22 +111,55 @@ export function assertResolved(sfn: SFn): void {
|
|
|
97
111
|
// recovered switch fall-through hit exactly this shape, and only an adversarial reviewer
|
|
98
112
|
// caught it.
|
|
99
113
|
//
|
|
100
|
-
//
|
|
114
|
+
// What is counted is what each render executes (ir/discipline.ts `counted`): a call, a memory access
|
|
115
|
+
// the lift pinned (the `device` placement), whose qualified spelling the recompile performs once per
|
|
116
|
+
// render, and a read of an object the symbol map declares volatile (`declared`), which the tree
|
|
117
|
+
// spells by the object's name. A store to such an object is checked for its spelling only.
|
|
118
|
+
// Deliberately narrow beyond that, so it never declines a function that is fine:
|
|
101
119
|
//
|
|
102
|
-
// • CALLS only.
|
|
103
|
-
// model, and the alias gate governs it);
|
|
104
|
-
// the readability DCE pass is allowed to drop a provably dead one.
|
|
120
|
+
// • CALLS AND QUALIFIED ACCESSES only. A plain load legitimately re-renders (that is the whole point
|
|
121
|
+
// of the inline-at-use model, and the alias gate governs it); a plain store is checked by
|
|
122
|
+
// neither direction because the readability DCE pass is allowed to drop a provably dead one.
|
|
105
123
|
// • PER PATH, not per tree. Structuring may legitimately emit one block twice — two exclusive
|
|
106
|
-
// switch arms sharing a body, a duplicated return merge
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
|
|
124
|
+
// switch arms sharing a body, a duplicated return merge, an exit tail copied into each
|
|
125
|
+
// `if (…) { …; return; }` that leaves a loop — and each path still executes it once. So the
|
|
126
|
+
// duplication rule compares the maximum over syntactic root-to-leaf paths (a branch takes the
|
|
127
|
+
// max of its arms, a loop body counts once, a fall-through arm chains into the next, and a
|
|
128
|
+
// `return` ends its path) against the IR's static count. What follows a statement every path
|
|
129
|
+
// leaves is on no path, so it is refused rather than left uncounted: it is code the asm never
|
|
130
|
+
// ran, and a region duplicated there would pass the count unseen.
|
|
131
|
+
// • Names the IR does not have are ignored. A call through a register names no callee, so every
|
|
132
|
+
// one is counted under the single key `call:(*pointer)`: the count holds for indirect calls as
|
|
133
|
+
// a whole, and a pass that makes one of them call what another calls goes unseen.
|
|
134
|
+
// • A pinned access is keyed by whether it reads or writes, and by the constant address both sides
|
|
135
|
+
// can read off it — the IR's base and offset, the tree's `cellAddress` — or `?` where a side
|
|
136
|
+
// cannot. A read never stands for a write. A `?` render may stand for any pinned access and a
|
|
137
|
+
// `?` access may render at any address, and each render is one execution, so both rules are
|
|
138
|
+
// Hall's condition over that compatibility: the renders on a path at any set of addresses are
|
|
139
|
+
// at most those addresses' accesses plus the `?` accesses, and the accesses at any set of
|
|
140
|
+
// addresses have at least as many renders there plus the `?` renders. An access through a named
|
|
141
|
+
// global is counted for re-runs and never as dropped: its qualifier may be the global's
|
|
142
|
+
// declaration, which leaves nothing in the tree.
|
|
143
|
+
// • A declared read is keyed by the object it reads, and counted for re-runs only. The tree side
|
|
144
|
+
// counts each access whose address is that object's name plus arithmetic, once, however it is
|
|
145
|
+
// spelled — `gSym`, `gSym[i]`, `gSym.f`, `gPtr->f`, a `volatile` cast through `&gSym` — and
|
|
146
|
+
// never a store target. An access whose base is another access is the inner one's: through
|
|
147
|
+
// `gPtr->arr[i]` the object is read once. One that reaches the object through a cast with no
|
|
148
|
+
// `volatile` — the tree's own, or the one the printer puts on a base that does not stride the
|
|
149
|
+
// access (l3/volstore.ts `castOffDeclaration`) — is a plain read the compiler may hoist or
|
|
150
|
+
// delete, and is refused. Objects the asm also reads unstamped are skipped, since nothing tells
|
|
151
|
+
// their renders apart. A pinned access through the object's name may render as one of its
|
|
152
|
+
// reads.
|
|
153
|
+
// • A declared store is not counted: it renders at its own position, once. One spelled through a
|
|
154
|
+
// cast with no `volatile` is a plain store the compiler may delete or sink, and is refused, as
|
|
155
|
+
// the read is. Objects the asm also writes unstamped are skipped, as for reads.
|
|
156
|
+
/** Executions per key: `call:<target>`, or `call:(*pointer)` for every call through a register;
|
|
157
|
+
* `device:r:<0xaddress>` for a pinned read and `device:w:<0xaddress>` for a pinned write, with `?`
|
|
158
|
+
* for the address where the counting side cannot read one; or `declared:<object>`. */
|
|
159
|
+
type EffectCounts = Map<string, number>;
|
|
113
160
|
|
|
114
161
|
/** per-key combine of two count maps (`sum` for sequence, `max` for exclusive alternatives) */
|
|
115
|
-
function combine(a:
|
|
162
|
+
function combine(a: EffectCounts, b: EffectCounts, f: (x: number, y: number) => number): EffectCounts {
|
|
116
163
|
const out = new Map(a);
|
|
117
164
|
for (const [k, v] of b) {
|
|
118
165
|
out.set(k, f(out.get(k) ?? 0, v));
|
|
@@ -120,80 +167,325 @@ function combine(a: CallCounts, b: CallCounts, f: (x: number, y: number) => numb
|
|
|
120
167
|
return out;
|
|
121
168
|
}
|
|
122
169
|
|
|
123
|
-
|
|
124
|
-
|
|
170
|
+
const DECLARED = 'declared:';
|
|
171
|
+
const STRIPPED = 'stripped:';
|
|
172
|
+
const STRIPPED_WRITE = 'stripped-write:';
|
|
173
|
+
/** The one callee key every indirect call is counted under: no identifier can spell it. */
|
|
174
|
+
const INDIRECT_CALL = '(*pointer)';
|
|
175
|
+
type Direction = 'r' | 'w';
|
|
176
|
+
const deviceKey = (dir: Direction, addr: number | null): string =>
|
|
177
|
+
`device:${dir}:${addr === null ? '?' : `0x${addr.toString(16)}`}`;
|
|
178
|
+
|
|
179
|
+
/** Is this access spelled through a `volatile` pointee? A dot member qualifies through the element
|
|
180
|
+
* or member it selects from, any other access through its own base's cast chain. */
|
|
181
|
+
function qualifiedAccessChain(e: Expr): boolean {
|
|
182
|
+
if (e.k === 'index') {
|
|
183
|
+
return qualifiedBase(e.base);
|
|
184
|
+
}
|
|
185
|
+
if (e.k === 'field') {
|
|
186
|
+
return fieldSpellsDot(e)
|
|
187
|
+
? (e.base.k === 'index' || e.base.k === 'field') && qualifiedAccessChain(e.base)
|
|
188
|
+
: qualifiedBase(e.base);
|
|
189
|
+
}
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** The global an access's address is named by: its base through casts, address arithmetic and the
|
|
194
|
+
* element or member a dot member selects from, down to `&gSym`, or to a `gSym` that spells its own
|
|
195
|
+
* address — an array's name, which decays to it (`addresses`), or the struct a dot member selects
|
|
196
|
+
* from (`dotted`). Any other bare `gSym` is the object's VALUE: `*(u16 *)gPtr` reads what the
|
|
197
|
+
* pointer points at, not the pointer, so the access names no object. Null too when the base is
|
|
198
|
+
* anything else — another access, a local. */
|
|
199
|
+
function accessedObject(base: Expr, addresses: ReadonlySet<string>, dotted = false): string | null {
|
|
200
|
+
switch (base.k) {
|
|
201
|
+
case 'var':
|
|
202
|
+
return dotted || addresses.has(base.name) ? base.name : null;
|
|
203
|
+
case 'addr':
|
|
204
|
+
return base.name;
|
|
205
|
+
case 'cast':
|
|
206
|
+
return accessedObject(base.e, addresses);
|
|
207
|
+
case 'bin':
|
|
208
|
+
return base.op === '+' || base.op === '-'
|
|
209
|
+
? (accessedObject(base.l, addresses) ?? accessedObject(base.r, addresses))
|
|
210
|
+
: null;
|
|
211
|
+
default:
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** What the tree side reads a render against: the objects whose every read the asm makes is
|
|
217
|
+
* declared, those whose every write is, and the rendered C type of an expression, which decides
|
|
218
|
+
* where the printer casts. */
|
|
219
|
+
interface TreeContext {
|
|
220
|
+
declared: ReadonlySet<string>;
|
|
221
|
+
written: ReadonlySet<string>;
|
|
222
|
+
typeOf: (e: Expr) => IrType | undefined;
|
|
223
|
+
/** The names whose bare spelling is the object's address: the array-declared globals the
|
|
224
|
+
* structurer subscripts by name (SFn.globals), and the array or struct statics it defines. */
|
|
225
|
+
addresses: ReadonlySet<string>;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** every call under `e`, counted by target name; every qualified memory access, by the address it
|
|
229
|
+
* denotes, or by its object where that object's reads are declared, and not at all where it is a
|
|
230
|
+
* write the object's declaration places; and every other access and bare global read, by the
|
|
231
|
+
* object it names — under `stripped:` (`stripped-write:` for a store target) where the access
|
|
232
|
+
* reaches a declared object through a cast that drops its qualifier. `selected` marks an element or member a dot
|
|
233
|
+
* member selects from: that is a part of the one access the member spells, not an access of its
|
|
234
|
+
* own. `named` marks the base of an access: a `gSym` there is the access's address, counted with
|
|
235
|
+
* it. `target` marks a store's target, which is a write. */
|
|
236
|
+
function effectsInExpr(
|
|
237
|
+
e: Expr,
|
|
238
|
+
into: EffectCounts,
|
|
239
|
+
ctx: TreeContext,
|
|
240
|
+
selected = false,
|
|
241
|
+
named = false,
|
|
242
|
+
target = false,
|
|
243
|
+
): void {
|
|
244
|
+
const bump = (k: string) => into.set(k, (into.get(k) ?? 0) + 1);
|
|
245
|
+
const access = e.k === 'index' || e.k === 'field';
|
|
246
|
+
const inner = access && !selected ? dotSelectedAccess(e) : null;
|
|
247
|
+
const object =
|
|
248
|
+
inner === null ? null : accessedObject(inner.base, ctx.addresses, inner.k === 'field' && fieldSpellsDot(inner));
|
|
249
|
+
const ofDeclared = object !== null && !target && ctx.declared.has(object);
|
|
250
|
+
const writesDeclared = object !== null && target && ctx.written.has(object);
|
|
125
251
|
if (e.k === 'call') {
|
|
126
|
-
|
|
252
|
+
bump(`call:${calleeOf(e) ?? INDIRECT_CALL}`);
|
|
253
|
+
} else if (!selected && qualifiedAccessChain(e)) {
|
|
254
|
+
if (!writesDeclared) {
|
|
255
|
+
bump(ofDeclared ? `${DECLARED}${object}` : deviceKey(target ? 'w' : 'r', cellAddress(e)));
|
|
256
|
+
}
|
|
257
|
+
} else if (!selected && target && access) {
|
|
258
|
+
if (writesDeclared && castOffDeclaration(e, ctx.typeOf)) {
|
|
259
|
+
bump(`${STRIPPED_WRITE}${object}`);
|
|
260
|
+
}
|
|
261
|
+
} else if (!selected && access) {
|
|
262
|
+
if (object !== null) {
|
|
263
|
+
bump(`${ofDeclared && castOffDeclaration(e, ctx.typeOf) ? STRIPPED : DECLARED}${object}`);
|
|
264
|
+
}
|
|
265
|
+
} else if (!target && e.k === 'var' && !(named && (selected || ctx.addresses.has(e.name)))) {
|
|
266
|
+
// A bare name is a read of the object's value unless it spells the address of an access, which
|
|
267
|
+
// counts it: the struct a dot member selects from, or an array's decayed name.
|
|
268
|
+
bump(`${DECLARED}${e.name}`);
|
|
127
269
|
}
|
|
128
|
-
|
|
270
|
+
const partOfThis = e.k === 'field' && fieldSpellsDot(e);
|
|
271
|
+
exprChildren(e).forEach((c) =>
|
|
272
|
+
effectsInExpr(c, into, ctx, partOfThis && c === e.base, access && c === e.base ? true : named && !access, false),
|
|
273
|
+
);
|
|
129
274
|
}
|
|
130
275
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
276
|
+
type Access = Extract<Expr, { k: 'index' } | { k: 'field' }>;
|
|
277
|
+
|
|
278
|
+
/** A dot member's innermost access, past the element or member it selects from: its base is the
|
|
279
|
+
* address of the one access the member spells. */
|
|
280
|
+
function dotSelectedAccess(e: Access): Access {
|
|
281
|
+
if (e.k === 'field' && fieldSpellsDot(e) && (e.base.k === 'index' || e.base.k === 'field')) {
|
|
282
|
+
return dotSelectedAccess(e.base);
|
|
283
|
+
}
|
|
284
|
+
return e;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** The executions on the paths through a statement list, split by how each path LEAVES it: falling off
|
|
288
|
+
* the end, or by a `return`, `break` or `continue`. Null when no path leaves that way. Each is the
|
|
289
|
+
* per-name maximum over the paths of that kind. */
|
|
290
|
+
interface PathCounts {
|
|
291
|
+
through: EffectCounts | null;
|
|
292
|
+
ret: EffectCounts | null;
|
|
293
|
+
brk: EffectCounts | null;
|
|
294
|
+
cont: EffectCounts | null;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const sum = (a: EffectCounts, b: EffectCounts): EffectCounts => combine(a, b, (x, y) => x + y);
|
|
298
|
+
const most = (...xs: (EffectCounts | null)[]): EffectCounts | null =>
|
|
299
|
+
xs.reduce<EffectCounts | null>((acc, x) => (x === null ? acc : acc === null ? x : combine(acc, x, Math.max)), null);
|
|
300
|
+
/** `c` run ahead of every path of `p` */
|
|
301
|
+
const after = (c: EffectCounts, p: PathCounts): PathCounts => ({
|
|
302
|
+
through: p.through && sum(c, p.through),
|
|
303
|
+
ret: p.ret && sum(c, p.ret),
|
|
304
|
+
brk: p.brk && sum(c, p.brk),
|
|
305
|
+
cont: p.cont && sum(c, p.cont),
|
|
306
|
+
});
|
|
307
|
+
/** `a` then `b`: only a path that falls off the end of `a` reaches `b` */
|
|
308
|
+
const seq = (a: PathCounts, b: PathCounts): PathCounts =>
|
|
309
|
+
a.through === null
|
|
310
|
+
? a
|
|
311
|
+
: {
|
|
312
|
+
through: b.through && sum(a.through, b.through),
|
|
313
|
+
ret: most(a.ret, b.ret && sum(a.through, b.ret)),
|
|
314
|
+
brk: most(a.brk, b.brk && sum(a.through, b.brk)),
|
|
315
|
+
cont: most(a.cont, b.cont && sum(a.through, b.cont)),
|
|
316
|
+
};
|
|
317
|
+
/** whichever of the alternatives runs */
|
|
318
|
+
const either = (...ps: PathCounts[]): PathCounts => ({
|
|
319
|
+
through: most(...ps.map((p) => p.through)),
|
|
320
|
+
ret: most(...ps.map((p) => p.ret)),
|
|
321
|
+
brk: most(...ps.map((p) => p.brk)),
|
|
322
|
+
cont: most(...ps.map((p) => p.cont)),
|
|
323
|
+
});
|
|
324
|
+
const falls = (c: EffectCounts): PathCounts => ({ through: c, ret: null, brk: null, cont: null });
|
|
325
|
+
|
|
326
|
+
/** `total` = every occurrence in the tree; `paths` = the most any single syntactic path executes,
|
|
327
|
+
* by how it leaves `stmts`. A path that leaves by `return` runs nothing after it, so two sequenced
|
|
328
|
+
* `if (…) { f(); return; }` arms are one call on any path, not two. `unreached` collects every
|
|
329
|
+
* statement that follows one no path falls past: the path count skips it, so the caller refuses it
|
|
330
|
+
* rather than let a duplicated region hide there. */
|
|
331
|
+
function countEffects(
|
|
332
|
+
stmts: Stmt[],
|
|
333
|
+
ctx: TreeContext,
|
|
334
|
+
unreached: Stmt[] = [],
|
|
335
|
+
): { total: EffectCounts; paths: PathCounts } {
|
|
336
|
+
let total: EffectCounts = new Map();
|
|
337
|
+
let paths = falls(new Map());
|
|
139
338
|
for (const s of stmts) {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
339
|
+
if (paths.through === null) {
|
|
340
|
+
unreached.push(s);
|
|
341
|
+
}
|
|
342
|
+
const own: EffectCounts = new Map();
|
|
343
|
+
stmtExprs(s).forEach((e) => effectsInExpr(e, own, ctx, false, false, s.k === 'store' && e === s.lval));
|
|
344
|
+
total = sum(total, own);
|
|
345
|
+
let here: PathCounts;
|
|
143
346
|
if (s.k === 'if') {
|
|
144
|
-
const t =
|
|
145
|
-
const e =
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
{ total: combine(t.total, e.total, (x, y) => x + y), path: combine(t.path, e.path, Math.max) },
|
|
149
|
-
(x, y) => x + y,
|
|
150
|
-
);
|
|
347
|
+
const t = countEffects(s.then, ctx, unreached);
|
|
348
|
+
const e = countEffects(s.else, ctx, unreached);
|
|
349
|
+
total = sum(total, sum(t.total, e.total));
|
|
350
|
+
here = after(own, either(t.paths, e.paths));
|
|
151
351
|
} else if (s.k === 'switch') {
|
|
152
|
-
const arms = s.cases.map((c) =>
|
|
153
|
-
const dflt =
|
|
352
|
+
const arms = s.cases.map((c) => countEffects(c.body, ctx, unreached));
|
|
353
|
+
const dflt = countEffects(s.default ?? [], ctx, unreached);
|
|
354
|
+
total = arms.reduce((acc, a) => sum(acc, a.total), sum(total, dflt.total));
|
|
154
355
|
// A fall-through arm continues into the NEXT one emitted (the last into `default`), so a
|
|
155
356
|
// path through arm i runs the chain starting at i — the shape the fall-through round's
|
|
156
|
-
// CRITICAL took. Built from the end; `chain[i]` is that arm's
|
|
157
|
-
const chain:
|
|
357
|
+
// CRITICAL took. Built from the end; `chain[i]` is that arm's paths.
|
|
358
|
+
const chain: PathCounts[] = new Array(arms.length);
|
|
158
359
|
for (let i = arms.length - 1; i >= 0; i--) {
|
|
159
|
-
const next = i + 1 < arms.length ? chain[i + 1] : dflt.
|
|
160
|
-
chain[i] = s.cases[i].fallsThrough ?
|
|
360
|
+
const next = i + 1 < arms.length ? chain[i + 1] : dflt.paths;
|
|
361
|
+
chain[i] = s.cases[i].fallsThrough ? seq(arms[i].paths, next) : arms[i].paths;
|
|
161
362
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
363
|
+
// No `default` is one more alternative: no arm runs. A `break` leaves the switch, so it
|
|
364
|
+
// continues after it as a fall-off-the-end does.
|
|
365
|
+
const inner = either(...chain, s.default ? dflt.paths : falls(new Map()));
|
|
366
|
+
here = after(own, { through: most(inner.through, inner.brk), ret: inner.ret, brk: null, cont: inner.cont });
|
|
367
|
+
} else if (stmtChildren(s).length > 0) {
|
|
368
|
+
// A loop: its sequenced children (a body, a `for`'s init/inc) counted ONCE — a loop's dynamic
|
|
369
|
+
// trip count is not a syntactic occurrence, and the IR side is static too. A `break` or
|
|
370
|
+
// `continue` in the body lands after the loop or at its test, and the test may exit without
|
|
371
|
+
// running the body.
|
|
372
|
+
const body = countEffects(stmtChildren(s), ctx, unreached);
|
|
373
|
+
total = sum(total, body.total);
|
|
374
|
+
const p = body.paths;
|
|
375
|
+
here = after(own, { through: most(p.through, p.brk, p.cont, new Map()), ret: p.ret, brk: null, cont: null });
|
|
376
|
+
} else if (s.k === 'return') {
|
|
377
|
+
here = { through: null, ret: own, brk: null, cont: null };
|
|
378
|
+
} else if (s.k === 'break') {
|
|
379
|
+
here = { through: null, ret: null, brk: own, cont: null };
|
|
380
|
+
} else if (s.k === 'continue') {
|
|
381
|
+
here = { through: null, ret: null, brk: null, cont: own };
|
|
165
382
|
} else {
|
|
166
|
-
|
|
167
|
-
// count is not a syntactic occurrence, and the IR side is static too.
|
|
168
|
-
for (const c of stmtChildren(s)) {
|
|
169
|
-
add(countCalls([c]), (x, y) => x + y);
|
|
170
|
-
}
|
|
383
|
+
here = falls(own);
|
|
171
384
|
}
|
|
385
|
+
paths = seq(paths, here);
|
|
172
386
|
}
|
|
173
|
-
return { total,
|
|
387
|
+
return { total, paths };
|
|
174
388
|
}
|
|
175
389
|
|
|
176
390
|
/**
|
|
177
|
-
* Post structuring: every call the asm makes is emitted, and none
|
|
178
|
-
* asm makes it on any one path. See the
|
|
391
|
+
* Post structuring: every call and pinned access the asm makes is emitted, and none of those or of
|
|
392
|
+
* its declared volatile reads is emitted more times than the asm makes it on any one path. See the
|
|
393
|
+
* note above for what this deliberately does not cover.
|
|
179
394
|
*/
|
|
180
395
|
export function assertEffectsPreserved(fn: Fn, sfn: SFn): void {
|
|
181
396
|
// Reachable blocks only: an unreachable block's call is legitimately never emitted.
|
|
182
397
|
const seen = reachableBlocks(fn);
|
|
183
|
-
const irCalls:
|
|
398
|
+
const irCalls: EffectCounts = new Map();
|
|
399
|
+
const irDevices: EffectCounts = new Map();
|
|
400
|
+
// Pinned accesses through a named global: in `irDevices` too, and exempt from the dropped rule.
|
|
401
|
+
const namedDevices = new Map<string, number>();
|
|
402
|
+
// …and the reads among them by the object they name, whose renders may be counted as its reads.
|
|
403
|
+
const pinnedReadsOf: EffectCounts = new Map();
|
|
404
|
+
const irDeclared: EffectCounts = new Map();
|
|
405
|
+
// Objects some unplaced read reads too, whose renders the tree side cannot tell from a declared one.
|
|
406
|
+
const plainlyRead = new Set<string>();
|
|
407
|
+
// Objects a declared store writes, and those some unplaced store writes too.
|
|
408
|
+
const declaredWritten = new Set<string>();
|
|
409
|
+
const plainlyWritten = new Set<string>();
|
|
184
410
|
// Unmodelled instructions, by the mnemonic the frontend stamped. Same "never dropped" property as
|
|
185
411
|
// a call, and it needs its own tally because an `opaque` carries no `target`.
|
|
186
412
|
const irOpaques = new Set<string>();
|
|
413
|
+
const defs = new Map<Value, Op>();
|
|
414
|
+
for (const b of fn.blocks) {
|
|
415
|
+
for (const op of b.ops) {
|
|
416
|
+
op.results.forEach((r) => defs.set(r, op));
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
const bump = (m: EffectCounts, k: string) => m.set(k, (m.get(k) ?? 0) + 1);
|
|
420
|
+
// Of the unplaced qualified accesses, those whose address may be a stack object the frame audit
|
|
421
|
+
// qualified, so a refusal names that object rather than a device register.
|
|
422
|
+
const stackUnplaced: Record<Direction, StackUnplaced> = { r: { may: 0, only: 0 }, w: { may: 0, only: 0 } };
|
|
423
|
+
const reachOf = addressReach(fn, defs);
|
|
187
424
|
for (const b of seen) {
|
|
188
425
|
for (const op of b.ops) {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
426
|
+
const reads = op.opcode === 'load' || op.opcode === 'aload';
|
|
427
|
+
const placement = placedAt(op);
|
|
428
|
+
switch (placement) {
|
|
429
|
+
case 'call':
|
|
430
|
+
bump(irCalls, calleeName(op) ?? INDIRECT_CALL);
|
|
431
|
+
break;
|
|
432
|
+
case 'helper':
|
|
433
|
+
// Placed, not counted: the value it computes is pure.
|
|
434
|
+
break;
|
|
435
|
+
case 'device': {
|
|
436
|
+
const plain = op.opcode === 'load' || op.opcode === 'store';
|
|
437
|
+
const dir = reads ? 'r' : 'w';
|
|
438
|
+
const key = deviceKey(dir, plain ? constAddressOf(defs, op.operands[0], op.attrs.off as number) : null);
|
|
439
|
+
bump(irDevices, key);
|
|
440
|
+
const reach = key === deviceKey(dir, null) ? reachOf(op.operands[0]) : null;
|
|
441
|
+
if (reach?.stack === true) {
|
|
442
|
+
stackUnplaced[dir].may++;
|
|
443
|
+
if (!reach.other) {
|
|
444
|
+
stackUnplaced[dir].only++;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
const object = globalBaseOf(defs, op.operands[0]);
|
|
448
|
+
if (object !== null) {
|
|
449
|
+
bump(namedDevices, key);
|
|
450
|
+
if (dir === 'r') {
|
|
451
|
+
bump(pinnedReadsOf, `${DECLARED}${object}`);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
break;
|
|
455
|
+
}
|
|
456
|
+
case 'declared': {
|
|
457
|
+
const object = globalBaseOf(defs, op.operands[0]);
|
|
458
|
+
if (object !== null && reads) {
|
|
459
|
+
bump(irDeclared, `${DECLARED}${object}`);
|
|
460
|
+
} else if (object !== null) {
|
|
461
|
+
declaredWritten.add(object);
|
|
462
|
+
}
|
|
463
|
+
break;
|
|
464
|
+
}
|
|
465
|
+
case null:
|
|
466
|
+
if (op.opcode === 'opaque') {
|
|
467
|
+
irOpaques.add(gapReasonFor(op.attrs));
|
|
468
|
+
} else if (MEM_BASE_OPS.has(op.opcode)) {
|
|
469
|
+
const object = globalBaseOf(defs, op.operands[0]);
|
|
470
|
+
if (object !== null) {
|
|
471
|
+
(reads ? plainlyRead : plainlyWritten).add(object);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
break;
|
|
475
|
+
default: {
|
|
476
|
+
// A new placement is keyed here before it is counted, or answered as not counted.
|
|
477
|
+
const unkeyed: never = placement;
|
|
478
|
+
throw new ContractError(`the effect contract keys no '${unkeyed as string}' placement`);
|
|
479
|
+
}
|
|
194
480
|
}
|
|
195
481
|
}
|
|
196
482
|
}
|
|
483
|
+
for (const object of plainlyRead) {
|
|
484
|
+
irDeclared.delete(`${DECLARED}${object}`);
|
|
485
|
+
}
|
|
486
|
+
for (const object of plainlyWritten) {
|
|
487
|
+
declaredWritten.delete(object);
|
|
488
|
+
}
|
|
197
489
|
// DROPPED only, not the RE-RUN half: a gap rendered twice is a diagnostic printed twice, which
|
|
198
490
|
// costs nothing because nothing recompiles it, and structuring legitimately duplicates a shared
|
|
199
491
|
// arm — so a per-path count here would fire on correct output.
|
|
@@ -216,21 +508,240 @@ export function assertEffectsPreserved(fn: Fn, sfn: SFn): void {
|
|
|
216
508
|
}
|
|
217
509
|
}
|
|
218
510
|
}
|
|
219
|
-
if (!irCalls.size) {
|
|
511
|
+
if (!irCalls.size && !irDevices.size && !irDeclared.size && !declaredWritten.size) {
|
|
220
512
|
return;
|
|
221
513
|
}
|
|
222
|
-
const
|
|
514
|
+
const unreached: Stmt[] = [];
|
|
515
|
+
const vt = declaredTypes(sfn);
|
|
516
|
+
const ctx: TreeContext = {
|
|
517
|
+
declared: new Set([...irDeclared.keys()].map((k) => k.slice(DECLARED.length))),
|
|
518
|
+
written: declaredWritten,
|
|
519
|
+
typeOf: (e) => exprCType(e, vt),
|
|
520
|
+
addresses: new Set([
|
|
521
|
+
...(sfn.globals ?? []).map((g) => g.name),
|
|
522
|
+
...[...(sfn.statics ?? []), ...(sfn.images ?? [])]
|
|
523
|
+
.filter((st) => st.type.kind === 'array' || st.type.kind === 'struct')
|
|
524
|
+
.map((st) => st.name),
|
|
525
|
+
]),
|
|
526
|
+
};
|
|
527
|
+
const { total, paths } = countEffects(sfn.body, ctx, unreached);
|
|
528
|
+
if (unreached.length) {
|
|
529
|
+
throw new ContractError(
|
|
530
|
+
`structuring emitted ${unreached.length} statement(s) no path reaches in '${sfn.name}', after one every path leaves`,
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
const path = most(paths.through, paths.ret, paths.brk, paths.cont) ?? new Map<string, number>();
|
|
223
534
|
for (const [name, n] of irCalls) {
|
|
224
|
-
if (!(total.get(name) ?? 0)) {
|
|
535
|
+
if (!(total.get(`call:${name}`) ?? 0)) {
|
|
225
536
|
throw new ContractError(`structuring dropped the call to '${name}' in '${sfn.name}' — its effect is lost`);
|
|
226
537
|
}
|
|
227
|
-
const p = path.get(name) ?? 0;
|
|
538
|
+
const p = path.get(`call:${name}`) ?? 0;
|
|
228
539
|
if (p > n) {
|
|
229
540
|
throw new ContractError(
|
|
230
541
|
`structuring emitted ${p} calls to '${name}' on one path in '${sfn.name}', where the asm makes ${n}`,
|
|
231
542
|
);
|
|
232
543
|
}
|
|
233
544
|
}
|
|
545
|
+
if (irDevices.size) {
|
|
546
|
+
devicesPreserved(sfn.name, irDevices, namedDevices, total, path, stackUnplaced);
|
|
547
|
+
}
|
|
548
|
+
for (const object of declaredWritten) {
|
|
549
|
+
if (total.has(`${STRIPPED_WRITE}${object}`)) {
|
|
550
|
+
throw new ContractError(
|
|
551
|
+
`structuring spelled a write to the volatile object '${object}' in '${sfn.name}' through a cast that ` +
|
|
552
|
+
'drops its qualifier',
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
for (const [key, n] of irDeclared) {
|
|
557
|
+
const object = key.slice(DECLARED.length);
|
|
558
|
+
if (total.has(`${STRIPPED}${object}`)) {
|
|
559
|
+
throw new ContractError(
|
|
560
|
+
`structuring spelled a read of the volatile object '${object}' in '${sfn.name}' through a cast that ` +
|
|
561
|
+
'drops its qualifier',
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
const p = path.get(key) ?? 0;
|
|
565
|
+
const made = n + (pinnedReadsOf.get(key) ?? 0);
|
|
566
|
+
if (p > made) {
|
|
567
|
+
throw new ContractError(
|
|
568
|
+
`structuring emitted ${p} reads of the volatile object '${object}' on one path in '${sfn.name}', ` +
|
|
569
|
+
`where the asm makes ${made}`,
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/** One direction of a device count map: the count at each address it reads off, and at `?`. */
|
|
576
|
+
function oneDirection(m: EffectCounts, dir: Direction): { at: Map<string, number>; unplaced: number; all: number } {
|
|
577
|
+
const prefix = `device:${dir}:`;
|
|
578
|
+
const at = new Map<string, number>();
|
|
579
|
+
let unplaced = 0;
|
|
580
|
+
let all = 0;
|
|
581
|
+
for (const [k, v] of m) {
|
|
582
|
+
if (k.startsWith(prefix)) {
|
|
583
|
+
all += v;
|
|
584
|
+
if (k === `${prefix}?`) {
|
|
585
|
+
unplaced = v;
|
|
586
|
+
} else {
|
|
587
|
+
at.set(k.slice(prefix.length), v);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
return { at, unplaced, all };
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/** What an address may be: a stack object, something else, or either. */
|
|
595
|
+
interface AddressReach {
|
|
596
|
+
stack: boolean;
|
|
597
|
+
other: boolean;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/** What each address may be: a stack object (a `laddr`, moved by `add`/`sub`), anything but a named
|
|
601
|
+
* global, or — through a phi whose incoming values differ — either. A fixpoint, so a loop phi
|
|
602
|
+
* stepped from a `laddr` is a stack object only. */
|
|
603
|
+
function addressReach(fn: Fn, defs: ReadonlyMap<Value, Op>): (addr: Value) => AddressReach {
|
|
604
|
+
const incoming = new Map<Value, Value[]>();
|
|
605
|
+
for (const b of fn.blocks) {
|
|
606
|
+
for (const op of b.ops) {
|
|
607
|
+
for (const s of op.successors ?? []) {
|
|
608
|
+
s.args.forEach((arg, i) => {
|
|
609
|
+
const param = s.block.params[i];
|
|
610
|
+
if (param !== undefined) {
|
|
611
|
+
(incoming.get(param) ?? incoming.set(param, []).get(param)!).push(arg);
|
|
612
|
+
}
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
const NONE: AddressReach = { stack: false, other: false };
|
|
618
|
+
const known = new Map<Value, AddressReach>();
|
|
619
|
+
const at = (v: Value) => known.get(v) ?? NONE;
|
|
620
|
+
// A value not yet reached stays NONE, so an `add` waiting on one does not call itself `other`.
|
|
621
|
+
const step = (v: Value): AddressReach => {
|
|
622
|
+
const d = defs.get(v);
|
|
623
|
+
if (d === undefined) {
|
|
624
|
+
const from = incoming.get(v) ?? [];
|
|
625
|
+
return from.length === 0
|
|
626
|
+
? { stack: false, other: true }
|
|
627
|
+
: { stack: from.some((a) => at(a).stack), other: from.some((a) => at(a).other) };
|
|
628
|
+
}
|
|
629
|
+
if (d.opcode === 'laddr') {
|
|
630
|
+
return { stack: true, other: false };
|
|
631
|
+
}
|
|
632
|
+
// An access through a named global is exempt from the dropped rule, so a phi side that names
|
|
633
|
+
// one is not what a refusal names.
|
|
634
|
+
if (d.opcode === 'gaddr') {
|
|
635
|
+
return NONE;
|
|
636
|
+
}
|
|
637
|
+
if (d.opcode !== 'add' && d.opcode !== 'sub') {
|
|
638
|
+
return { stack: false, other: true };
|
|
639
|
+
}
|
|
640
|
+
const roots = (d.opcode === 'add' ? d.operands : d.operands.slice(0, 1)).map(at);
|
|
641
|
+
const bases = roots.filter((r) => r.stack);
|
|
642
|
+
if (bases.length > 0) {
|
|
643
|
+
return { stack: true, other: bases.some((r) => r.other) };
|
|
644
|
+
}
|
|
645
|
+
return roots.some((r) => !r.other) ? NONE : { stack: false, other: true };
|
|
646
|
+
};
|
|
647
|
+
const values = [...defs.keys(), ...incoming.keys()];
|
|
648
|
+
for (let changed = true; changed;) {
|
|
649
|
+
changed = false;
|
|
650
|
+
for (const v of values) {
|
|
651
|
+
const was = at(v);
|
|
652
|
+
const now = step(v);
|
|
653
|
+
const joined = { stack: was.stack || now.stack, other: was.other || now.other };
|
|
654
|
+
if (joined.stack !== was.stack || joined.other !== was.other) {
|
|
655
|
+
known.set(v, joined);
|
|
656
|
+
changed = true;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
return (addr) => (known.has(addr) ? at(addr) : { stack: false, other: true });
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/** How many unplaced qualified accesses may be a stack object, and how many can be nothing else. */
|
|
664
|
+
interface StackUnplaced {
|
|
665
|
+
may: number;
|
|
666
|
+
only: number;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/** What an unplaced qualified access may be, given how many of `unplaced` may or must be a stack
|
|
670
|
+
* object. */
|
|
671
|
+
function unplacedNoun(stack: StackUnplaced, unplaced: number): { one: string; many: string } {
|
|
672
|
+
if (stack.may === 0) {
|
|
673
|
+
return { one: 'a device register', many: 'device registers' };
|
|
674
|
+
}
|
|
675
|
+
if (stack.only >= unplaced) {
|
|
676
|
+
return { one: 'an address-taken stack local', many: 'address-taken stack locals' };
|
|
677
|
+
}
|
|
678
|
+
return { one: 'a device register or a stack local', many: 'device registers or a stack local' };
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
const NOUNS: Readonly<Record<Direction, { one: string; many: string }>> = {
|
|
682
|
+
r: { one: 'read of', many: 'reads of' },
|
|
683
|
+
w: { one: 'write to', many: 'writes to' },
|
|
684
|
+
};
|
|
685
|
+
|
|
686
|
+
/** The pinned-access half of {@link assertEffectsPreserved}, over the counts keyed by `deviceKey`:
|
|
687
|
+
* first that no access is dropped, then that none is re-run, each direction on its own. */
|
|
688
|
+
function devicesPreserved(
|
|
689
|
+
name: string,
|
|
690
|
+
ir: EffectCounts,
|
|
691
|
+
named: EffectCounts,
|
|
692
|
+
total: EffectCounts,
|
|
693
|
+
path: EffectCounts,
|
|
694
|
+
stackUnplaced: Readonly<Record<Direction, StackUnplaced>>,
|
|
695
|
+
): void {
|
|
696
|
+
const directions = ['r', 'w'] as const;
|
|
697
|
+
for (const dir of directions) {
|
|
698
|
+
const made = oneDirection(ir, dir);
|
|
699
|
+
const exempt = oneDirection(named, dir);
|
|
700
|
+
const renders = oneDirection(total, dir);
|
|
701
|
+
// An access through a named global needs no render of its own.
|
|
702
|
+
const owed = (a: string) => Math.max(0, (made.at.get(a) ?? 0) - (exempt.at.get(a) ?? 0));
|
|
703
|
+
const short = [...made.at.keys()].filter((a) => owed(a) > (renders.at.get(a) ?? 0));
|
|
704
|
+
const deficit = short.reduce((n, a) => n + owed(a) - (renders.at.get(a) ?? 0), 0);
|
|
705
|
+
if (deficit > renders.unplaced) {
|
|
706
|
+
throw new ContractError(
|
|
707
|
+
short.length === 1
|
|
708
|
+
? `structuring dropped the ${NOUNS[dir].one} the device register at ${short[0]} in '${name}' — ` +
|
|
709
|
+
'the machine made it and the source does not'
|
|
710
|
+
: `structuring dropped ${NOUNS[dir].many} the device registers at ${short.join(', ')} in '${name}' — ` +
|
|
711
|
+
'the machine made them and the source does not',
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
const owedAll = [...made.at.keys()].reduce((n, a) => n + owed(a), 0) + Math.max(0, made.unplaced - exempt.unplaced);
|
|
715
|
+
if (owedAll > renders.all) {
|
|
716
|
+
throw new ContractError(
|
|
717
|
+
`structuring dropped a ${NOUNS[dir].one} ${unplacedNoun(stackUnplaced[dir], made.unplaced).one} in ` +
|
|
718
|
+
`'${name}' — the machine made it and the source does not`,
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
for (const dir of directions) {
|
|
723
|
+
const made = oneDirection(ir, dir);
|
|
724
|
+
const onPath = oneDirection(path, dir);
|
|
725
|
+
let excess = 0;
|
|
726
|
+
for (const [a, p] of onPath.at) {
|
|
727
|
+
const n = made.at.get(a) ?? 0;
|
|
728
|
+
if (p > n + made.unplaced) {
|
|
729
|
+
throw new ContractError(
|
|
730
|
+
`structuring emitted ${p} ${NOUNS[dir].many} the device register at ${a} on one path in '${name}', ` +
|
|
731
|
+
`where the asm makes ${n + made.unplaced}`,
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
excess += Math.max(0, p - n);
|
|
735
|
+
}
|
|
736
|
+
if (excess > made.unplaced || onPath.all > made.all) {
|
|
737
|
+
// past the unplaced count, the excess is at the addresses, every one a device register
|
|
738
|
+
const what = excess > made.unplaced ? 'device registers' : unplacedNoun(stackUnplaced[dir], made.unplaced).many;
|
|
739
|
+
throw new ContractError(
|
|
740
|
+
`structuring emitted ${onPath.all} ${NOUNS[dir].many} ${what} on one path in '${name}', where the ` +
|
|
741
|
+
`asm makes ${made.all}`,
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
234
745
|
}
|
|
235
746
|
|
|
236
747
|
/** Post structuring: a local the body READS must be WRITTEN somewhere in it. A materialized value
|
|
@@ -259,12 +770,24 @@ export function assertLocalsWritten(sfn: SFn): void {
|
|
|
259
770
|
if ((e.k === 'var' || e.k === 'addr') && suspect.has(e.name)) {
|
|
260
771
|
(e.k === 'addr' ? written : read).add(e.name);
|
|
261
772
|
}
|
|
773
|
+
// `v++` is a read and a write, so it answers both sides.
|
|
774
|
+
if (e.k === 'postincr' && suspect.has(e.name)) {
|
|
775
|
+
read.add(e.name);
|
|
776
|
+
written.add(e.name);
|
|
777
|
+
}
|
|
262
778
|
exprChildren(e).forEach(walkExpr);
|
|
263
779
|
};
|
|
780
|
+
// A store to a member of a local struct (`ret.a = v`) writes that local.
|
|
781
|
+
const memberOf = (e: Expr): string | undefined =>
|
|
782
|
+
e.k === 'field' && e.dot === true ? (e.base.k === 'var' ? e.base.name : memberOf(e.base)) : undefined;
|
|
264
783
|
const walkStmt = (st: Stmt): void => {
|
|
265
784
|
if (st.k === 'assign' && suspect.has(st.name)) {
|
|
266
785
|
written.add(st.name);
|
|
267
786
|
}
|
|
787
|
+
const member = st.k === 'store' ? memberOf(st.lval) : undefined;
|
|
788
|
+
if (member !== undefined && suspect.has(member)) {
|
|
789
|
+
written.add(member);
|
|
790
|
+
}
|
|
268
791
|
stmtExprs(st).forEach(walkExpr);
|
|
269
792
|
stmtChildren(st).forEach(walkStmt);
|
|
270
793
|
};
|
|
@@ -278,6 +801,46 @@ export function assertLocalsWritten(sfn: SFn): void {
|
|
|
278
801
|
}
|
|
279
802
|
}
|
|
280
803
|
|
|
804
|
+
/** Post structuring, and after every respell variation: a `postincr` is the ONLY mention of its
|
|
805
|
+
* local inside the full expression holding it.
|
|
806
|
+
*
|
|
807
|
+
* C89 leaves a program undefined when an object is modified and also read elsewhere between two
|
|
808
|
+
* sequence points, so `v1++ <= 9 && v1 != 0` is not a spelling asmlift may emit — the compiler
|
|
809
|
+
* picks either value, and the candidate still compiles and scores. The producer establishes the
|
|
810
|
+
* property on the tree it builds (structure.ts's `emitDoWhile`, judged by `PREUPDATE_COND_GATES`);
|
|
811
|
+
* what this catches is a later pass bringing a SECOND mention into that expression — `l3/coalesce.ts`
|
|
812
|
+
* renames one local onto another over a whole body, and asks nothing about this.
|
|
813
|
+
*
|
|
814
|
+
* PER FULL EXPRESSION, not per function: a statement boundary is a sequence point, so the loop
|
|
815
|
+
* body reading the same name is well-defined, and a pass that duplicates a whole expression owes
|
|
816
|
+
* the evaluation-trace argument to `exprHasEffect` rather than to this contract. */
|
|
817
|
+
export function assertPostIncrUnshared(sfn: SFn): void {
|
|
818
|
+
const overUpdated = (e: Expr): string | undefined => {
|
|
819
|
+
const mentions = new Map<string, number>();
|
|
820
|
+
const updated = new Set<string>();
|
|
821
|
+
const walk = (x: Expr): void => {
|
|
822
|
+
if (x.k === 'var' || x.k === 'addr' || x.k === 'postincr') {
|
|
823
|
+
mentions.set(x.name, (mentions.get(x.name) ?? 0) + 1);
|
|
824
|
+
}
|
|
825
|
+
if (x.k === 'postincr') {
|
|
826
|
+
updated.add(x.name);
|
|
827
|
+
}
|
|
828
|
+
exprChildren(x).forEach(walk);
|
|
829
|
+
};
|
|
830
|
+
walk(e);
|
|
831
|
+
return [...updated].find((n) => mentions.get(n)! > 1);
|
|
832
|
+
};
|
|
833
|
+
const bad = (s: Stmt): string | undefined =>
|
|
834
|
+
stmtExprs(s).map(overUpdated).find(Boolean) ?? stmtChildren(s).map(bad).find(Boolean);
|
|
835
|
+
const name = sfn.body.map(bad).find(Boolean);
|
|
836
|
+
if (name !== undefined) {
|
|
837
|
+
throw new ContractError(
|
|
838
|
+
`'${name}' is updated by a post-increment and read again in the same expression in '${sfn.name}' — ` +
|
|
839
|
+
`C89 leaves which value the other read sees undefined`,
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
281
844
|
/** After a respell variation: a local a pass DELETED from the declaration list is named nowhere in the tree it
|
|
282
845
|
* produced.
|
|
283
846
|
*
|
|
@@ -326,6 +889,131 @@ export function assertNoOrphanedLocals(before: SFn, after: SFn): void {
|
|
|
326
889
|
}
|
|
327
890
|
}
|
|
328
891
|
|
|
892
|
+
/** The types a tree reads or writes LOCAL STORAGE at, other than an integer — the frame bytes
|
|
893
|
+
* reached by address (l3/storage.ts `localAddresses`), each access typed as it is spelled
|
|
894
|
+
* (`exprCType`: the cast on the storage, or the element struct's field). An aggregate is the
|
|
895
|
+
* element a `.field` selects from, not an access, so only scalars count. */
|
|
896
|
+
function localStorageNonIntegers(sfn: SFn): Set<string> {
|
|
897
|
+
const { isAddress, accessAddress } = localAddresses(sfn);
|
|
898
|
+
const vt = declaredTypes(sfn);
|
|
899
|
+
const out = new Set<string>();
|
|
900
|
+
for (const e of walkExprs(sfn.body)) {
|
|
901
|
+
if ((e.k === 'index' || e.k === 'field') && isAddress(accessAddress(e))) {
|
|
902
|
+
const t = exprCType(e, vt);
|
|
903
|
+
if (t !== undefined && t.kind !== 'int' && t.kind !== 'struct' && t.kind !== 'array') {
|
|
904
|
+
out.add(typeToString(t));
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
return out;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
/** After a respell variation: no access to LOCAL STORAGE is re-typed to a non-integer.
|
|
912
|
+
*
|
|
913
|
+
* The frame audit's licence for a runtime index wider than a byte (frontend/frame-objects.ts
|
|
914
|
+
* `indexedWidth`) rests on what L3 SPELLS: agbcc at -O2 keeps a read ordered after a store to the
|
|
915
|
+
* same bytes only while both are integers of one width, and gives a pointer (or `long`, or a
|
|
916
|
+
* float) an alias set of its own, so a variation that re-types the bytes' reads hands the
|
|
917
|
+
* compiler a stale read the asm never had — C that compiles, scores, and is wrong. `/ptr-field`'s
|
|
918
|
+
* `over-local-storage` gate is the refusal that names itself; this is the backstop for the next
|
|
919
|
+
* variation that re-types, failing at its own stage rather than three away.
|
|
920
|
+
*
|
|
921
|
+
* A DIFFERENTIAL, like `assertNoOrphanedLocals`: it judges only a non-integer type the tree it
|
|
922
|
+
* derives from did not already read the storage at. The default tree is the frame audit's
|
|
923
|
+
* answer, and a type it gives a local object (a header-typed out-parameter's pointer member, say)
|
|
924
|
+
* is the source's own spelling, not a variation's claim. */
|
|
925
|
+
export function assertLocalStorageStaysInteger(before: SFn, after: SFn): void {
|
|
926
|
+
const had = localStorageNonIntegers(before);
|
|
927
|
+
const added = [...localStorageNonIntegers(after)].filter((t) => !had.has(t));
|
|
928
|
+
if (added.length) {
|
|
929
|
+
throw new ContractError(
|
|
930
|
+
`a respell variation re-typed an access to local storage in '${after.name}' as ${added
|
|
931
|
+
.sort()
|
|
932
|
+
.map((t) => `'${t}'`)
|
|
933
|
+
.join(', ')} — not an integer, so agbcc may read it stale past an integer store to the same bytes`,
|
|
934
|
+
);
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
/** A FRAME UNION IS READ AND WRITTEN ONLY THROUGH ITS VIEWS. The frame audit declares storage
|
|
939
|
+
* accessed at two widths over one byte as a union of per-width views (frontend/frame-objects.ts
|
|
940
|
+
* `unionViews`, structure.ts `laddrType`), because agbcc gives an access through a union member
|
|
941
|
+
* alias set 0 and reorders nothing past it — where a cast on the same bytes is read stale past a
|
|
942
|
+
* store of the other width. That licence holds only while EVERY access is a member access:
|
|
943
|
+
* - the union's name appears only as the base of a view (`u.half`), and an element of an array
|
|
944
|
+
* view is subscripted, element 0 included — `*u.half` dereferences the view's decay, an
|
|
945
|
+
* INDIRECT_REF agbcc types by its pointee and reads stale exactly as the cast (compiled);
|
|
946
|
+
* - nothing is accessed through its ADDRESS: `&u`, under casts and `+`/`-`, or a local ever
|
|
947
|
+
* assigned such a value, as the base of a subscript, an arrow or a deref is the cast
|
|
948
|
+
* spelling again. The address itself may go anywhere a value goes — a call's argument, a
|
|
949
|
+
* device's source register — since handing it on accesses nothing.
|
|
950
|
+
* ABSOLUTE, not a differential: the default tree satisfies it by construction, so any tree that
|
|
951
|
+
* does not was made by a variation. Checked on the default tree (pipeline.ts) and on every
|
|
952
|
+
* respelled one (rank.ts, beside `assertLocalStorageStaysInteger`). */
|
|
953
|
+
export function assertFrameUnionsViewed(sfn: SFn): void {
|
|
954
|
+
const unions = new Map(sfn.locals.filter((l) => l.type.kind === 'union').map((l) => [l.name, l.type]));
|
|
955
|
+
if (unions.size === 0) {
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
const bad = (why: string): never => {
|
|
959
|
+
throw new ContractError(`'${sfn.name}' ${why} — agbcc may read the frame union stale through it`);
|
|
960
|
+
};
|
|
961
|
+
const reaches = (e: Expr, held: ReadonlySet<string>): boolean => {
|
|
962
|
+
switch (e.k) {
|
|
963
|
+
case 'addr':
|
|
964
|
+
return unions.has(e.name);
|
|
965
|
+
case 'var':
|
|
966
|
+
return held.has(e.name);
|
|
967
|
+
case 'cast':
|
|
968
|
+
return reaches(e.e, held);
|
|
969
|
+
case 'bin':
|
|
970
|
+
return (e.op === '+' || e.op === '-') && (reaches(e.l, held) || reaches(e.r, held));
|
|
971
|
+
default:
|
|
972
|
+
return false;
|
|
973
|
+
}
|
|
974
|
+
};
|
|
975
|
+
const held = taintOverAssigns(assignsIn(sfn.body), [], (a, t) => reaches(a.value, t));
|
|
976
|
+
const isView = (e: Expr): boolean =>
|
|
977
|
+
e.k === 'field' && e.dot === true && e.base.k === 'var' && unions.has(e.base.name);
|
|
978
|
+
const visit = (e: Expr): void => {
|
|
979
|
+
if (e.k === 'field' && isView(e)) {
|
|
980
|
+
if (memberOf(unions.get((e.base as { name: string }).name), e.name) === undefined) {
|
|
981
|
+
bad(`names '${e.name}', which is no view of the frame union`);
|
|
982
|
+
}
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
if (e.k === 'index' && isView(e.base)) {
|
|
986
|
+
if (e.idx.k === 'const' && e.idx.value === 0 && e.subscript !== true) {
|
|
987
|
+
bad('dereferences a view of a frame union (`*u.view`) instead of subscripting it');
|
|
988
|
+
}
|
|
989
|
+
visit(e.base);
|
|
990
|
+
visit(e.idx);
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
if ((e.k === 'index' || (e.k === 'field' && e.dot !== true)) && reaches(e.base, held)) {
|
|
994
|
+
bad('accesses a frame union through a pointer to its bytes');
|
|
995
|
+
}
|
|
996
|
+
if (e.k === 'var' && unions.has(e.name)) {
|
|
997
|
+
bad(`uses the frame union '${e.name}' other than through a view`);
|
|
998
|
+
}
|
|
999
|
+
exprChildren(e).forEach(visit);
|
|
1000
|
+
};
|
|
1001
|
+
const walk = (list: readonly Stmt[]): void => {
|
|
1002
|
+
for (const st of list) {
|
|
1003
|
+
if (st.k === 'assign' && unions.has(st.name)) {
|
|
1004
|
+
bad(`assigns the frame union '${st.name}' whole`);
|
|
1005
|
+
}
|
|
1006
|
+
stmtExprs(st).forEach(visit);
|
|
1007
|
+
// a `for`'s init and inc are statements of their own, outside both lists
|
|
1008
|
+
stmtLists(st).forEach(walk);
|
|
1009
|
+
if (st.k === 'for') {
|
|
1010
|
+
walk([st.init, st.inc]);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
};
|
|
1014
|
+
walk(sfn.body);
|
|
1015
|
+
}
|
|
1016
|
+
|
|
329
1017
|
/** After a respell variation: every read of a MINTED local — its ADDRESS being taken included — must sit where
|
|
330
1018
|
* that local's assignment has already run.
|
|
331
1019
|
*
|
|
@@ -358,9 +1046,10 @@ export function assertHoistsDominate(sfn: SFn, minted: ReadonlySet<string>): voi
|
|
|
358
1046
|
const readUndominated = (e: Expr, live: ReadonlySet<string>): string | null => {
|
|
359
1047
|
// `&p` COUNTS, the same mention the placing passes query on (l3/hoist.ts): the address is what
|
|
360
1048
|
// a callee reads the cell through, so an init has to precede it as surely as it must precede a
|
|
361
|
-
// read.
|
|
362
|
-
|
|
363
|
-
|
|
1049
|
+
// read. `mentionedName` is the vocabulary rather than a pair of arms here for that reason.
|
|
1050
|
+
const n = mentionedName(e);
|
|
1051
|
+
if (n !== undefined && minted.has(n) && !live.has(n)) {
|
|
1052
|
+
return n;
|
|
364
1053
|
}
|
|
365
1054
|
let bad: string | null = null;
|
|
366
1055
|
mapExprChildren(e, (c) => {
|
|
@@ -460,9 +1149,12 @@ export function assertDerefsTyped(sfn: SFn): void {
|
|
|
460
1149
|
const NO_PTR_OPS = new Set<BinOp>(['&', '|', '^', '<<', '>>', '>>>', '*', '/', '/u', '%', '%u']);
|
|
461
1150
|
// The comparison operators — where a bare `&SYM` operand is SIGN-ambiguous, not ill-formed.
|
|
462
1151
|
const CMP_OPS = new Set(['<', '<=', '>', '>=', '==', '!=']);
|
|
463
|
-
// 1/2/4 only
|
|
464
|
-
//
|
|
465
|
-
//
|
|
1152
|
+
// 1/2/4 only. `s64` IS in the typedef vocabulary now (target.ts C_TYPEDEFS), and the 8-byte
|
|
1153
|
+
// ACCESS still is not: nothing in this pipeline recognises a pair of word loads as one 64-bit
|
|
1154
|
+
// read, so a width-8 index here would be a claim no pass makes. The rule pre-empts the
|
|
1155
|
+
// three-stages-later failure either way. (If f64 loads ever land they are floats, not a scalar
|
|
1156
|
+
// width here.) Adding 8 would also silently under-report in `l3/typing.ts` `exprIntWidth`, whose
|
|
1157
|
+
// soundness rests on memory not being a way a 64-bit value enters a rendered expression.
|
|
466
1158
|
const SCALAR_WIDTHS = new Set([1, 2, 4]);
|
|
467
1159
|
// Dot-form field bases (struct-array elements) carry the struct STRIDE as their width — any
|
|
468
1160
|
// stride matching the element size is legal there (the tree-level struct cast governs the
|
|
@@ -477,8 +1169,9 @@ export function assertDerefsTyped(sfn: SFn): void {
|
|
|
477
1169
|
if (e.k === 'index' && !structElem.has(e) && !SCALAR_WIDTHS.has(e.width)) {
|
|
478
1170
|
bad.push(`index width ${e.width} is not a C scalar width`);
|
|
479
1171
|
}
|
|
480
|
-
// The emitter legalizes pointer operands away from these ops (structure.ts
|
|
481
|
-
// pointer surviving here is ill-typed C the compiler will
|
|
1172
|
+
// The emitter legalizes pointer operands away from these ops (structure/pointer-spelling.ts
|
|
1173
|
+
// `ARITH_ROWS`, its `bitwise` row); a pointer surviving here is ill-typed C the compiler will
|
|
1174
|
+
// reject.
|
|
482
1175
|
if (e.k === 'bin' && NO_PTR_OPS.has(e.op)) {
|
|
483
1176
|
for (const side of [e.l, e.r]) {
|
|
484
1177
|
if (ctype(side)?.kind === 'ptr') {
|
|
@@ -490,9 +1183,9 @@ export function assertDerefsTyped(sfn: SFn): void {
|
|
|
490
1183
|
// offset by sizeof(SYM), which is unknown for a header-typed global, so `&SYM + N` is byte-
|
|
491
1184
|
// inexact. Nothing emits this shape anymore: a load/store base folds byte-correctly (globalOf
|
|
492
1185
|
// turns `&SYM + N` into an `index`/`field` node whose base is a bare `addr`), and the additive
|
|
493
|
-
// lowering intifies every other `addr` operand to `(u32)&SYM` (structure.ts
|
|
494
|
-
// cast types int, so it never lands here). A bare `addr` reaching a `+`/`-`
|
|
495
|
-
// a lowering REGRESSION — flag it rather than emit wrong bytes.
|
|
1186
|
+
// lowering intifies every other `addr` operand to `(u32)&SYM` (structure/pointer-spelling.ts
|
|
1187
|
+
// ARITH_ROWS — the cast types int, so it never lands here). A bare `addr` reaching a `+`/`-`
|
|
1188
|
+
// operand is therefore a lowering REGRESSION — flag it rather than emit wrong bytes.
|
|
496
1189
|
if (e.k === 'bin' && (e.op === '+' || e.op === '-')) {
|
|
497
1190
|
const addrSide = e.l.k === 'addr' ? e.l : e.r.k === 'addr' ? e.r : undefined;
|
|
498
1191
|
if (addrSide) {
|
|
@@ -526,10 +1219,10 @@ export function assertDerefsTyped(sfn: SFn): void {
|
|
|
526
1219
|
if (bt) {
|
|
527
1220
|
// type-check against the same dot-vs-arrow spelling the printer will use (shared rule)
|
|
528
1221
|
const st = fieldSpellsDot(e) ? bt : bt.kind === 'ptr' ? bt.to : undefined;
|
|
529
|
-
if (!st || st.kind !== 'struct') {
|
|
1222
|
+
if (!st || (st.kind !== 'struct' && st.kind !== 'union')) {
|
|
530
1223
|
bad.push(`member access '${e.name}' on a non-struct base (C type '${typeToString(bt)}')`);
|
|
531
|
-
} else if (
|
|
532
|
-
bad.push(`member access '${e.name}' not declared on '${st
|
|
1224
|
+
} else if (memberOf(st, e.name) === undefined) {
|
|
1225
|
+
bad.push(`member access '${e.name}' not declared on '${typeToString(st)}'`);
|
|
533
1226
|
}
|
|
534
1227
|
}
|
|
535
1228
|
}
|