@asmlift/core 0.8.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -30
- package/package.json +1 -1
- package/src/aggregate.ts +248 -0
- package/src/backend/c.ts +5 -2
- package/src/backend/cfamily.ts +236 -37
- package/src/backend/cpp.ts +114 -9
- package/src/backend/pascal.ts +39 -0
- 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 +615 -79
- package/src/declare.ts +75 -5
- package/src/frontend/asmdata.ts +19 -5
- package/src/frontend/call-plan.ts +686 -0
- package/src/frontend/device-pins.ts +277 -0
- package/src/frontend/errors.ts +5 -0
- package/src/frontend/flags-edge.ts +101 -0
- package/src/frontend/fpu.ts +167 -0
- package/src/frontend/frame-objects.ts +2131 -0
- package/src/frontend/high-half.ts +2 -1
- package/src/frontend/local-object.ts +490 -0
- package/src/frontend/mips.ts +363 -40
- package/src/frontend/opaque.ts +97 -18
- package/src/frontend/ppc.ts +520 -129
- package/src/frontend/reloc-symbol.ts +94 -13
- package/src/frontend/splat.ts +108 -5
- package/src/frontend/ssa.ts +669 -76
- package/src/frontend/stackargs.ts +120 -39
- package/src/frontend/thumb.ts +2764 -2018
- package/src/ir/alias.ts +29 -0
- package/src/ir/core.ts +101 -3
- package/src/ir/discipline.ts +248 -0
- package/src/ir/float-bits.ts +47 -0
- package/src/ir/opcodes.ts +84 -112
- package/src/ir/parse.ts +1 -0
- package/src/ir/types.ts +118 -2
- package/src/ir/verify.ts +150 -3
- package/src/l3/address.ts +5 -0
- package/src/l3/argcopy.ts +38 -6
- package/src/l3/ast.ts +186 -14
- package/src/l3/coalesce.ts +21 -13
- package/src/l3/dce.ts +32 -10
- package/src/l3/gates.ts +9 -3
- package/src/l3/hoist.ts +8 -7
- package/src/l3/initfirst.ts +27 -9
- package/src/l3/intcell.ts +97 -0
- package/src/l3/mentions.ts +10 -4
- package/src/l3/narrowdecl.ts +218 -0
- package/src/l3/nearbase.ts +10 -5
- package/src/l3/parkfirst.ts +8 -5
- package/src/l3/pollguard.ts +3 -2
- package/src/l3/ptrcell.ts +357 -0
- package/src/l3/regspell.ts +17 -3
- package/src/l3/reindex.ts +10 -7
- package/src/l3/symbol-refs.ts +104 -23
- package/src/l3/tailmerge.ts +6 -19
- package/src/l3/typing.ts +137 -25
- package/src/l3/unreduce.ts +4 -4
- package/src/l3/volstore.ts +96 -7
- package/src/mangle.ts +11 -1
- package/src/pattern/engine.ts +19 -16
- package/src/pipeline.ts +57 -12
- package/src/proto-context.ts +788 -0
- package/src/proto.ts +689 -50
- package/src/raise/arrays.ts +4 -3
- package/src/raise/declared-volatile.ts +131 -0
- package/src/raise/divpow2.ts +2 -2
- package/src/raise/errors.ts +17 -0
- package/src/raise/floathelpers.ts +243 -0
- package/src/raise/globalshape.ts +11 -5
- package/src/raise/gvn.ts +26 -5
- package/src/raise/narrow.ts +29 -2
- package/src/raise/narrowlocal.ts +11 -6
- package/src/raise/offsetnames.ts +510 -0
- package/src/raise/pairparams.ts +81 -0
- package/src/raise/paramwidth.ts +7 -0
- package/src/raise/pre-recovery.ts +78 -4
- package/src/raise/recover.ts +14 -10
- package/src/raise/retsink.ts +1 -1
- package/src/raise/shortcircuit.ts +41 -19
- package/src/raise/softdiv.ts +21 -23
- package/src/raise/struct-arrays.ts +5 -4
- package/src/raise/structs.ts +311 -98
- package/src/raise/truncload.ts +366 -0
- package/src/raise/widehelpers.ts +106 -0
- package/src/rank-declare.ts +30 -5
- package/src/rank-variations.ts +36 -8
- package/src/rank.ts +267 -79
- package/src/runtime-helpers.ts +287 -0
- package/src/stillborn.ts +289 -0
- package/src/structure/analysis.ts +1285 -448
- package/src/structure/hazards.ts +633 -58
- package/src/structure/local-statics.ts +381 -0
- package/src/structure/namecoalesce.ts +1 -1
- package/src/structure/redundant-test.ts +99 -0
- package/src/structure/structure.ts +1772 -395
- package/src/structure/switch-recover.ts +159 -78
- package/src/symbols.ts +38 -3
- package/src/target.ts +662 -59
- package/src/trace.ts +44 -24
- package/src/variation-definitions.ts +119 -1
- package/src/variation-tokens.ts +20 -6
package/src/contracts.ts
CHANGED
|
@@ -3,14 +3,19 @@
|
|
|
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 {
|
|
10
14
|
exprChildren,
|
|
11
15
|
fieldSpellsDot,
|
|
12
16
|
gapReasonFor,
|
|
13
17
|
mapExprChildren,
|
|
18
|
+
mentionedName,
|
|
14
19
|
stmtChildren,
|
|
15
20
|
stmtExprs,
|
|
16
21
|
stmtLists,
|
|
@@ -18,6 +23,7 @@ import {
|
|
|
18
23
|
} from './l3/ast';
|
|
19
24
|
import { mentionedLocals } from './l3/mentions';
|
|
20
25
|
import { declaredTypes, exprCType } from './l3/typing';
|
|
26
|
+
import { castOffDeclaration } from './l3/volstore';
|
|
21
27
|
|
|
22
28
|
export class ContractError extends Error {
|
|
23
29
|
constructor(message: string) {
|
|
@@ -68,9 +74,14 @@ export function assertResolved(sfn: SFn): void {
|
|
|
68
74
|
// Every Expr kind that CARRIES a name, not just `var` — each is read through the same
|
|
69
75
|
// `map.get(d)!` / `attrs.x as string` and prints straight into the source. `carriesName` is asked
|
|
70
76
|
// separately because an ABSENT name is the case being caught: keying off `nameOf` alone refuses nothing.
|
|
71
|
-
const carriesName = (e: Expr): boolean =>
|
|
77
|
+
const carriesName = (e: Expr): boolean =>
|
|
78
|
+
e.k === 'var' ||
|
|
79
|
+
e.k === 'addr' ||
|
|
80
|
+
e.k === 'field' ||
|
|
81
|
+
(e.k === 'call' && typeof e.fn === 'string') ||
|
|
82
|
+
e.k === 'postincr';
|
|
72
83
|
const nameOf = (e: Expr): string | undefined =>
|
|
73
|
-
e.k === 'call' ? e
|
|
84
|
+
e.k === 'call' ? calleeOf(e) : e.k === 'marker' ? undefined : (e as { name?: string }).name;
|
|
74
85
|
const badExpr = (e: Expr): boolean => (carriesName(e) && badName(nameOf(e))) || exprChildren(e).some(badExpr);
|
|
75
86
|
// An `assign`'s DESTINATION is a bare string field, so the expression walk never reaches it.
|
|
76
87
|
const badStmt = (s: Stmt): boolean =>
|
|
@@ -86,10 +97,10 @@ export function assertResolved(sfn: SFn): void {
|
|
|
86
97
|
// ── effects: executed once, never dropped ──────────────────────────────────────────────────
|
|
87
98
|
//
|
|
88
99
|
// 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:
|
|
100
|
+
// property the structurer's materialization model exists to preserve: an execution the asm makes
|
|
101
|
+
// must happen exactly as often in the emitted source. Its two failure modes are the two that hurt
|
|
102
|
+
// most — asmlift's first rule is that a loud failure beats a silently wrong answer, and both of
|
|
103
|
+
// these are silent:
|
|
93
104
|
//
|
|
94
105
|
// • DROPPED — a call the asm makes has no counterpart in the tree at all;
|
|
95
106
|
// • RE-RUN — inlining a call's value at more than one render position (or a structuring copy
|
|
@@ -97,22 +108,55 @@ export function assertResolved(sfn: SFn): void {
|
|
|
97
108
|
// recovered switch fall-through hit exactly this shape, and only an adversarial reviewer
|
|
98
109
|
// caught it.
|
|
99
110
|
//
|
|
100
|
-
//
|
|
111
|
+
// What is counted is what each render executes (ir/discipline.ts `counted`): a call, a memory access
|
|
112
|
+
// the lift pinned (the `device` placement), whose qualified spelling the recompile performs once per
|
|
113
|
+
// render, and a read of an object the symbol map declares volatile (`declared`), which the tree
|
|
114
|
+
// spells by the object's name. A store to such an object is checked for its spelling only.
|
|
115
|
+
// Deliberately narrow beyond that, so it never declines a function that is fine:
|
|
101
116
|
//
|
|
102
|
-
// • CALLS only.
|
|
103
|
-
// model, and the alias gate governs it);
|
|
104
|
-
// the readability DCE pass is allowed to drop a provably dead one.
|
|
117
|
+
// • CALLS AND QUALIFIED ACCESSES only. A plain load legitimately re-renders (that is the whole point
|
|
118
|
+
// of the inline-at-use model, and the alias gate governs it); a plain store is checked by
|
|
119
|
+
// neither direction because the readability DCE pass is allowed to drop a provably dead one.
|
|
105
120
|
// • 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
|
-
|
|
121
|
+
// switch arms sharing a body, a duplicated return merge, an exit tail copied into each
|
|
122
|
+
// `if (…) { …; return; }` that leaves a loop — and each path still executes it once. So the
|
|
123
|
+
// duplication rule compares the maximum over syntactic root-to-leaf paths (a branch takes the
|
|
124
|
+
// max of its arms, a loop body counts once, a fall-through arm chains into the next, and a
|
|
125
|
+
// `return` ends its path) against the IR's static count. What follows a statement every path
|
|
126
|
+
// leaves is on no path, so it is refused rather than left uncounted: it is code the asm never
|
|
127
|
+
// ran, and a region duplicated there would pass the count unseen.
|
|
128
|
+
// • Names the IR does not have are ignored. A call through a register names no callee, so every
|
|
129
|
+
// one is counted under the single key `call:(*pointer)`: the count holds for indirect calls as
|
|
130
|
+
// a whole, and a pass that makes one of them call what another calls goes unseen.
|
|
131
|
+
// • A pinned access is keyed by whether it reads or writes, and by the constant address both sides
|
|
132
|
+
// can read off it — the IR's base and offset, the tree's `cellAddress` — or `?` where a side
|
|
133
|
+
// cannot. A read never stands for a write. A `?` render may stand for any pinned access and a
|
|
134
|
+
// `?` access may render at any address, and each render is one execution, so both rules are
|
|
135
|
+
// Hall's condition over that compatibility: the renders on a path at any set of addresses are
|
|
136
|
+
// at most those addresses' accesses plus the `?` accesses, and the accesses at any set of
|
|
137
|
+
// addresses have at least as many renders there plus the `?` renders. An access through a named
|
|
138
|
+
// global is counted for re-runs and never as dropped: its qualifier may be the global's
|
|
139
|
+
// declaration, which leaves nothing in the tree.
|
|
140
|
+
// • A declared read is keyed by the object it reads, and counted for re-runs only. The tree side
|
|
141
|
+
// counts each access whose address is that object's name plus arithmetic, once, however it is
|
|
142
|
+
// spelled — `gSym`, `gSym[i]`, `gSym.f`, `gPtr->f`, a `volatile` cast through `&gSym` — and
|
|
143
|
+
// never a store target. An access whose base is another access is the inner one's: through
|
|
144
|
+
// `gPtr->arr[i]` the object is read once. One that reaches the object through a cast with no
|
|
145
|
+
// `volatile` — the tree's own, or the one the printer puts on a base that does not stride the
|
|
146
|
+
// access (l3/volstore.ts `castOffDeclaration`) — is a plain read the compiler may hoist or
|
|
147
|
+
// delete, and is refused. Objects the asm also reads unstamped are skipped, since nothing tells
|
|
148
|
+
// their renders apart. A pinned access through the object's name may render as one of its
|
|
149
|
+
// reads.
|
|
150
|
+
// • A declared store is not counted: it renders at its own position, once. One spelled through a
|
|
151
|
+
// cast with no `volatile` is a plain store the compiler may delete or sink, and is refused, as
|
|
152
|
+
// the read is. Objects the asm also writes unstamped are skipped, as for reads.
|
|
153
|
+
/** Executions per key: `call:<target>`, or `call:(*pointer)` for every call through a register;
|
|
154
|
+
* `device:r:<0xaddress>` for a pinned read and `device:w:<0xaddress>` for a pinned write, with `?`
|
|
155
|
+
* for the address where the counting side cannot read one; or `declared:<object>`. */
|
|
156
|
+
type EffectCounts = Map<string, number>;
|
|
113
157
|
|
|
114
158
|
/** per-key combine of two count maps (`sum` for sequence, `max` for exclusive alternatives) */
|
|
115
|
-
function combine(a:
|
|
159
|
+
function combine(a: EffectCounts, b: EffectCounts, f: (x: number, y: number) => number): EffectCounts {
|
|
116
160
|
const out = new Map(a);
|
|
117
161
|
for (const [k, v] of b) {
|
|
118
162
|
out.set(k, f(out.get(k) ?? 0, v));
|
|
@@ -120,80 +164,310 @@ function combine(a: CallCounts, b: CallCounts, f: (x: number, y: number) => numb
|
|
|
120
164
|
return out;
|
|
121
165
|
}
|
|
122
166
|
|
|
123
|
-
|
|
124
|
-
|
|
167
|
+
const DECLARED = 'declared:';
|
|
168
|
+
const STRIPPED = 'stripped:';
|
|
169
|
+
const STRIPPED_WRITE = 'stripped-write:';
|
|
170
|
+
/** The one callee key every indirect call is counted under: no identifier can spell it. */
|
|
171
|
+
const INDIRECT_CALL = '(*pointer)';
|
|
172
|
+
type Direction = 'r' | 'w';
|
|
173
|
+
const deviceKey = (dir: Direction, addr: number | null): string =>
|
|
174
|
+
`device:${dir}:${addr === null ? '?' : `0x${addr.toString(16)}`}`;
|
|
175
|
+
|
|
176
|
+
/** Is this access spelled through a `volatile` pointee? A dot member qualifies through the element
|
|
177
|
+
* or member it selects from, any other access through its own base's cast chain. */
|
|
178
|
+
function qualifiedAccessChain(e: Expr): boolean {
|
|
179
|
+
if (e.k === 'index') {
|
|
180
|
+
return qualifiedBase(e.base);
|
|
181
|
+
}
|
|
182
|
+
if (e.k === 'field') {
|
|
183
|
+
return fieldSpellsDot(e)
|
|
184
|
+
? (e.base.k === 'index' || e.base.k === 'field') && qualifiedAccessChain(e.base)
|
|
185
|
+
: qualifiedBase(e.base);
|
|
186
|
+
}
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** The global an access's address is named by: its base through casts, address arithmetic and the
|
|
191
|
+
* element or member a dot member selects from, down to `gSym` or `&gSym`. Null when the base is
|
|
192
|
+
* anything else — another access, a local. */
|
|
193
|
+
function accessedObject(base: Expr): string | null {
|
|
194
|
+
switch (base.k) {
|
|
195
|
+
case 'var':
|
|
196
|
+
case 'addr':
|
|
197
|
+
return base.name;
|
|
198
|
+
case 'cast':
|
|
199
|
+
return accessedObject(base.e);
|
|
200
|
+
case 'bin':
|
|
201
|
+
return base.op === '+' || base.op === '-' ? (accessedObject(base.l) ?? accessedObject(base.r)) : null;
|
|
202
|
+
default:
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** What the tree side reads a render against: the objects whose every read the asm makes is
|
|
208
|
+
* declared, those whose every write is, and the rendered C type of an expression, which decides
|
|
209
|
+
* where the printer casts. */
|
|
210
|
+
interface TreeContext {
|
|
211
|
+
declared: ReadonlySet<string>;
|
|
212
|
+
written: ReadonlySet<string>;
|
|
213
|
+
typeOf: (e: Expr) => IrType | undefined;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** every call under `e`, counted by target name; every qualified memory access, by the address it
|
|
217
|
+
* denotes, or by its object where that object's reads are declared, and not at all where it is a
|
|
218
|
+
* write the object's declaration places; and every other access and bare global read, by the
|
|
219
|
+
* object it names — under `stripped:` (`stripped-write:` for a store target) where the access
|
|
220
|
+
* reaches a declared object through a cast that drops its qualifier. `selected` marks an element or member a dot
|
|
221
|
+
* member selects from: that is a part of the one access the member spells, not an access of its
|
|
222
|
+
* own. `named` marks the base of an access: a `gSym` there is the access's address, counted with
|
|
223
|
+
* it. `target` marks a store's target, which is a write. */
|
|
224
|
+
function effectsInExpr(
|
|
225
|
+
e: Expr,
|
|
226
|
+
into: EffectCounts,
|
|
227
|
+
ctx: TreeContext,
|
|
228
|
+
selected = false,
|
|
229
|
+
named = false,
|
|
230
|
+
target = false,
|
|
231
|
+
): void {
|
|
232
|
+
const bump = (k: string) => into.set(k, (into.get(k) ?? 0) + 1);
|
|
233
|
+
const access = e.k === 'index' || e.k === 'field';
|
|
234
|
+
const object = access && !selected ? accessedObject(dotSelected(e)) : null;
|
|
235
|
+
const ofDeclared = object !== null && !target && ctx.declared.has(object);
|
|
236
|
+
const writesDeclared = object !== null && target && ctx.written.has(object);
|
|
125
237
|
if (e.k === 'call') {
|
|
126
|
-
|
|
238
|
+
bump(`call:${calleeOf(e) ?? INDIRECT_CALL}`);
|
|
239
|
+
} else if (!selected && qualifiedAccessChain(e)) {
|
|
240
|
+
if (!writesDeclared) {
|
|
241
|
+
bump(ofDeclared ? `${DECLARED}${object}` : deviceKey(target ? 'w' : 'r', cellAddress(e)));
|
|
242
|
+
}
|
|
243
|
+
} else if (!selected && target && access) {
|
|
244
|
+
if (writesDeclared && castOffDeclaration(e, ctx.typeOf)) {
|
|
245
|
+
bump(`${STRIPPED_WRITE}${object}`);
|
|
246
|
+
}
|
|
247
|
+
} else if (!selected && access) {
|
|
248
|
+
if (object !== null) {
|
|
249
|
+
bump(`${ofDeclared && castOffDeclaration(e, ctx.typeOf) ? STRIPPED : DECLARED}${object}`);
|
|
250
|
+
}
|
|
251
|
+
} else if (!named && !target && e.k === 'var') {
|
|
252
|
+
bump(`${DECLARED}${e.name}`);
|
|
127
253
|
}
|
|
128
|
-
|
|
254
|
+
const partOfThis = e.k === 'field' && fieldSpellsDot(e);
|
|
255
|
+
exprChildren(e).forEach((c) =>
|
|
256
|
+
effectsInExpr(c, into, ctx, partOfThis && c === e.base, access && c === e.base ? true : named && !access, false),
|
|
257
|
+
);
|
|
129
258
|
}
|
|
130
259
|
|
|
131
|
-
/**
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
260
|
+
/** A dot member's own base chain, past the element or member it selects from: the address of the
|
|
261
|
+
* one access the member spells. */
|
|
262
|
+
function dotSelected(e: Extract<Expr, { k: 'index' } | { k: 'field' }>): Expr {
|
|
263
|
+
if (e.k === 'field' && fieldSpellsDot(e) && (e.base.k === 'index' || e.base.k === 'field')) {
|
|
264
|
+
return dotSelected(e.base);
|
|
265
|
+
}
|
|
266
|
+
return e.base;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** The executions on the paths through a statement list, split by how each path LEAVES it: falling off
|
|
270
|
+
* the end, or by a `return`, `break` or `continue`. Null when no path leaves that way. Each is the
|
|
271
|
+
* per-name maximum over the paths of that kind. */
|
|
272
|
+
interface PathCounts {
|
|
273
|
+
through: EffectCounts | null;
|
|
274
|
+
ret: EffectCounts | null;
|
|
275
|
+
brk: EffectCounts | null;
|
|
276
|
+
cont: EffectCounts | null;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const sum = (a: EffectCounts, b: EffectCounts): EffectCounts => combine(a, b, (x, y) => x + y);
|
|
280
|
+
const most = (...xs: (EffectCounts | null)[]): EffectCounts | null =>
|
|
281
|
+
xs.reduce<EffectCounts | null>((acc, x) => (x === null ? acc : acc === null ? x : combine(acc, x, Math.max)), null);
|
|
282
|
+
/** `c` run ahead of every path of `p` */
|
|
283
|
+
const after = (c: EffectCounts, p: PathCounts): PathCounts => ({
|
|
284
|
+
through: p.through && sum(c, p.through),
|
|
285
|
+
ret: p.ret && sum(c, p.ret),
|
|
286
|
+
brk: p.brk && sum(c, p.brk),
|
|
287
|
+
cont: p.cont && sum(c, p.cont),
|
|
288
|
+
});
|
|
289
|
+
/** `a` then `b`: only a path that falls off the end of `a` reaches `b` */
|
|
290
|
+
const seq = (a: PathCounts, b: PathCounts): PathCounts =>
|
|
291
|
+
a.through === null
|
|
292
|
+
? a
|
|
293
|
+
: {
|
|
294
|
+
through: b.through && sum(a.through, b.through),
|
|
295
|
+
ret: most(a.ret, b.ret && sum(a.through, b.ret)),
|
|
296
|
+
brk: most(a.brk, b.brk && sum(a.through, b.brk)),
|
|
297
|
+
cont: most(a.cont, b.cont && sum(a.through, b.cont)),
|
|
298
|
+
};
|
|
299
|
+
/** whichever of the alternatives runs */
|
|
300
|
+
const either = (...ps: PathCounts[]): PathCounts => ({
|
|
301
|
+
through: most(...ps.map((p) => p.through)),
|
|
302
|
+
ret: most(...ps.map((p) => p.ret)),
|
|
303
|
+
brk: most(...ps.map((p) => p.brk)),
|
|
304
|
+
cont: most(...ps.map((p) => p.cont)),
|
|
305
|
+
});
|
|
306
|
+
const falls = (c: EffectCounts): PathCounts => ({ through: c, ret: null, brk: null, cont: null });
|
|
307
|
+
|
|
308
|
+
/** `total` = every occurrence in the tree; `paths` = the most any single syntactic path executes,
|
|
309
|
+
* by how it leaves `stmts`. A path that leaves by `return` runs nothing after it, so two sequenced
|
|
310
|
+
* `if (…) { f(); return; }` arms are one call on any path, not two. `unreached` collects every
|
|
311
|
+
* statement that follows one no path falls past: the path count skips it, so the caller refuses it
|
|
312
|
+
* rather than let a duplicated region hide there. */
|
|
313
|
+
function countEffects(
|
|
314
|
+
stmts: Stmt[],
|
|
315
|
+
ctx: TreeContext,
|
|
316
|
+
unreached: Stmt[] = [],
|
|
317
|
+
): { total: EffectCounts; paths: PathCounts } {
|
|
318
|
+
let total: EffectCounts = new Map();
|
|
319
|
+
let paths = falls(new Map());
|
|
139
320
|
for (const s of stmts) {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
321
|
+
if (paths.through === null) {
|
|
322
|
+
unreached.push(s);
|
|
323
|
+
}
|
|
324
|
+
const own: EffectCounts = new Map();
|
|
325
|
+
stmtExprs(s).forEach((e) => effectsInExpr(e, own, ctx, false, false, s.k === 'store' && e === s.lval));
|
|
326
|
+
total = sum(total, own);
|
|
327
|
+
let here: PathCounts;
|
|
143
328
|
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
|
-
);
|
|
329
|
+
const t = countEffects(s.then, ctx, unreached);
|
|
330
|
+
const e = countEffects(s.else, ctx, unreached);
|
|
331
|
+
total = sum(total, sum(t.total, e.total));
|
|
332
|
+
here = after(own, either(t.paths, e.paths));
|
|
151
333
|
} else if (s.k === 'switch') {
|
|
152
|
-
const arms = s.cases.map((c) =>
|
|
153
|
-
const dflt =
|
|
334
|
+
const arms = s.cases.map((c) => countEffects(c.body, ctx, unreached));
|
|
335
|
+
const dflt = countEffects(s.default ?? [], ctx, unreached);
|
|
336
|
+
total = arms.reduce((acc, a) => sum(acc, a.total), sum(total, dflt.total));
|
|
154
337
|
// A fall-through arm continues into the NEXT one emitted (the last into `default`), so a
|
|
155
338
|
// 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:
|
|
339
|
+
// CRITICAL took. Built from the end; `chain[i]` is that arm's paths.
|
|
340
|
+
const chain: PathCounts[] = new Array(arms.length);
|
|
158
341
|
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 ?
|
|
342
|
+
const next = i + 1 < arms.length ? chain[i + 1] : dflt.paths;
|
|
343
|
+
chain[i] = s.cases[i].fallsThrough ? seq(arms[i].paths, next) : arms[i].paths;
|
|
161
344
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
345
|
+
// No `default` is one more alternative: no arm runs. A `break` leaves the switch, so it
|
|
346
|
+
// continues after it as a fall-off-the-end does.
|
|
347
|
+
const inner = either(...chain, s.default ? dflt.paths : falls(new Map()));
|
|
348
|
+
here = after(own, { through: most(inner.through, inner.brk), ret: inner.ret, brk: null, cont: inner.cont });
|
|
349
|
+
} else if (stmtChildren(s).length > 0) {
|
|
350
|
+
// A loop: its sequenced children (a body, a `for`'s init/inc) counted ONCE — a loop's dynamic
|
|
351
|
+
// trip count is not a syntactic occurrence, and the IR side is static too. A `break` or
|
|
352
|
+
// `continue` in the body lands after the loop or at its test, and the test may exit without
|
|
353
|
+
// running the body.
|
|
354
|
+
const body = countEffects(stmtChildren(s), ctx, unreached);
|
|
355
|
+
total = sum(total, body.total);
|
|
356
|
+
const p = body.paths;
|
|
357
|
+
here = after(own, { through: most(p.through, p.brk, p.cont, new Map()), ret: p.ret, brk: null, cont: null });
|
|
358
|
+
} else if (s.k === 'return') {
|
|
359
|
+
here = { through: null, ret: own, brk: null, cont: null };
|
|
360
|
+
} else if (s.k === 'break') {
|
|
361
|
+
here = { through: null, ret: null, brk: own, cont: null };
|
|
362
|
+
} else if (s.k === 'continue') {
|
|
363
|
+
here = { through: null, ret: null, brk: null, cont: own };
|
|
165
364
|
} 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
|
-
}
|
|
365
|
+
here = falls(own);
|
|
171
366
|
}
|
|
367
|
+
paths = seq(paths, here);
|
|
172
368
|
}
|
|
173
|
-
return { total,
|
|
369
|
+
return { total, paths };
|
|
174
370
|
}
|
|
175
371
|
|
|
176
372
|
/**
|
|
177
|
-
* Post structuring: every call the asm makes is emitted, and none
|
|
178
|
-
* asm makes it on any one path. See the
|
|
373
|
+
* Post structuring: every call and pinned access the asm makes is emitted, and none of those or of
|
|
374
|
+
* its declared volatile reads is emitted more times than the asm makes it on any one path. See the
|
|
375
|
+
* note above for what this deliberately does not cover.
|
|
179
376
|
*/
|
|
180
377
|
export function assertEffectsPreserved(fn: Fn, sfn: SFn): void {
|
|
181
378
|
// Reachable blocks only: an unreachable block's call is legitimately never emitted.
|
|
182
379
|
const seen = reachableBlocks(fn);
|
|
183
|
-
const irCalls:
|
|
380
|
+
const irCalls: EffectCounts = new Map();
|
|
381
|
+
const irDevices: EffectCounts = new Map();
|
|
382
|
+
// Pinned accesses through a named global: in `irDevices` too, and exempt from the dropped rule.
|
|
383
|
+
const namedDevices = new Map<string, number>();
|
|
384
|
+
// …and the reads among them by the object they name, whose renders may be counted as its reads.
|
|
385
|
+
const pinnedReadsOf: EffectCounts = new Map();
|
|
386
|
+
const irDeclared: EffectCounts = new Map();
|
|
387
|
+
// Objects some unplaced read reads too, whose renders the tree side cannot tell from a declared one.
|
|
388
|
+
const plainlyRead = new Set<string>();
|
|
389
|
+
// Objects a declared store writes, and those some unplaced store writes too.
|
|
390
|
+
const declaredWritten = new Set<string>();
|
|
391
|
+
const plainlyWritten = new Set<string>();
|
|
184
392
|
// Unmodelled instructions, by the mnemonic the frontend stamped. Same "never dropped" property as
|
|
185
393
|
// a call, and it needs its own tally because an `opaque` carries no `target`.
|
|
186
394
|
const irOpaques = new Set<string>();
|
|
395
|
+
const defs = new Map<Value, Op>();
|
|
396
|
+
for (const b of fn.blocks) {
|
|
397
|
+
for (const op of b.ops) {
|
|
398
|
+
op.results.forEach((r) => defs.set(r, op));
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
const bump = (m: EffectCounts, k: string) => m.set(k, (m.get(k) ?? 0) + 1);
|
|
402
|
+
// Of the unplaced qualified accesses, those whose address may be a stack object the frame audit
|
|
403
|
+
// qualified, so a refusal names that object rather than a device register.
|
|
404
|
+
const stackUnplaced: Record<Direction, StackUnplaced> = { r: { may: 0, only: 0 }, w: { may: 0, only: 0 } };
|
|
405
|
+
const reachOf = addressReach(fn, defs);
|
|
187
406
|
for (const b of seen) {
|
|
188
407
|
for (const op of b.ops) {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
408
|
+
const reads = op.opcode === 'load' || op.opcode === 'aload';
|
|
409
|
+
const placement = placedAt(op);
|
|
410
|
+
switch (placement) {
|
|
411
|
+
case 'call':
|
|
412
|
+
bump(irCalls, calleeName(op) ?? INDIRECT_CALL);
|
|
413
|
+
break;
|
|
414
|
+
case 'helper':
|
|
415
|
+
// Placed, not counted: the value it computes is pure.
|
|
416
|
+
break;
|
|
417
|
+
case 'device': {
|
|
418
|
+
const plain = op.opcode === 'load' || op.opcode === 'store';
|
|
419
|
+
const dir = reads ? 'r' : 'w';
|
|
420
|
+
const key = deviceKey(dir, plain ? constAddressOf(defs, op.operands[0], op.attrs.off as number) : null);
|
|
421
|
+
bump(irDevices, key);
|
|
422
|
+
const reach = key === deviceKey(dir, null) ? reachOf(op.operands[0]) : null;
|
|
423
|
+
if (reach?.stack === true) {
|
|
424
|
+
stackUnplaced[dir].may++;
|
|
425
|
+
if (!reach.other) {
|
|
426
|
+
stackUnplaced[dir].only++;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
const object = globalBaseOf(defs, op.operands[0]);
|
|
430
|
+
if (object !== null) {
|
|
431
|
+
bump(namedDevices, key);
|
|
432
|
+
if (dir === 'r') {
|
|
433
|
+
bump(pinnedReadsOf, `${DECLARED}${object}`);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
break;
|
|
437
|
+
}
|
|
438
|
+
case 'declared': {
|
|
439
|
+
const object = globalBaseOf(defs, op.operands[0]);
|
|
440
|
+
if (object !== null && reads) {
|
|
441
|
+
bump(irDeclared, `${DECLARED}${object}`);
|
|
442
|
+
} else if (object !== null) {
|
|
443
|
+
declaredWritten.add(object);
|
|
444
|
+
}
|
|
445
|
+
break;
|
|
446
|
+
}
|
|
447
|
+
case null:
|
|
448
|
+
if (op.opcode === 'opaque') {
|
|
449
|
+
irOpaques.add(gapReasonFor(op.attrs));
|
|
450
|
+
} else if (MEM_BASE_OPS.has(op.opcode)) {
|
|
451
|
+
const object = globalBaseOf(defs, op.operands[0]);
|
|
452
|
+
if (object !== null) {
|
|
453
|
+
(reads ? plainlyRead : plainlyWritten).add(object);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
break;
|
|
457
|
+
default: {
|
|
458
|
+
// A new placement is keyed here before it is counted, or answered as not counted.
|
|
459
|
+
const unkeyed: never = placement;
|
|
460
|
+
throw new ContractError(`the effect contract keys no '${unkeyed as string}' placement`);
|
|
461
|
+
}
|
|
194
462
|
}
|
|
195
463
|
}
|
|
196
464
|
}
|
|
465
|
+
for (const object of plainlyRead) {
|
|
466
|
+
irDeclared.delete(`${DECLARED}${object}`);
|
|
467
|
+
}
|
|
468
|
+
for (const object of plainlyWritten) {
|
|
469
|
+
declaredWritten.delete(object);
|
|
470
|
+
}
|
|
197
471
|
// DROPPED only, not the RE-RUN half: a gap rendered twice is a diagnostic printed twice, which
|
|
198
472
|
// costs nothing because nothing recompiles it, and structuring legitimately duplicates a shared
|
|
199
473
|
// arm — so a per-path count here would fire on correct output.
|
|
@@ -216,21 +490,234 @@ export function assertEffectsPreserved(fn: Fn, sfn: SFn): void {
|
|
|
216
490
|
}
|
|
217
491
|
}
|
|
218
492
|
}
|
|
219
|
-
if (!irCalls.size) {
|
|
493
|
+
if (!irCalls.size && !irDevices.size && !irDeclared.size && !declaredWritten.size) {
|
|
220
494
|
return;
|
|
221
495
|
}
|
|
222
|
-
const
|
|
496
|
+
const unreached: Stmt[] = [];
|
|
497
|
+
const vt = declaredTypes(sfn);
|
|
498
|
+
const ctx: TreeContext = {
|
|
499
|
+
declared: new Set([...irDeclared.keys()].map((k) => k.slice(DECLARED.length))),
|
|
500
|
+
written: declaredWritten,
|
|
501
|
+
typeOf: (e) => exprCType(e, vt),
|
|
502
|
+
};
|
|
503
|
+
const { total, paths } = countEffects(sfn.body, ctx, unreached);
|
|
504
|
+
if (unreached.length) {
|
|
505
|
+
throw new ContractError(
|
|
506
|
+
`structuring emitted ${unreached.length} statement(s) no path reaches in '${sfn.name}', after one every path leaves`,
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
const path = most(paths.through, paths.ret, paths.brk, paths.cont) ?? new Map<string, number>();
|
|
223
510
|
for (const [name, n] of irCalls) {
|
|
224
|
-
if (!(total.get(name) ?? 0)) {
|
|
511
|
+
if (!(total.get(`call:${name}`) ?? 0)) {
|
|
225
512
|
throw new ContractError(`structuring dropped the call to '${name}' in '${sfn.name}' — its effect is lost`);
|
|
226
513
|
}
|
|
227
|
-
const p = path.get(name) ?? 0;
|
|
514
|
+
const p = path.get(`call:${name}`) ?? 0;
|
|
228
515
|
if (p > n) {
|
|
229
516
|
throw new ContractError(
|
|
230
517
|
`structuring emitted ${p} calls to '${name}' on one path in '${sfn.name}', where the asm makes ${n}`,
|
|
231
518
|
);
|
|
232
519
|
}
|
|
233
520
|
}
|
|
521
|
+
if (irDevices.size) {
|
|
522
|
+
devicesPreserved(sfn.name, irDevices, namedDevices, total, path, stackUnplaced);
|
|
523
|
+
}
|
|
524
|
+
for (const object of declaredWritten) {
|
|
525
|
+
if (total.has(`${STRIPPED_WRITE}${object}`)) {
|
|
526
|
+
throw new ContractError(
|
|
527
|
+
`structuring spelled a write to the volatile object '${object}' in '${sfn.name}' through a cast that ` +
|
|
528
|
+
'drops its qualifier',
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
for (const [key, n] of irDeclared) {
|
|
533
|
+
const object = key.slice(DECLARED.length);
|
|
534
|
+
if (total.has(`${STRIPPED}${object}`)) {
|
|
535
|
+
throw new ContractError(
|
|
536
|
+
`structuring spelled a read of the volatile object '${object}' in '${sfn.name}' through a cast that ` +
|
|
537
|
+
'drops its qualifier',
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
const p = path.get(key) ?? 0;
|
|
541
|
+
const made = n + (pinnedReadsOf.get(key) ?? 0);
|
|
542
|
+
if (p > made) {
|
|
543
|
+
throw new ContractError(
|
|
544
|
+
`structuring emitted ${p} reads of the volatile object '${object}' on one path in '${sfn.name}', ` +
|
|
545
|
+
`where the asm makes ${made}`,
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/** One direction of a device count map: the count at each address it reads off, and at `?`. */
|
|
552
|
+
function oneDirection(m: EffectCounts, dir: Direction): { at: Map<string, number>; unplaced: number; all: number } {
|
|
553
|
+
const prefix = `device:${dir}:`;
|
|
554
|
+
const at = new Map<string, number>();
|
|
555
|
+
let unplaced = 0;
|
|
556
|
+
let all = 0;
|
|
557
|
+
for (const [k, v] of m) {
|
|
558
|
+
if (k.startsWith(prefix)) {
|
|
559
|
+
all += v;
|
|
560
|
+
if (k === `${prefix}?`) {
|
|
561
|
+
unplaced = v;
|
|
562
|
+
} else {
|
|
563
|
+
at.set(k.slice(prefix.length), v);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
return { at, unplaced, all };
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/** What an address may be: a stack object, something else, or either. */
|
|
571
|
+
interface AddressReach {
|
|
572
|
+
stack: boolean;
|
|
573
|
+
other: boolean;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/** What each address may be: a stack object (a `laddr`, moved by `add`/`sub`), anything but a named
|
|
577
|
+
* global, or — through a phi whose incoming values differ — either. A fixpoint, so a loop phi
|
|
578
|
+
* stepped from a `laddr` is a stack object only. */
|
|
579
|
+
function addressReach(fn: Fn, defs: ReadonlyMap<Value, Op>): (addr: Value) => AddressReach {
|
|
580
|
+
const incoming = new Map<Value, Value[]>();
|
|
581
|
+
for (const b of fn.blocks) {
|
|
582
|
+
for (const op of b.ops) {
|
|
583
|
+
for (const s of op.successors ?? []) {
|
|
584
|
+
s.args.forEach((arg, i) => {
|
|
585
|
+
const param = s.block.params[i];
|
|
586
|
+
if (param !== undefined) {
|
|
587
|
+
(incoming.get(param) ?? incoming.set(param, []).get(param)!).push(arg);
|
|
588
|
+
}
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
const NONE: AddressReach = { stack: false, other: false };
|
|
594
|
+
const known = new Map<Value, AddressReach>();
|
|
595
|
+
const at = (v: Value) => known.get(v) ?? NONE;
|
|
596
|
+
// A value not yet reached stays NONE, so an `add` waiting on one does not call itself `other`.
|
|
597
|
+
const step = (v: Value): AddressReach => {
|
|
598
|
+
const d = defs.get(v);
|
|
599
|
+
if (d === undefined) {
|
|
600
|
+
const from = incoming.get(v) ?? [];
|
|
601
|
+
return from.length === 0
|
|
602
|
+
? { stack: false, other: true }
|
|
603
|
+
: { stack: from.some((a) => at(a).stack), other: from.some((a) => at(a).other) };
|
|
604
|
+
}
|
|
605
|
+
if (d.opcode === 'laddr') {
|
|
606
|
+
return { stack: true, other: false };
|
|
607
|
+
}
|
|
608
|
+
// An access through a named global is exempt from the dropped rule, so a phi side that names
|
|
609
|
+
// one is not what a refusal names.
|
|
610
|
+
if (d.opcode === 'gaddr') {
|
|
611
|
+
return NONE;
|
|
612
|
+
}
|
|
613
|
+
if (d.opcode !== 'add' && d.opcode !== 'sub') {
|
|
614
|
+
return { stack: false, other: true };
|
|
615
|
+
}
|
|
616
|
+
const roots = (d.opcode === 'add' ? d.operands : d.operands.slice(0, 1)).map(at);
|
|
617
|
+
const bases = roots.filter((r) => r.stack);
|
|
618
|
+
if (bases.length > 0) {
|
|
619
|
+
return { stack: true, other: bases.some((r) => r.other) };
|
|
620
|
+
}
|
|
621
|
+
return roots.some((r) => !r.other) ? NONE : { stack: false, other: true };
|
|
622
|
+
};
|
|
623
|
+
const values = [...defs.keys(), ...incoming.keys()];
|
|
624
|
+
for (let changed = true; changed;) {
|
|
625
|
+
changed = false;
|
|
626
|
+
for (const v of values) {
|
|
627
|
+
const was = at(v);
|
|
628
|
+
const now = step(v);
|
|
629
|
+
const joined = { stack: was.stack || now.stack, other: was.other || now.other };
|
|
630
|
+
if (joined.stack !== was.stack || joined.other !== was.other) {
|
|
631
|
+
known.set(v, joined);
|
|
632
|
+
changed = true;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
return (addr) => (known.has(addr) ? at(addr) : { stack: false, other: true });
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/** How many unplaced qualified accesses may be a stack object, and how many can be nothing else. */
|
|
640
|
+
interface StackUnplaced {
|
|
641
|
+
may: number;
|
|
642
|
+
only: number;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/** What an unplaced qualified access may be, given how many of `unplaced` may or must be a stack
|
|
646
|
+
* object. */
|
|
647
|
+
function unplacedNoun(stack: StackUnplaced, unplaced: number): { one: string; many: string } {
|
|
648
|
+
if (stack.may === 0) {
|
|
649
|
+
return { one: 'a device register', many: 'device registers' };
|
|
650
|
+
}
|
|
651
|
+
if (stack.only >= unplaced) {
|
|
652
|
+
return { one: 'an address-taken stack local', many: 'address-taken stack locals' };
|
|
653
|
+
}
|
|
654
|
+
return { one: 'a device register or a stack local', many: 'device registers or a stack local' };
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
const NOUNS: Readonly<Record<Direction, { one: string; many: string }>> = {
|
|
658
|
+
r: { one: 'read of', many: 'reads of' },
|
|
659
|
+
w: { one: 'write to', many: 'writes to' },
|
|
660
|
+
};
|
|
661
|
+
|
|
662
|
+
/** The pinned-access half of {@link assertEffectsPreserved}, over the counts keyed by `deviceKey`:
|
|
663
|
+
* first that no access is dropped, then that none is re-run, each direction on its own. */
|
|
664
|
+
function devicesPreserved(
|
|
665
|
+
name: string,
|
|
666
|
+
ir: EffectCounts,
|
|
667
|
+
named: EffectCounts,
|
|
668
|
+
total: EffectCounts,
|
|
669
|
+
path: EffectCounts,
|
|
670
|
+
stackUnplaced: Readonly<Record<Direction, StackUnplaced>>,
|
|
671
|
+
): void {
|
|
672
|
+
const directions = ['r', 'w'] as const;
|
|
673
|
+
for (const dir of directions) {
|
|
674
|
+
const made = oneDirection(ir, dir);
|
|
675
|
+
const exempt = oneDirection(named, dir);
|
|
676
|
+
const renders = oneDirection(total, dir);
|
|
677
|
+
// An access through a named global needs no render of its own.
|
|
678
|
+
const owed = (a: string) => Math.max(0, (made.at.get(a) ?? 0) - (exempt.at.get(a) ?? 0));
|
|
679
|
+
const short = [...made.at.keys()].filter((a) => owed(a) > (renders.at.get(a) ?? 0));
|
|
680
|
+
const deficit = short.reduce((n, a) => n + owed(a) - (renders.at.get(a) ?? 0), 0);
|
|
681
|
+
if (deficit > renders.unplaced) {
|
|
682
|
+
throw new ContractError(
|
|
683
|
+
short.length === 1
|
|
684
|
+
? `structuring dropped the ${NOUNS[dir].one} the device register at ${short[0]} in '${name}' — ` +
|
|
685
|
+
'the machine made it and the source does not'
|
|
686
|
+
: `structuring dropped ${NOUNS[dir].many} the device registers at ${short.join(', ')} in '${name}' — ` +
|
|
687
|
+
'the machine made them and the source does not',
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
const owedAll = [...made.at.keys()].reduce((n, a) => n + owed(a), 0) + Math.max(0, made.unplaced - exempt.unplaced);
|
|
691
|
+
if (owedAll > renders.all) {
|
|
692
|
+
throw new ContractError(
|
|
693
|
+
`structuring dropped a ${NOUNS[dir].one} ${unplacedNoun(stackUnplaced[dir], made.unplaced).one} in ` +
|
|
694
|
+
`'${name}' — the machine made it and the source does not`,
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
for (const dir of directions) {
|
|
699
|
+
const made = oneDirection(ir, dir);
|
|
700
|
+
const onPath = oneDirection(path, dir);
|
|
701
|
+
let excess = 0;
|
|
702
|
+
for (const [a, p] of onPath.at) {
|
|
703
|
+
const n = made.at.get(a) ?? 0;
|
|
704
|
+
if (p > n + made.unplaced) {
|
|
705
|
+
throw new ContractError(
|
|
706
|
+
`structuring emitted ${p} ${NOUNS[dir].many} the device register at ${a} on one path in '${name}', ` +
|
|
707
|
+
`where the asm makes ${n + made.unplaced}`,
|
|
708
|
+
);
|
|
709
|
+
}
|
|
710
|
+
excess += Math.max(0, p - n);
|
|
711
|
+
}
|
|
712
|
+
if (excess > made.unplaced || onPath.all > made.all) {
|
|
713
|
+
// past the unplaced count, the excess is at the addresses, every one a device register
|
|
714
|
+
const what = excess > made.unplaced ? 'device registers' : unplacedNoun(stackUnplaced[dir], made.unplaced).many;
|
|
715
|
+
throw new ContractError(
|
|
716
|
+
`structuring emitted ${onPath.all} ${NOUNS[dir].many} ${what} on one path in '${name}', where the ` +
|
|
717
|
+
`asm makes ${made.all}`,
|
|
718
|
+
);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
234
721
|
}
|
|
235
722
|
|
|
236
723
|
/** Post structuring: a local the body READS must be WRITTEN somewhere in it. A materialized value
|
|
@@ -259,6 +746,11 @@ export function assertLocalsWritten(sfn: SFn): void {
|
|
|
259
746
|
if ((e.k === 'var' || e.k === 'addr') && suspect.has(e.name)) {
|
|
260
747
|
(e.k === 'addr' ? written : read).add(e.name);
|
|
261
748
|
}
|
|
749
|
+
// `v++` is a read and a write, so it answers both sides.
|
|
750
|
+
if (e.k === 'postincr' && suspect.has(e.name)) {
|
|
751
|
+
read.add(e.name);
|
|
752
|
+
written.add(e.name);
|
|
753
|
+
}
|
|
262
754
|
exprChildren(e).forEach(walkExpr);
|
|
263
755
|
};
|
|
264
756
|
const walkStmt = (st: Stmt): void => {
|
|
@@ -278,6 +770,46 @@ export function assertLocalsWritten(sfn: SFn): void {
|
|
|
278
770
|
}
|
|
279
771
|
}
|
|
280
772
|
|
|
773
|
+
/** Post structuring, and after every respell variation: a `postincr` is the ONLY mention of its
|
|
774
|
+
* local inside the full expression holding it.
|
|
775
|
+
*
|
|
776
|
+
* C89 leaves a program undefined when an object is modified and also read elsewhere between two
|
|
777
|
+
* sequence points, so `v1++ <= 9 && v1 != 0` is not a spelling asmlift may emit — the compiler
|
|
778
|
+
* picks either value, and the candidate still compiles and scores. The producer establishes the
|
|
779
|
+
* property on the tree it builds (structure.ts's `emitDoWhile`, judged by `PREUPDATE_COND_GATES`);
|
|
780
|
+
* what this catches is a later pass bringing a SECOND mention into that expression — `l3/coalesce.ts`
|
|
781
|
+
* renames one local onto another over a whole body, and asks nothing about this.
|
|
782
|
+
*
|
|
783
|
+
* PER FULL EXPRESSION, not per function: a statement boundary is a sequence point, so the loop
|
|
784
|
+
* body reading the same name is well-defined, and a pass that duplicates a whole expression owes
|
|
785
|
+
* the evaluation-trace argument to `exprHasEffect` rather than to this contract. */
|
|
786
|
+
export function assertPostIncrUnshared(sfn: SFn): void {
|
|
787
|
+
const overUpdated = (e: Expr): string | undefined => {
|
|
788
|
+
const mentions = new Map<string, number>();
|
|
789
|
+
const updated = new Set<string>();
|
|
790
|
+
const walk = (x: Expr): void => {
|
|
791
|
+
if (x.k === 'var' || x.k === 'addr' || x.k === 'postincr') {
|
|
792
|
+
mentions.set(x.name, (mentions.get(x.name) ?? 0) + 1);
|
|
793
|
+
}
|
|
794
|
+
if (x.k === 'postincr') {
|
|
795
|
+
updated.add(x.name);
|
|
796
|
+
}
|
|
797
|
+
exprChildren(x).forEach(walk);
|
|
798
|
+
};
|
|
799
|
+
walk(e);
|
|
800
|
+
return [...updated].find((n) => mentions.get(n)! > 1);
|
|
801
|
+
};
|
|
802
|
+
const bad = (s: Stmt): string | undefined =>
|
|
803
|
+
stmtExprs(s).map(overUpdated).find(Boolean) ?? stmtChildren(s).map(bad).find(Boolean);
|
|
804
|
+
const name = sfn.body.map(bad).find(Boolean);
|
|
805
|
+
if (name !== undefined) {
|
|
806
|
+
throw new ContractError(
|
|
807
|
+
`'${name}' is updated by a post-increment and read again in the same expression in '${sfn.name}' — ` +
|
|
808
|
+
`C89 leaves which value the other read sees undefined`,
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
281
813
|
/** After a respell variation: a local a pass DELETED from the declaration list is named nowhere in the tree it
|
|
282
814
|
* produced.
|
|
283
815
|
*
|
|
@@ -358,9 +890,10 @@ export function assertHoistsDominate(sfn: SFn, minted: ReadonlySet<string>): voi
|
|
|
358
890
|
const readUndominated = (e: Expr, live: ReadonlySet<string>): string | null => {
|
|
359
891
|
// `&p` COUNTS, the same mention the placing passes query on (l3/hoist.ts): the address is what
|
|
360
892
|
// a callee reads the cell through, so an init has to precede it as surely as it must precede a
|
|
361
|
-
// read.
|
|
362
|
-
|
|
363
|
-
|
|
893
|
+
// read. `mentionedName` is the vocabulary rather than a pair of arms here for that reason.
|
|
894
|
+
const n = mentionedName(e);
|
|
895
|
+
if (n !== undefined && minted.has(n) && !live.has(n)) {
|
|
896
|
+
return n;
|
|
364
897
|
}
|
|
365
898
|
let bad: string | null = null;
|
|
366
899
|
mapExprChildren(e, (c) => {
|
|
@@ -460,9 +993,12 @@ export function assertDerefsTyped(sfn: SFn): void {
|
|
|
460
993
|
const NO_PTR_OPS = new Set<BinOp>(['&', '|', '^', '<<', '>>', '>>>', '*', '/', '/u', '%', '%u']);
|
|
461
994
|
// The comparison operators — where a bare `&SYM` operand is SIGN-ambiguous, not ill-formed.
|
|
462
995
|
const CMP_OPS = new Set(['<', '<=', '>', '>=', '==', '!=']);
|
|
463
|
-
// 1/2/4 only
|
|
464
|
-
//
|
|
465
|
-
//
|
|
996
|
+
// 1/2/4 only. `s64` IS in the typedef vocabulary now (target.ts C_TYPEDEFS), and the 8-byte
|
|
997
|
+
// ACCESS still is not: nothing in this pipeline recognises a pair of word loads as one 64-bit
|
|
998
|
+
// read, so a width-8 index here would be a claim no pass makes. The rule pre-empts the
|
|
999
|
+
// three-stages-later failure either way. (If f64 loads ever land they are floats, not a scalar
|
|
1000
|
+
// width here.) Adding 8 would also silently under-report in `l3/typing.ts` `exprIntWidth`, whose
|
|
1001
|
+
// soundness rests on memory not being a way a 64-bit value enters a rendered expression.
|
|
466
1002
|
const SCALAR_WIDTHS = new Set([1, 2, 4]);
|
|
467
1003
|
// Dot-form field bases (struct-array elements) carry the struct STRIDE as their width — any
|
|
468
1004
|
// stride matching the element size is legal there (the tree-level struct cast governs the
|
|
@@ -526,10 +1062,10 @@ export function assertDerefsTyped(sfn: SFn): void {
|
|
|
526
1062
|
if (bt) {
|
|
527
1063
|
// type-check against the same dot-vs-arrow spelling the printer will use (shared rule)
|
|
528
1064
|
const st = fieldSpellsDot(e) ? bt : bt.kind === 'ptr' ? bt.to : undefined;
|
|
529
|
-
if (!st || st.kind !== 'struct') {
|
|
1065
|
+
if (!st || (st.kind !== 'struct' && st.kind !== 'union')) {
|
|
530
1066
|
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
|
|
1067
|
+
} else if (memberOf(st, e.name) === undefined) {
|
|
1068
|
+
bad.push(`member access '${e.name}' not declared on '${typeToString(st)}'`);
|
|
533
1069
|
}
|
|
534
1070
|
}
|
|
535
1071
|
}
|