@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/l3/hoist.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// see `HoistPlacement` and `BaseInitPlacement`. Everything lives in one file because each half was
|
|
11
11
|
// a per-caller copy once and every copy drifted from its original.
|
|
12
12
|
import type { Expr, SFn, Stmt } from './ast';
|
|
13
|
-
import { exprChildren, mapStmtLists, stmtChildren, stmtExprs, stmtLists } from './ast';
|
|
13
|
+
import { exprChildren, mapStmtLists, mentionedName, stmtChildren, stmtExprs, stmtLists } from './ast';
|
|
14
14
|
import { localMentions } from './mentions';
|
|
15
15
|
|
|
16
16
|
/** Every identifier a MINTED name must not collide with, anywhere in `sfn` — the hoists below,
|
|
@@ -18,17 +18,18 @@ import { localMentions } from './mentions';
|
|
|
18
18
|
*
|
|
19
19
|
* Wider than "the declared locals" on purpose, and each addition is a real collision:
|
|
20
20
|
* - params and locals, obviously;
|
|
21
|
-
* - every
|
|
22
|
-
* silently redirects every later mention of it;
|
|
21
|
+
* - every name an expression mentions — a GLOBAL is referenced by bare name, so a local shadowing
|
|
22
|
+
* one silently redirects every later mention of it;
|
|
23
23
|
* - every CALL TARGET — a local named like a callee shadows the function;
|
|
24
24
|
* - every assignment target, which includes names no declaration list carries. */
|
|
25
25
|
export function takenNames(sfn: SFn): Set<string> {
|
|
26
26
|
const taken = new Set<string>([...sfn.params.map((p) => p.name), ...sfn.locals.map((l) => l.name)]);
|
|
27
27
|
const visit = (e: Expr): void => {
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
const n = mentionedName(e);
|
|
29
|
+
if (n !== undefined) {
|
|
30
|
+
taken.add(n);
|
|
30
31
|
}
|
|
31
|
-
if (e.k === 'call') {
|
|
32
|
+
if (e.k === 'call' && typeof e.fn === 'string') {
|
|
32
33
|
taken.add(e.fn);
|
|
33
34
|
}
|
|
34
35
|
for (const c of exprChildren(e)) {
|
|
@@ -115,7 +116,7 @@ function mentionsHere(s: Stmt, name: string): boolean {
|
|
|
115
116
|
}
|
|
116
117
|
let found = false;
|
|
117
118
|
const visit = (e: Expr): void => {
|
|
118
|
-
if ((e
|
|
119
|
+
if (mentionedName(e) === name) {
|
|
119
120
|
found = true;
|
|
120
121
|
}
|
|
121
122
|
for (const c of exprChildren(e)) {
|
package/src/l3/initfirst.ts
CHANGED
|
@@ -38,15 +38,33 @@
|
|
|
38
38
|
// re-enters everything, a case can fall through), so there the variable must appear nowhere
|
|
39
39
|
// outside the rewritten `if` at all. Declines (null) when nothing changes.
|
|
40
40
|
import type { Expr, SFn, Stmt } from './ast';
|
|
41
|
-
import {
|
|
42
|
-
|
|
41
|
+
import {
|
|
42
|
+
NEGATE_REL,
|
|
43
|
+
exprChildren,
|
|
44
|
+
exprEquals,
|
|
45
|
+
exprHasEffect,
|
|
46
|
+
keepsHelperRel,
|
|
47
|
+
mentionedName,
|
|
48
|
+
stmtChildren,
|
|
49
|
+
stmtExprs,
|
|
50
|
+
walkExprs,
|
|
51
|
+
} from './ast';
|
|
52
|
+
import {
|
|
53
|
+
arithConversionSignedness,
|
|
54
|
+
declaredTypes,
|
|
55
|
+
exprIntWidth,
|
|
56
|
+
provablyNonNegative,
|
|
57
|
+
rendersSignedOverUnsigned,
|
|
58
|
+
} from './typing';
|
|
43
59
|
|
|
44
60
|
const readsVar = (e: Expr, name: string): boolean =>
|
|
45
|
-
(
|
|
61
|
+
mentionedName(e) === name || exprChildren(e).some((c) => readsVar(c, name));
|
|
46
62
|
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
63
|
+
// MENTIONS in an expression — a read, an `&v` a callee may read through, a `v++` that reads before
|
|
64
|
+
// it writes. A whole-statement write is what is benign (an `assign` overwrites the minted value on
|
|
65
|
+
// every path, and any read after it is that write's business), which is why the target of one is
|
|
66
|
+
// not consulted here; touchesOutside below is TOTAL because strong mode must know the name is
|
|
67
|
+
// absent, presence of any kind included.
|
|
50
68
|
const stmtTouches = (s: Stmt, name: string): boolean =>
|
|
51
69
|
stmtExprs(s).some((e) => readsVar(e, name)) || stmtChildren(s).some((x) => stmtTouches(x, name));
|
|
52
70
|
|
|
@@ -67,10 +85,11 @@ const stripWideIntCast = (e: Expr): Expr =>
|
|
|
67
85
|
* would silently discard it. */
|
|
68
86
|
const varRooted = (e: Expr): boolean => (e.k === 'var' ? true : e.k === 'cast' ? varRooted(e.e) : false);
|
|
69
87
|
|
|
70
|
-
/** A guard re-spell's X: call/marker-free, every named leaf a non-volatile param/local
|
|
71
|
-
* function (a global or `&gSym` could be project-declared volatile), and every deref
|
|
88
|
+
/** A guard re-spell's X: call/marker/post-increment-free, every named leaf a non-volatile param/local
|
|
89
|
+
* of THIS function (a global or `&gSym` could be project-declared volatile), and every deref
|
|
90
|
+
* `varRooted`. */
|
|
72
91
|
const hoistableRead = (e: Expr, ownNames: ReadonlySet<string>, volatileLocals: ReadonlySet<string>): boolean => {
|
|
73
|
-
if (e.k === 'call' || e.k === 'marker' || e.k === 'addr') {
|
|
92
|
+
if (e.k === 'call' || e.k === 'marker' || e.k === 'postincr' || e.k === 'addr') {
|
|
74
93
|
return false;
|
|
75
94
|
}
|
|
76
95
|
if (e.k === 'var' && (!ownNames.has(e.name) || volatileLocals.has(e.name))) {
|
|
@@ -83,8 +102,9 @@ const hoistableRead = (e: Expr, ownNames: ReadonlySet<string>, volatileLocals: R
|
|
|
83
102
|
};
|
|
84
103
|
|
|
85
104
|
/** A compare operand and the init's value denote the same 32-bit value under different SPELLINGS
|
|
86
|
-
* when a width-32 cast is all that separates them:
|
|
87
|
-
* the branch unsigned, and on a zero-trip guard that side is the very const the
|
|
105
|
+
* when a width-32 cast is all that separates them: the unsigned-compare spelling wraps one side in
|
|
106
|
+
* `(u32)` to make the branch unsigned, and on a zero-trip guard that side is the very const the
|
|
107
|
+
* init assigns.
|
|
88
108
|
* The swap is still exact — `v = X` stores X's 32 bits and `v` is 32-bit-declared
|
|
89
109
|
* (meaningPreserved refuses otherwise), so `v` and `(u32)X` carry the same bit pattern and only
|
|
90
110
|
* the compare's rendered signedness can differ, which meaningPreserved checks separately. A
|
|
@@ -110,6 +130,12 @@ const meaningPreserved = (
|
|
|
110
130
|
if (vt?.kind !== 'int' || vt.width !== 32) {
|
|
111
131
|
return false;
|
|
112
132
|
}
|
|
133
|
+
// …AND SO MUST X, which the premise above states and the check did not: `v = X` represents X
|
|
134
|
+
// exactly only while X fits in v, so a 64-bit X assigned to a 32-bit v truncates and every
|
|
135
|
+
// signedness argument below it is about a value the program does not have.
|
|
136
|
+
if (exprIntWidth(side === 'l' ? l : r, env) !== 32) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
113
139
|
if (provablyNonNegative(l, env) && provablyNonNegative(r, env)) {
|
|
114
140
|
return true;
|
|
115
141
|
}
|
|
@@ -119,6 +145,25 @@ const meaningPreserved = (
|
|
|
119
145
|
return before !== undefined && before === after;
|
|
120
146
|
};
|
|
121
147
|
|
|
148
|
+
/** `cond` with one side read through `v`, AFTER `meaningPreserved` said yes. That answer is a
|
|
149
|
+
* value proof the finished tree cannot repeat: `v` holds X, but `v`'s declaration does not say
|
|
150
|
+
* what range X had, so rank.ts's judgment of the tree (typing.ts `signedOverUnsignedCompare`)
|
|
151
|
+
* would call an unsigned compare whose ORIGINAL operands were both non-negative signed, and
|
|
152
|
+
* withhold a spelling that compares the same values. So a compare that was value-faithful before
|
|
153
|
+
* the swap is marked proved: its `unsignedCmp` evidence is dropped. One that was not keeps it. */
|
|
154
|
+
const swapSide = (
|
|
155
|
+
cond: Extract<Expr, { k: 'bin' }>,
|
|
156
|
+
side: 'l' | 'r',
|
|
157
|
+
v: string,
|
|
158
|
+
env: ReturnType<typeof declaredTypes>,
|
|
159
|
+
): Expr => {
|
|
160
|
+
const out: Extract<Expr, { k: 'bin' }> = { ...cond, [side]: { k: 'var', name: v } };
|
|
161
|
+
if (cond.unsignedCmp === true && !rendersSignedOverUnsigned(cond.l, cond.r, env)) {
|
|
162
|
+
delete out.unsignedCmp;
|
|
163
|
+
}
|
|
164
|
+
return out;
|
|
165
|
+
};
|
|
166
|
+
|
|
122
167
|
/** TOTAL (reads and pure writes) — see the note on stmtTouches. Runs against the pre-rewrite
|
|
123
168
|
* tree (`skip` is an original-tree statement, found by identity); the rewrites never change a
|
|
124
169
|
* name's presence in a subtree, so the verdict carries over to the rewritten one. */
|
|
@@ -205,7 +250,7 @@ export function initFirstGuards(sfn: SFn): SFn | null {
|
|
|
205
250
|
then = then.slice(1);
|
|
206
251
|
els = els.slice(1);
|
|
207
252
|
}
|
|
208
|
-
if (then.length === 0 && els.length > 0) {
|
|
253
|
+
if (then.length === 0 && els.length > 0 && !keepsHelperRel(cond)) {
|
|
209
254
|
const neg = cond.k === 'bin' ? NEGATE_REL[cond.op] : undefined;
|
|
210
255
|
if (neg !== undefined && cond.k === 'bin') {
|
|
211
256
|
cond = { ...cond, op: neg };
|
|
@@ -216,16 +261,20 @@ export function initFirstGuards(sfn: SFn): SFn | null {
|
|
|
216
261
|
// a COMMON-hoisted variable holds its const on both paths, so the condition's matching
|
|
217
262
|
// const operand reads through it unconditionally — no tail gate needed.
|
|
218
263
|
// A BARE const only, where the guard re-spelling below uses the cast-tolerant `sameValue`:
|
|
219
|
-
//
|
|
220
|
-
// type
|
|
221
|
-
//
|
|
222
|
-
//
|
|
264
|
+
// peeling a `(u32)` here would put a re-rendered signedness on the table with no cast left to
|
|
265
|
+
// say which. The name's DECLARED type can re-render it too — `if (a1 > 0)` read through a
|
|
266
|
+
// `u32 v` that holds the 0 is `a1 != 0` — so the swap asks `meaningPreserved` as the guard
|
|
267
|
+
// re-spelling does.
|
|
223
268
|
for (const hv of hoisted) {
|
|
224
269
|
if (cond.k === 'bin' && NEGATE_REL[cond.op]) {
|
|
225
270
|
if (cond.l.k === 'const' && cond.l.value === hv.value) {
|
|
226
|
-
cond
|
|
271
|
+
if (meaningPreserved(cond.l, cond.r, 'l', hv.name, env)) {
|
|
272
|
+
cond = swapSide(cond, 'l', hv.name, env);
|
|
273
|
+
}
|
|
227
274
|
} else if (cond.r.k === 'const' && cond.r.value === hv.value) {
|
|
228
|
-
|
|
275
|
+
if (meaningPreserved(cond.l, cond.r, 'r', hv.name, env)) {
|
|
276
|
+
cond = swapSide(cond, 'r', hv.name, env);
|
|
277
|
+
}
|
|
229
278
|
}
|
|
230
279
|
}
|
|
231
280
|
}
|
|
@@ -265,7 +314,7 @@ export function initFirstGuards(sfn: SFn): SFn | null {
|
|
|
265
314
|
) {
|
|
266
315
|
out.push(init);
|
|
267
316
|
moved.add(init);
|
|
268
|
-
cond =
|
|
317
|
+
cond = swapSide(cond, side, init.name, env);
|
|
269
318
|
then = then.slice(1);
|
|
270
319
|
changed = true;
|
|
271
320
|
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// L3 respell variation: a pointer cell spelled as the integer a project may have declared it.
|
|
2
|
+
//
|
|
3
|
+
// A global the body stores a pointer into is spelled as a pointer (l3/ptrcell.ts): the store is
|
|
4
|
+
// `g = (void *)((u8 *)g + K)` or `g = (void *)X`, the integer uses convert it, `(u32)g`, and the
|
|
5
|
+
// candidate's own world declares it `void *`. A project that declares the cell an INTEGER
|
|
6
|
+
// (`u32 g; … g += 4;`) wrote none of those conversions, and two compilers tell: CodeWarrior rejects
|
|
7
|
+
// a pointer stored into the integer, and IDO allocates registers differently around a `(u32)g` it
|
|
8
|
+
// takes as a conversion. Under a pointer declaration `g = g + K` scales K by the pointee instead,
|
|
9
|
+
// so which of the two is right is a fact about the project's declaration, which nothing in the asm
|
|
10
|
+
// states.
|
|
11
|
+
//
|
|
12
|
+
// So the spelling is a PROOF-CARRYING candidate (`needsProof`, Candidate.matchOnly): published
|
|
13
|
+
// only at a byte-exact score, where the bytes are the evidence that the declaration it compiled
|
|
14
|
+
// against gives K the asm's stride. Its own self-declared world declares the cell an integer,
|
|
15
|
+
// because no pointer store is left in it.
|
|
16
|
+
//
|
|
17
|
+
// SCOPE (decline over approximate): a cell no declaration in the tree types and the symbol map
|
|
18
|
+
// does not declare other than an integer, and of its uses only the ones the pointer spelling made:
|
|
19
|
+
// the `(void *)` stores into it and a `(u32)` conversion of its bare value. A map's pointer is the
|
|
20
|
+
// project's own declaration, and its own world declares that, under which a byte-exact score is
|
|
21
|
+
// no evidence of the stride the project's pointee gives K. A global the body only reads converted
|
|
22
|
+
// is not this question.
|
|
23
|
+
import type { IrType } from '../ir/types';
|
|
24
|
+
import { T } from '../ir/types';
|
|
25
|
+
import { type Expr, type SFn, type Stmt, mapExprChildren, mapStmtExprs } from './ast';
|
|
26
|
+
import { pointerCellStore, pointerCellsOf } from './ptrcell';
|
|
27
|
+
import { declaredTypes } from './typing';
|
|
28
|
+
|
|
29
|
+
const isPtrTo = (t: IrType, to: 'void' | 'u8'): boolean =>
|
|
30
|
+
t.kind === 'ptr' && (to === 'void' ? t.to.kind === 'void' : t.to.kind === 'int' && t.to.width === 8);
|
|
31
|
+
const isWord = (t: IrType): boolean => t.kind === 'int' && t.width === 32;
|
|
32
|
+
|
|
33
|
+
/** `g`, `(u8 *)g`, `(u32)g` or `(u32)(u8 *)g` for the bare name `name`. */
|
|
34
|
+
const isCell = (x: Expr, name: string): boolean =>
|
|
35
|
+
(x.k === 'var' && x.name === name) || (x.k === 'cast' && (isWord(x.to) || isPtrTo(x.to, 'u8')) && isCell(x.e, name));
|
|
36
|
+
|
|
37
|
+
/** The integer a `(void *)` store into the cell `name` stores: its own arithmetic for a self-store,
|
|
38
|
+
* a bare value as it stands, anything else converted. */
|
|
39
|
+
function integerValue(value: Expr, name: string): Expr {
|
|
40
|
+
const stored = value.k === 'cast' ? value.e : value;
|
|
41
|
+
// `(u8 *)((u32)g + r)`: the integer sum the arithmetic table restores to a byte pointer
|
|
42
|
+
// (structure/pointer-spelling.ts ARITH_ROWS, `restore: 'bytes'`)
|
|
43
|
+
const sum = stored.k === 'cast' && isPtrTo(stored.to, 'u8') ? stored.e : stored;
|
|
44
|
+
if (sum.k === 'bin' && (sum.op === '+' || sum.op === '-') && isCell(sum.l, name)) {
|
|
45
|
+
return { ...sum, l: { k: 'var', name } };
|
|
46
|
+
}
|
|
47
|
+
return stored.k === 'var' ? stored : { k: 'cast', to: T.u(32), e: stored };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The `/int-cell` candidate: every pointer cell no declaration types, spelled as an integer.
|
|
51
|
+
* `mapDeclared` says the symbol map declares a name something other than an integer. */
|
|
52
|
+
export function integerCells(
|
|
53
|
+
sfn: SFn,
|
|
54
|
+
mapDeclared: (name: string) => boolean,
|
|
55
|
+
): { sfn: SFn; needsProof: boolean } | null {
|
|
56
|
+
const vt = declaredTypes(sfn);
|
|
57
|
+
const cells = new Set([...pointerCellsOf(sfn)].filter((n) => vt(n) === undefined && !mapDeclared(n)));
|
|
58
|
+
if (cells.size === 0) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
const expr = (e0: Expr): Expr => {
|
|
62
|
+
const e = mapExprChildren(e0, expr);
|
|
63
|
+
if (e.k === 'cast' && isWord(e.to)) {
|
|
64
|
+
const inner = e.e.k === 'cast' && isPtrTo(e.e.to, 'u8') ? e.e.e : e.e;
|
|
65
|
+
if (inner.k === 'var' && cells.has(inner.name)) {
|
|
66
|
+
return inner;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return e;
|
|
70
|
+
};
|
|
71
|
+
const stmt = (s: Stmt): Stmt => {
|
|
72
|
+
switch (s.k) {
|
|
73
|
+
case 'assign': {
|
|
74
|
+
const value = expr(s.value);
|
|
75
|
+
return cells.has(s.name) && pointerCellStore(s) !== undefined
|
|
76
|
+
? { ...s, value: integerValue(value, s.name) }
|
|
77
|
+
: { ...s, value };
|
|
78
|
+
}
|
|
79
|
+
case 'if':
|
|
80
|
+
return { ...s, cond: expr(s.cond), then: s.then.map(stmt), else: s.else.map(stmt) };
|
|
81
|
+
case 'while':
|
|
82
|
+
case 'dowhile':
|
|
83
|
+
return { ...s, cond: expr(s.cond), body: s.body.map(stmt) };
|
|
84
|
+
case 'for':
|
|
85
|
+
return { ...s, init: stmt(s.init), cond: expr(s.cond), inc: stmt(s.inc), body: s.body.map(stmt) };
|
|
86
|
+
case 'switch':
|
|
87
|
+
return {
|
|
88
|
+
...s,
|
|
89
|
+
scrutinee: expr(s.scrutinee),
|
|
90
|
+
cases: s.cases.map((c) => ({ ...c, body: c.body.map(stmt) })),
|
|
91
|
+
default: s.default?.map(stmt),
|
|
92
|
+
};
|
|
93
|
+
default:
|
|
94
|
+
return mapStmtExprs(s, expr);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
return { sfn: { ...sfn, body: sfn.body.map(stmt) }, needsProof: true };
|
|
98
|
+
}
|
package/src/l3/mentions.ts
CHANGED
|
@@ -90,7 +90,7 @@ export function localMentions(sfn: SFn): Map<string, Mentions> {
|
|
|
90
90
|
}
|
|
91
91
|
for (const e of stmtExprs(s)) {
|
|
92
92
|
walkExpr(e, (x, isIndexBase) => {
|
|
93
|
-
if (x.k === 'var' || x.k === 'addr') {
|
|
93
|
+
if (x.k === 'var' || x.k === 'addr' || x.k === 'postincr') {
|
|
94
94
|
const m = seen(x.name, at);
|
|
95
95
|
if (m) {
|
|
96
96
|
if (x.k === 'addr') {
|
|
@@ -100,6 +100,11 @@ export function localMentions(sfn: SFn): Map<string, Mentions> {
|
|
|
100
100
|
} else {
|
|
101
101
|
m.otherUses++;
|
|
102
102
|
}
|
|
103
|
+
// `v++` reads AND writes, so it counts on both sides. `topAssignAt`/`constValue` stay
|
|
104
|
+
// untouched: they describe a top-level `assign` STATEMENT, which this is not.
|
|
105
|
+
if (x.k === 'postincr') {
|
|
106
|
+
m.assigns++;
|
|
107
|
+
}
|
|
103
108
|
}
|
|
104
109
|
}
|
|
105
110
|
});
|
|
@@ -126,8 +131,9 @@ export function localMentions(sfn: SFn): Map<string, Mentions> {
|
|
|
126
131
|
* rewriting and go stale.
|
|
127
132
|
*
|
|
128
133
|
* TOTAL over the vocabulary by construction: `assign` is the only `Stmt` carrying a bare name and
|
|
129
|
-
* `var`/`addr` the only `Expr`s, and both walks are derived from
|
|
130
|
-
* `for`'s init and inc, a `switch`'s scrutinee, its cases and its
|
|
134
|
+
* `var`/`addr`/`postincr` the only `Expr`s, and both walks are derived from
|
|
135
|
+
* `stmtChildren`/`stmtExprs` — so a `for`'s init and inc, a `switch`'s scrutinee, its cases and its
|
|
136
|
+
* default are all covered. */
|
|
131
137
|
function scanMentions(stmts: readonly Stmt[], names: ReadonlySet<string>, first: boolean): Set<string> {
|
|
132
138
|
// TWO FLAT SWEEPS, not one expression walk per nesting level. `walkExprs` already descends
|
|
133
139
|
// `stmtChildren` (ast.ts), so calling it per statement from inside a recursion that ALSO
|
|
@@ -149,7 +155,7 @@ function scanMentions(stmts: readonly Stmt[], names: ReadonlySet<string>, first:
|
|
|
149
155
|
stack.push(...stmtChildren(s));
|
|
150
156
|
}
|
|
151
157
|
for (const e of walkExprs(body)) {
|
|
152
|
-
if ((e.k === 'var' || e.k === 'addr') && names.has(e.name)) {
|
|
158
|
+
if ((e.k === 'var' || e.k === 'addr' || e.k === 'postincr') && names.has(e.name)) {
|
|
153
159
|
found.add(e.name);
|
|
154
160
|
if (first) {
|
|
155
161
|
return found;
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
// L3 stacked variations `/narrow-decl`, `/narrow-load` and `/narrow-read`: a named narrow value
|
|
2
|
+
// declared at its width. `/narrow-decl` takes the narrowing cast at the local's one write,
|
|
3
|
+
// `/narrow-load` a write that is a narrow memory read, which has no cast to drop, and `/narrow-read`
|
|
4
|
+
// the narrowing at every one of its reads:
|
|
5
|
+
//
|
|
6
|
+
// s32 v; v = (u8)(x - 1); … v … becomes u8 v; v = x - 1; … v … (/narrow-decl)
|
|
7
|
+
// s32 v; v = p[3]; … v … becomes u8 v; v = p[3]; … v … (/narrow-load)
|
|
8
|
+
// s32 v; v = f(); … (u8)v … (u8)v … becomes u8 v; v = f(); … v … v … (/narrow-read)
|
|
9
|
+
//
|
|
10
|
+
// The two spellings compute the same C value at every read. The declaration truncates where the
|
|
11
|
+
// cast did, or stores a value already of its type, and a `u8`, `s8`, `u16` or `s16` read is
|
|
12
|
+
// promoted to `int`, which is what an `s32` read already is. They are not the same program to gcc
|
|
13
|
+
// 2.9's front end. `get_narrower` (gcc/tree.c:4516) sees through the promotion of a narrow VARIABLE
|
|
14
|
+
// and not through an `int` that a cast was assigned to, so for an unsigned narrow one
|
|
15
|
+
// `shorten_compare` (gcc/c-common.c:1158) makes a compare against a constant unsigned and folds
|
|
16
|
+
// `>= 0`, `/` and `%` are shortened to their unsigned helpers (gcc/c-typeck.c:2036-2041, :2079-2090),
|
|
17
|
+
// and `>>` becomes a logical shift (gcc/c-typeck.c:2119). Without such an operator the object can
|
|
18
|
+
// still differ: `convert_to_integer` pushes a narrow store's truncation into an AND's operands
|
|
19
|
+
// (gcc/convert.c:278-284), and a `u8` local is a pseudo PROMOTE_MODE already zero-extended
|
|
20
|
+
// (gcc/thumb.h:344, gcc/stmt.c:3318-3323), whose low byte is free to read (gcc/expr.c:830-836),
|
|
21
|
+
// where an `s32` one takes a fresh byte pseudo and a register copy. Compiled with agbcc
|
|
22
|
+
// `-O2 -mthumb-interwork -fhex-asm -fprologue-bugfix`,
|
|
23
|
+
//
|
|
24
|
+
// v = g.c - 1; h.d = (v & 1) + rnd() % (5 - v) + 1;
|
|
25
|
+
//
|
|
26
|
+
// copies `v` before the AND under `s32 v` (`mov r1, #1; add r4, r5, #0; and r4, r4, r1`) and
|
|
27
|
+
// operates on it in place under `u8 v` (`mov r5, #1; and r5, r5, r4`), which is what
|
|
28
|
+
// `kleod:sub_0803E8CC` holds; over `v | 2`, `v ^ 2`, `v + 3` and `v * 3` the two compile alike.
|
|
29
|
+
// IDO, KMC gcc and gcc 2.7.2 each emit different code for that pair as well, and every mwcc
|
|
30
|
+
// build does for a compare of the narrowed value. Which one the
|
|
31
|
+
// source declared is not in the asm, so the differ referees, and the candidate it is derived onto
|
|
32
|
+
// stays in the fan. A load written to a `u8` local is loaded where it is written, and one written to
|
|
33
|
+
// an `s32` local where it is first read (two byte reads held across two calls, compiled in the
|
|
34
|
+
// matching suite: `pokeemerald:LoadMonInfo`).
|
|
35
|
+
//
|
|
36
|
+
// At the reads, `(u8)(s32)e`, `(u8)(u32)e` and `(u8)e` keep the same low byte of any integer `e`,
|
|
37
|
+
// so dropping each read's cast keeps its value, whichever write stored it. The pair compiles
|
|
38
|
+
// differently by where the extension lands: a `u8` local holding a call's result is zero-extended
|
|
39
|
+
// as it is stored, right after the `bl`, where `(u8)v` extends at the read, after whatever ran in
|
|
40
|
+
// between (`pokeemerald:RtcGetDayCount`, three results passed on as `u8`s).
|
|
41
|
+
//
|
|
42
|
+
// Three STACKED variations (rank-variations.ts), derived onto every other candidate's tree: the
|
|
43
|
+
// width of a declaration is orthogonal to what every respell variation outside that table changes,
|
|
44
|
+
// and the row that needs it needs it on top of `/offmember`. Three, not one, because a function
|
|
45
|
+
// holding a local narrowed at its cast write, a local written by a narrow load and a local narrowed
|
|
46
|
+
// at its reads may need any combination of the kinds narrowed. Each rewrites every local it admits.
|
|
47
|
+
// They do not commute: a cast that is one local's write and another's read is taken by whichever
|
|
48
|
+
// runs first, and so is a load-written local whose reads are all narrowed. So every subset of the
|
|
49
|
+
// three is its own candidate, and in one holding a write-side member and `/narrow-read` the
|
|
50
|
+
// write side runs first and takes the local.
|
|
51
|
+
//
|
|
52
|
+
// KNOWN GAP, and it is the price of deciding at L3 rather than where the value is named: which
|
|
53
|
+
// spelling a call's result had shows only in where its extension lands, right after its `bl` or
|
|
54
|
+
// just before its read, and structure/analysis.ts settles that per local when it names the call and
|
|
55
|
+
// inlines the extension into the read. So two read-side locals declared differently (`s32 y` cast
|
|
56
|
+
// at its read beside `u8 m`) have no candidate, as each entry narrows both or neither; here that
|
|
57
|
+
// would take 2^k candidates over k such locals. Nor has `s32 v = (u8)f()` once another such value
|
|
58
|
+
// is held across its call, which then compiles like neither spelling; held alone it compiles like
|
|
59
|
+
// `u8 v`, which /narrow-read reaches.
|
|
60
|
+
//
|
|
61
|
+
// REFUSED, each because the two spellings would stop computing the same value or would not build:
|
|
62
|
+
// • a local that is not a 32-bit integer;
|
|
63
|
+
// • a local whose address is taken, or that is volatile, a frame object, uninitialized, or homed
|
|
64
|
+
// in a stack slot. Each of those is an object in memory, whose width is its access width;
|
|
65
|
+
// • a write of a constant that fits neither the signed nor the unsigned type of the narrow width,
|
|
66
|
+
// which gcc warns of when it converts one implicitly (gcc/c-common.c:849-861, :870-895) and a
|
|
67
|
+
// `-Werror` build refuses;
|
|
68
|
+
// • /narrow-decl, /narrow-load: a `u32` local. Its reads stay bare, and a `u32` read is unsigned,
|
|
69
|
+
// so narrowing it would turn its compares, divisions and shifts into signed ones;
|
|
70
|
+
// • /narrow-decl, /narrow-load: a local written more than once, or by `v++`, where a later write
|
|
71
|
+
// could store a value the narrow declaration would truncate and a bare read would not;
|
|
72
|
+
// • /narrow-decl: a local whose one write is a `for` loop's init, or that is not an integer
|
|
73
|
+
// narrowed to a narrower integer. A call's operand is not known to be an integer (its callee
|
|
74
|
+
// may return a pointer), so `(u8)f()` is refused too;
|
|
75
|
+
// • /narrow-load: a local whose one write is a `for` loop's init, or is no `p[i]` or member read
|
|
76
|
+
// of a narrower integer type. A narrow variable (`v = a1` of a `u8 a1`) is refused: no row has
|
|
77
|
+
// needed it;
|
|
78
|
+
// • /narrow-read: a read that is not a narrowing cast (a bare `v`, an index base, a `v++`), reads
|
|
79
|
+
// cast to two widths or two signednesses, a write in a `for` loop's init or step, and a write
|
|
80
|
+
// whose value is a pointer, a float or of no known type. A call is admitted: its value already
|
|
81
|
+
// converts implicitly to the 32-bit local, and narrowing changes only the width of that
|
|
82
|
+
// conversion, never whether C performs one. A `u32` local and one written more than once are
|
|
83
|
+
// admitted, because every read already narrows each value stored to the one width.
|
|
84
|
+
import type { IrType } from '../ir/types';
|
|
85
|
+
import {
|
|
86
|
+
type Expr,
|
|
87
|
+
type SFn,
|
|
88
|
+
type Stmt,
|
|
89
|
+
mapExprChildren,
|
|
90
|
+
mapStmtExprs,
|
|
91
|
+
mapStmtLists,
|
|
92
|
+
stmtLists,
|
|
93
|
+
walkExprs,
|
|
94
|
+
} from './ast';
|
|
95
|
+
import { type Mentions, localMentions, readsOf } from './mentions';
|
|
96
|
+
import { declaredTypes, exprCType } from './typing';
|
|
97
|
+
|
|
98
|
+
type Narrowing = Extract<Expr, { k: 'cast' }> & { to: Extract<IrType, { kind: 'int' }> };
|
|
99
|
+
|
|
100
|
+
/** An integer narrowed to a narrower integer. A pointer or a float operand keeps its cast: C converts
|
|
101
|
+
* neither to an integer by assignment alone. */
|
|
102
|
+
const isNarrowing = (e: Expr, env: ReturnType<typeof declaredTypes>): e is Narrowing =>
|
|
103
|
+
e.k === 'cast' && !e.volatile && e.to.kind === 'int' && e.to.width < 32 && exprCType(e.e, env)?.kind === 'int';
|
|
104
|
+
|
|
105
|
+
/** The narrower integer type a memory read (`p[i]`, `*p`, a member) already has, which a local it is
|
|
106
|
+
* assigned to may be declared at with nothing to drop. */
|
|
107
|
+
const narrowLoad = (e: Expr, env: ReturnType<typeof declaredTypes>): Narrowing['to'] | undefined => {
|
|
108
|
+
if (e.k !== 'index' && e.k !== 'field') {
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
const t = exprCType(e, env);
|
|
112
|
+
return t?.kind === 'int' && t.width < 32 ? (t as Narrowing['to']) : undefined;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/** `(T)v`, the read of `name` narrowed to the narrower integer T. */
|
|
116
|
+
const isReadNarrowing = (e: Expr, name: string): e is Narrowing =>
|
|
117
|
+
e.k === 'cast' && !e.volatile && e.to.kind === 'int' && e.to.width < 32 && e.e.k === 'var' && e.e.name === name;
|
|
118
|
+
|
|
119
|
+
/** The one type every read of `name` is narrowed to, or undefined when a read is not narrowed
|
|
120
|
+
* or two reads are narrowed differently. `reads` counts every read however spelled, so a read
|
|
121
|
+
* that is not a narrowing leaves the count short. */
|
|
122
|
+
function readNarrowing(body: Stmt[], name: string, reads: number): Narrowing['to'] | undefined {
|
|
123
|
+
let to: Narrowing['to'] | undefined;
|
|
124
|
+
let narrowed = 0;
|
|
125
|
+
for (const e of walkExprs(body)) {
|
|
126
|
+
if (!isReadNarrowing(e, name)) {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (to !== undefined && (to.width !== e.to.width || to.signed !== e.to.signed)) {
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
to = e.to;
|
|
133
|
+
narrowed++;
|
|
134
|
+
}
|
|
135
|
+
return narrowed === reads ? to : undefined;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** The value of every assignment statement to `name`, found the way the write-side rewrite finds
|
|
139
|
+
* them: through statement lists, so a `for` init or step is not one. */
|
|
140
|
+
function writtenValues(body: Stmt[], name: string): Expr[] {
|
|
141
|
+
const values: Expr[] = [];
|
|
142
|
+
const walk = (list: Stmt[]): void => {
|
|
143
|
+
for (const s of list) {
|
|
144
|
+
if (s.k === 'assign' && s.name === name) {
|
|
145
|
+
values.push(s.value);
|
|
146
|
+
}
|
|
147
|
+
stmtLists(s).forEach(walk);
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
walk(body);
|
|
151
|
+
return values;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** `body` with every `(T)name` read spelled `name`. */
|
|
155
|
+
const dropReadCasts = (body: Stmt[], name: string): Stmt[] => {
|
|
156
|
+
const expr = (e: Expr): Expr => (isReadNarrowing(e, name) ? e.e : mapExprChildren(e, expr));
|
|
157
|
+
return body.map((s) => mapStmtExprs(s, expr));
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
/** The local's admission common to both sides: a 32-bit integer in no memory. */
|
|
161
|
+
const admissible = (l: SFn['locals'][number], m: Mentions | undefined): m is Mentions =>
|
|
162
|
+
m !== undefined &&
|
|
163
|
+
m.addrTaken === 0 &&
|
|
164
|
+
l.type.kind === 'int' &&
|
|
165
|
+
l.type.width === 32 &&
|
|
166
|
+
!l.volatile &&
|
|
167
|
+
!l.frame &&
|
|
168
|
+
!l.uninit &&
|
|
169
|
+
!l.slots;
|
|
170
|
+
|
|
171
|
+
/** Whether storing `value` into an `int` of `to`'s width converts without gcc's constant warning: a
|
|
172
|
+
* constant must fit the signed or the unsigned type of that width. */
|
|
173
|
+
const storesQuietly = (value: Expr, to: Narrowing['to']): boolean =>
|
|
174
|
+
value.k !== 'const' || (value.value >= -(2 ** (to.width - 1)) && value.value < 2 ** to.width);
|
|
175
|
+
|
|
176
|
+
/** `sfn` with each local in `narrowed` declared at its width, or null when it is empty. */
|
|
177
|
+
const declared = (sfn: SFn, body: Stmt[], narrowed: Map<string, IrType>): SFn | null =>
|
|
178
|
+
narrowed.size === 0
|
|
179
|
+
? null
|
|
180
|
+
: { ...sfn, body, locals: sfn.locals.map((l) => ({ ...l, type: narrowed.get(l.name) ?? l.type })) };
|
|
181
|
+
|
|
182
|
+
/** What a write-side variation makes of a local's one write: the narrow type to declare the local
|
|
183
|
+
* at and the value to store, or undefined when it does not take that write. */
|
|
184
|
+
type WriteNarrowing = (
|
|
185
|
+
value: Expr,
|
|
186
|
+
env: ReturnType<typeof declaredTypes>,
|
|
187
|
+
) => { to: Narrowing['to']; value: Expr } | undefined;
|
|
188
|
+
|
|
189
|
+
/** A narrowing cast, dropped. */
|
|
190
|
+
const castWrite: WriteNarrowing = (value, env) =>
|
|
191
|
+
isNarrowing(value, env) && storesQuietly(value.e, value.to) ? { to: value.to, value: value.e } : undefined;
|
|
192
|
+
|
|
193
|
+
/** A narrow memory read, kept as written. */
|
|
194
|
+
const loadWrite: WriteNarrowing = (value, env) => {
|
|
195
|
+
const to = narrowLoad(value, env);
|
|
196
|
+
return to === undefined ? undefined : { to, value };
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
/** The tree with every admitted local whose one write `take` takes narrowed at that write, or null
|
|
200
|
+
* when none is. */
|
|
201
|
+
function narrowAtWrite(sfn: SFn, take: WriteNarrowing): SFn | null {
|
|
202
|
+
const mentions = localMentions(sfn);
|
|
203
|
+
const env = declaredTypes(sfn);
|
|
204
|
+
let body = sfn.body;
|
|
205
|
+
const narrowed = new Map<string, IrType>();
|
|
206
|
+
for (const l of sfn.locals) {
|
|
207
|
+
const m = mentions.get(l.name);
|
|
208
|
+
if (!admissible(l, m) || m.assigns !== 1 || l.type.kind !== 'int' || !l.type.signed) {
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
let to: IrType | undefined;
|
|
212
|
+
const rewrite = (list: Stmt[]): Stmt[] =>
|
|
213
|
+
list.map((s) => {
|
|
214
|
+
const taken = s.k === 'assign' && s.name === l.name ? take(s.value, env) : undefined;
|
|
215
|
+
if (taken !== undefined) {
|
|
216
|
+
to = taken.to;
|
|
217
|
+
return { ...s, value: taken.value };
|
|
218
|
+
}
|
|
219
|
+
return mapStmtLists(s, rewrite);
|
|
220
|
+
});
|
|
221
|
+
const next = rewrite(body);
|
|
222
|
+
if (to !== undefined) {
|
|
223
|
+
body = next;
|
|
224
|
+
narrowed.set(l.name, to);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return declared(sfn, body, narrowed);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** `/narrow-decl`: the tree with every admitted local narrowed at its cast write, or null when none is. */
|
|
231
|
+
export const narrowDeclarations = (sfn: SFn): SFn | null => narrowAtWrite(sfn, castWrite);
|
|
232
|
+
|
|
233
|
+
/** `/narrow-load`: the tree with every admitted local written by a narrow memory read declared at that
|
|
234
|
+
* read's type, or null when none is. */
|
|
235
|
+
export const narrowLoadDeclarations = (sfn: SFn): SFn | null => narrowAtWrite(sfn, loadWrite);
|
|
236
|
+
|
|
237
|
+
/** `/narrow-read`: the tree with every admitted local narrowed at its reads, or null when none is. */
|
|
238
|
+
export function narrowReadDeclarations(sfn: SFn): SFn | null {
|
|
239
|
+
const mentions = localMentions(sfn);
|
|
240
|
+
const env = declaredTypes(sfn);
|
|
241
|
+
let body = sfn.body;
|
|
242
|
+
const narrowed = new Map<string, IrType>();
|
|
243
|
+
for (const l of sfn.locals) {
|
|
244
|
+
const m = mentions.get(l.name);
|
|
245
|
+
if (!admissible(l, m)) {
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
const written = writtenValues(body, l.name);
|
|
249
|
+
if (written.length !== m.assigns || written.some((w) => w.k !== 'call' && exprCType(w, env)?.kind !== 'int')) {
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
const read = readNarrowing(body, l.name, readsOf(m));
|
|
253
|
+
if (read !== undefined && written.every((w) => storesQuietly(w, read))) {
|
|
254
|
+
body = dropReadCasts(body, l.name);
|
|
255
|
+
narrowed.set(l.name, read);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return declared(sfn, body, narrowed);
|
|
259
|
+
}
|
package/src/l3/nearbase.ts
CHANGED
|
@@ -11,11 +11,13 @@
|
|
|
11
11
|
//
|
|
12
12
|
// SCOPE (decline over approximate): cluster MEMBERSHIP comes from CONST deref bases only (a
|
|
13
13
|
// struct-pointer cast base and everything inside a dot-form field subtree keep their spelling —
|
|
14
|
-
// their stride is the struct's, not a byte's
|
|
14
|
+
// their stride is the struct's, not a byte's — and a `volatile`-qualified base keeps its spelling
|
|
15
|
+
// and stays out of every cluster, since a `u8 *` base local would read and write the device
|
|
16
|
+
// plainly); a cluster needs at least two DISTINCT addresses
|
|
15
17
|
// within the target's declared derivation reach of its lowest (TargetDescription nearBaseSpan —
|
|
16
18
|
// beyond it the derive costs more than the pool word it saves); every access the walk visits
|
|
17
|
-
// rewrites, so a cluster splits only across the field-subtree
|
|
18
|
-
// boundaries. A member basecse
|
|
19
|
+
// rewrites, so a cluster splits only across the field-subtree, struct-pointer-cast and
|
|
20
|
+
// `volatile` boundaries. A member basecse
|
|
19
21
|
// already hoisted arrives as a `var` base and is invisible here — the reused-base and
|
|
20
22
|
// neighbor-base spellings stay separate candidates. Once a cluster HAS formed, a bare const
|
|
21
23
|
// VALUE inside its window re-spells too, as `(s32)(b + off)` — the address of a cell handed to
|
|
@@ -24,7 +26,7 @@
|
|
|
24
26
|
// only coincidentally lands in the window, which is the stated cost of the variation (the `s32` cast
|
|
25
27
|
// assumes addresses below 2^31, true of every target that declares nearBaseSpan today). Declines
|
|
26
28
|
// (null) when no cluster forms.
|
|
27
|
-
import { baseConst } from './address';
|
|
29
|
+
import { baseConst, qualifiedBase } from './address';
|
|
28
30
|
import type { Expr, SFn } from './ast';
|
|
29
31
|
import { mapExprChildren, mapStmtExprs } from './ast';
|
|
30
32
|
import { type BaseInit, nameAllocator, placeBaseLocals } from './hoist';
|
|
@@ -46,7 +48,7 @@ export function nearBaseClusters(sfn: SFn, span: number): SFn | null {
|
|
|
46
48
|
return e; // rewrite refuses these subtrees, so collecting under them would seed a cluster
|
|
47
49
|
}
|
|
48
50
|
const m = mapExprChildren(e, collect);
|
|
49
|
-
if (m.k === 'index') {
|
|
51
|
+
if (m.k === 'index' && !qualifiedBase(m.base)) {
|
|
50
52
|
const c = baseConst(m.base);
|
|
51
53
|
if (c !== null) {
|
|
52
54
|
addrs.add(c);
|
|
@@ -92,6 +94,9 @@ export function nearBaseClusters(sfn: SFn, span: number): SFn | null {
|
|
|
92
94
|
if (e.k === 'field') {
|
|
93
95
|
return e;
|
|
94
96
|
}
|
|
97
|
+
if (e.k === 'index' && qualifiedBase(e.base)) {
|
|
98
|
+
return { ...e, idx: rewrite(e.idx) }; // the whole cast chain stays, so the access stays volatile
|
|
99
|
+
}
|
|
95
100
|
if (e.k === 'index') {
|
|
96
101
|
const c = baseConst(e.base);
|
|
97
102
|
const lo = c !== null ? baseOf.get(c) : undefined;
|