@asmlift/core 0.9.0 → 0.11.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 +25 -24
- package/package.json +1 -1
- package/src/aggregate.ts +149 -0
- package/src/backend/c.ts +2 -0
- package/src/backend/cfamily.ts +197 -61
- package/src/backend/cpp.ts +3 -1
- package/src/backend/lines.ts +10 -0
- package/src/backend/pascal.ts +47 -16
- package/src/cdecl/parse.ts +21 -0
- package/src/contracts.ts +246 -36
- package/src/declare.ts +69 -20
- package/src/frontend/arm-body.ts +155 -0
- package/src/frontend/asmdata.ts +9 -2
- package/src/frontend/call-plan.ts +1117 -61
- package/src/frontend/callee-body.ts +370 -0
- package/src/frontend/dead-reads.ts +628 -0
- package/src/frontend/device-pins.ts +33 -4
- package/src/frontend/emit.ts +4 -3
- package/src/frontend/fpu.ts +224 -17
- package/src/frontend/frame-objects.ts +1689 -206
- package/src/frontend/frontend.ts +84 -3
- package/src/frontend/high-half.ts +53 -4
- package/src/frontend/jump-table.ts +545 -0
- package/src/frontend/local-object.ts +158 -32
- package/src/frontend/mips.ts +2296 -217
- package/src/frontend/own-struct-return.ts +500 -0
- package/src/frontend/ppc.ts +2748 -245
- package/src/frontend/register-struct-call.ts +399 -0
- package/src/frontend/reloc-symbol.ts +22 -7
- package/src/frontend/ssa.ts +1721 -43
- package/src/frontend/stackargs.ts +199 -72
- package/src/frontend/thumb-entry.ts +340 -0
- package/src/frontend/thumb.ts +2400 -325
- package/src/ir/alias.ts +65 -2
- package/src/ir/bits.ts +1699 -4
- package/src/ir/core.ts +254 -9
- package/src/ir/discipline.ts +86 -21
- package/src/ir/entry.ts +157 -0
- package/src/{structure → ir}/loops.ts +6 -2
- package/src/ir/opcodes.ts +137 -9
- package/src/ir/parse.ts +1 -0
- package/src/ir/simplify.ts +61 -10
- package/src/ir/types.ts +86 -2
- package/src/ir/verify.ts +96 -11
- package/src/l3/argbase.ts +2 -0
- package/src/l3/ast.ts +582 -18
- package/src/l3/basecse.ts +68 -15
- package/src/l3/caseendgoto.ts +223 -0
- package/src/l3/coalesce.ts +42 -14
- package/src/l3/constinit.ts +410 -0
- package/src/l3/dce.ts +53 -9
- package/src/l3/initfirst.ts +42 -11
- package/src/l3/intcell.ts +2 -1
- package/src/l3/narrowdecl.ts +77 -36
- package/src/l3/pollguard.ts +8 -2
- package/src/l3/ptrcell.ts +33 -34
- package/src/l3/ptrfield.ts +123 -8
- package/src/l3/regspell.ts +11 -4
- package/src/l3/reindex.ts +295 -99
- package/src/l3/scopebase.ts +2 -0
- package/src/l3/storage.ts +98 -1
- package/src/l3/symbol-refs.ts +84 -6
- package/src/l3/tailmerge.ts +6 -2
- package/src/l3/typing.ts +108 -15
- package/src/l3/unreduce.ts +6 -5
- package/src/l3/volatileptr.ts +17 -54
- package/src/mangle.ts +30 -0
- package/src/pattern/engine.ts +361 -73
- package/src/pipeline.ts +1039 -38
- package/src/proto-context.ts +1039 -45
- package/src/proto.ts +555 -40
- package/src/raise/aggmembers.ts +290 -0
- package/src/raise/arrays.ts +5 -1
- package/src/raise/const.ts +25 -0
- package/src/raise/declared-volatile.ts +207 -31
- package/src/raise/divpow2.ts +192 -0
- package/src/raise/floathelpers.ts +254 -33
- package/src/raise/gvn.ts +3 -3
- package/src/raise/latch.ts +162 -6
- package/src/raise/memberarrays.ts +10 -4
- package/src/raise/offsetnames.ts +422 -36
- package/src/raise/pairparams.ts +33 -0
- package/src/raise/paramwidth.ts +196 -6
- package/src/raise/pre-recovery.ts +107 -21
- package/src/raise/pre-structure.ts +73 -0
- package/src/raise/recover.ts +12 -5
- package/src/raise/reroll.ts +674 -0
- package/src/raise/shortcircuit.ts +217 -34
- package/src/raise/softdiv.ts +1 -1
- package/src/raise/storesign.ts +153 -0
- package/src/raise/struct-arrays.ts +30 -11
- package/src/raise/structs.ts +396 -33
- package/src/raise/truncload.ts +4 -3
- package/src/raise/unpre.ts +594 -0
- package/src/raise/unthread.ts +400 -0
- package/src/rank-declare.ts +118 -12
- package/src/rank-variations.ts +186 -35
- package/src/rank.ts +536 -68
- package/src/runtime-helpers.ts +140 -22
- package/src/structure/analysis.ts +217 -8
- package/src/structure/arith-ops.ts +23 -0
- package/src/structure/cross-jumped-tail.ts +216 -0
- package/src/structure/globalaccess.ts +157 -10
- package/src/structure/hazards.ts +16 -64
- package/src/structure/local-statics.ts +145 -9
- package/src/structure/namecoalesce.ts +1 -1
- package/src/structure/pointer-spelling.ts +1106 -0
- package/src/structure/redundant-test.ts +1 -1
- package/src/structure/structure.ts +6192 -1213
- package/src/structure/switch-recover.ts +583 -120
- package/src/structure/unfolded-branch-constant.ts +90 -0
- package/src/symbols.ts +58 -15
- package/src/target.ts +1028 -54
- package/src/trace.ts +49 -17
- package/src/variation-definitions.ts +333 -20
- package/src/variation-gates.ts +4 -0
- package/src/variation-tokens.ts +68 -3
package/src/l3/narrowdecl.ts
CHANGED
|
@@ -1,22 +1,25 @@
|
|
|
1
|
-
// L3 stacked variations `/narrow-decl` and `/narrow-read`: a named narrow value
|
|
2
|
-
// width. `/narrow-decl` takes the narrowing at the local's one write,
|
|
3
|
-
//
|
|
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:
|
|
4
5
|
//
|
|
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)
|
|
6
8
|
// s32 v; v = f(); … (u8)v … (u8)v … becomes u8 v; v = f(); … v … v … (/narrow-read)
|
|
7
9
|
//
|
|
8
10
|
// The two spellings compute the same C value at every read. The declaration truncates where the
|
|
9
|
-
// cast did, and a `u8`, `s8`, `u16` or `s16` read is
|
|
10
|
-
// already is. They are not the same program to gcc
|
|
11
|
-
// (gcc/tree.c:4516) sees through the promotion of a narrow VARIABLE
|
|
12
|
-
// a cast was assigned to, so for an unsigned narrow one
|
|
13
|
-
// makes a compare against a constant unsigned and folds
|
|
14
|
-
// unsigned helpers (gcc/c-typeck.c:2036-2041, :2079-2090),
|
|
15
|
-
// (gcc/c-typeck.c:2119). Without such an operator the object can
|
|
16
|
-
// pushes a narrow store's truncation into an AND's operands
|
|
17
|
-
// local is a pseudo PROMOTE_MODE already zero-extended
|
|
18
|
-
// whose low byte is free to read (gcc/expr.c:830-836),
|
|
19
|
-
// and a register copy. Compiled with agbcc
|
|
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`,
|
|
20
23
|
//
|
|
21
24
|
// v = g.c - 1; h.d = (v & 1) + rnd() % (5 - v) + 1;
|
|
22
25
|
//
|
|
@@ -26,7 +29,9 @@
|
|
|
26
29
|
// IDO, KMC gcc and gcc 2.7.2 each emit different code for that pair as well, and every mwcc
|
|
27
30
|
// build does for a compare of the narrowed value. Which one the
|
|
28
31
|
// source declared is not in the asm, so the differ referees, and the candidate it is derived onto
|
|
29
|
-
// stays in the fan.
|
|
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`).
|
|
30
35
|
//
|
|
31
36
|
// At the reads, `(u8)(s32)e`, `(u8)(u32)e` and `(u8)e` keep the same low byte of any integer `e`,
|
|
32
37
|
// so dropping each read's cast keeps its value, whichever write stored it. The pair compiles
|
|
@@ -34,13 +39,15 @@
|
|
|
34
39
|
// as it is stored, right after the `bl`, where `(u8)v` extends at the read, after whatever ran in
|
|
35
40
|
// between (`pokeemerald:RtcGetDayCount`, three results passed on as `u8`s).
|
|
36
41
|
//
|
|
37
|
-
//
|
|
38
|
-
// of a declaration is orthogonal to what every respell variation outside that table changes,
|
|
39
|
-
// the row that needs it needs it on top of `/offmember`.
|
|
40
|
-
// local narrowed at its write
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
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.
|
|
44
51
|
//
|
|
45
52
|
// KNOWN GAP, and it is the price of deciding at L3 rather than where the value is named: which
|
|
46
53
|
// spelling a call's result had shows only in where its extension lands, right after its `bl` or
|
|
@@ -58,13 +65,16 @@
|
|
|
58
65
|
// • a write of a constant that fits neither the signed nor the unsigned type of the narrow width,
|
|
59
66
|
// which gcc warns of when it converts one implicitly (gcc/c-common.c:849-861, :870-895) and a
|
|
60
67
|
// `-Werror` build refuses;
|
|
61
|
-
// • /narrow-decl: a `u32` local. Its reads stay bare, and a `u32` read is unsigned,
|
|
62
|
-
// it would turn its compares, divisions and shifts into signed ones;
|
|
63
|
-
// • /narrow-decl: a local written more than once, or by `v++`, where a later write
|
|
64
|
-
// value the narrow declaration would truncate and a bare read would not;
|
|
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;
|
|
65
72
|
// • /narrow-decl: a local whose one write is a `for` loop's init, or that is not an integer
|
|
66
73
|
// narrowed to a narrower integer. A call's operand is not known to be an integer (its callee
|
|
67
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;
|
|
68
78
|
// • /narrow-read: a read that is not a narrowing cast (a bare `v`, an index base, a `v++`), reads
|
|
69
79
|
// cast to two widths or two signednesses, a write in a `for` loop's init or step, and a write
|
|
70
80
|
// whose value is a pointer, a float or of no known type. A call is admitted: its value already
|
|
@@ -92,6 +102,16 @@ type Narrowing = Extract<Expr, { k: 'cast' }> & { to: Extract<IrType, { kind: 'i
|
|
|
92
102
|
const isNarrowing = (e: Expr, env: ReturnType<typeof declaredTypes>): e is Narrowing =>
|
|
93
103
|
e.k === 'cast' && !e.volatile && e.to.kind === 'int' && e.to.width < 32 && exprCType(e.e, env)?.kind === 'int';
|
|
94
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
|
+
|
|
95
115
|
/** `(T)v`, the read of `name` narrowed to the narrower integer T. */
|
|
96
116
|
const isReadNarrowing = (e: Expr, name: string): e is Narrowing =>
|
|
97
117
|
e.k === 'cast' && !e.volatile && e.to.kind === 'int' && e.to.width < 32 && e.e.k === 'var' && e.e.name === name;
|
|
@@ -159,8 +179,26 @@ const declared = (sfn: SFn, body: Stmt[], narrowed: Map<string, IrType>): SFn |
|
|
|
159
179
|
? null
|
|
160
180
|
: { ...sfn, body, locals: sfn.locals.map((l) => ({ ...l, type: narrowed.get(l.name) ?? l.type })) };
|
|
161
181
|
|
|
162
|
-
/**
|
|
163
|
-
|
|
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 {
|
|
164
202
|
const mentions = localMentions(sfn);
|
|
165
203
|
const env = declaredTypes(sfn);
|
|
166
204
|
let body = sfn.body;
|
|
@@ -173,14 +211,10 @@ export function narrowDeclarations(sfn: SFn): SFn | null {
|
|
|
173
211
|
let to: IrType | undefined;
|
|
174
212
|
const rewrite = (list: Stmt[]): Stmt[] =>
|
|
175
213
|
list.map((s) => {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
storesQuietly(s.value.e, s.value.to)
|
|
181
|
-
) {
|
|
182
|
-
to = s.value.to;
|
|
183
|
-
return { ...s, value: s.value.e };
|
|
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 };
|
|
184
218
|
}
|
|
185
219
|
return mapStmtLists(s, rewrite);
|
|
186
220
|
});
|
|
@@ -193,6 +227,13 @@ export function narrowDeclarations(sfn: SFn): SFn | null {
|
|
|
193
227
|
return declared(sfn, body, narrowed);
|
|
194
228
|
}
|
|
195
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
|
+
|
|
196
237
|
/** `/narrow-read`: the tree with every admitted local narrowed at its reads, or null when none is. */
|
|
197
238
|
export function narrowReadDeclarations(sfn: SFn): SFn | null {
|
|
198
239
|
const mentions = localMentions(sfn);
|
package/src/l3/pollguard.ts
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
// where the forms genuinely differ (the body runs at least once vs at least zero times), which
|
|
20
20
|
// is why it never wraps. Declines (null) when no empty do-while exists.
|
|
21
21
|
import type { Expr, SFn, Stmt } from './ast';
|
|
22
|
-
import { exprChildren, exprEquals, mapExprChildren, mapStmtLists, stmtChildren, stmtExprs } from './ast';
|
|
22
|
+
import { exprChildren, exprEquals, mapExprChildren, mapStmtLists, stmtChildren, stmtExprs, unionEvidence } from './ast';
|
|
23
23
|
|
|
24
24
|
export function pollGuards(sfn: SFn): SFn | null {
|
|
25
25
|
let changed = false;
|
|
@@ -140,7 +140,13 @@ export function pollReads(sfn: SFn): SFn | null {
|
|
|
140
140
|
condFoldable(w.cond, volatileLocals) &&
|
|
141
141
|
ownsEveryOccurrence(sfn.body, a.name)
|
|
142
142
|
) {
|
|
143
|
-
|
|
143
|
+
// The loop's re-read and the first read print alike; the folded read stands for both
|
|
144
|
+
// (ast.ts `unionEvidence`).
|
|
145
|
+
out.push({
|
|
146
|
+
k: 'while',
|
|
147
|
+
cond: subst(w.cond, a.name, unionEvidence(a.value, (w.body[0] as typeof a).value)),
|
|
148
|
+
body: [],
|
|
149
|
+
});
|
|
144
150
|
dropped.add(a.name);
|
|
145
151
|
i++;
|
|
146
152
|
continue;
|
package/src/l3/ptrcell.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
// L3: a POINTER CELL — a global the body stores a pointer into, and the one place that spelling is
|
|
2
2
|
// recognised.
|
|
3
3
|
//
|
|
4
|
-
// structure() spells a pointer value stored into a word global as `g = (void *)X`
|
|
5
|
-
// `intoPtrCell`), the store every pointer declaration of `g` takes.
|
|
6
|
-
//
|
|
4
|
+
// structure() spells a pointer value stored into a word global as `g = (void *)X`
|
|
5
|
+
// (structure/pointer-spelling.ts `intoPtrCell`), the store every pointer declaration of `g` takes.
|
|
6
|
+
// That store is built by `pointerCellValue` and read by `pointerCellStore`, both below; the one other
|
|
7
|
+
// writer of it, `legalizePointerCells`, converts an integer stored into a cell through the same
|
|
8
|
+
// function. Three readers act on it, and all three ask `pointerCells` rather than re-reading the cast:
|
|
7
9
|
//
|
|
8
10
|
// • l3/symbol-refs.ts declares a name-only cell `void *` in the candidate's own world, the type
|
|
9
11
|
// that store needs, and so each global the cell meets bare that nothing reads as an integer
|
|
@@ -15,11 +17,24 @@
|
|
|
15
17
|
// l3/symbol-refs.ts's header gives: a rewrite that drops the store must drop the fact with it.
|
|
16
18
|
import { type IrType, T } from '../ir/types';
|
|
17
19
|
import { declaredWidth } from '../proto';
|
|
18
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
type Expr,
|
|
22
|
+
type SFn,
|
|
23
|
+
type Stmt,
|
|
24
|
+
assignsIn,
|
|
25
|
+
exprChildren,
|
|
26
|
+
mapExprChildren,
|
|
27
|
+
taintOverAssigns,
|
|
28
|
+
walkExprs,
|
|
29
|
+
} from './ast';
|
|
19
30
|
import { declaredTypes, exprCType } from './typing';
|
|
20
31
|
|
|
21
|
-
/**
|
|
22
|
-
*
|
|
32
|
+
/** A value stored into a pointer cell: `(void *)X`, which every pointer declaration of the cell
|
|
33
|
+
* takes. */
|
|
34
|
+
export const pointerCellValue = (e: Expr): Expr => ({ k: 'cast', to: T.ptr(T.void()), e });
|
|
35
|
+
|
|
36
|
+
/** The bare name an `assign` stores a `(void *)` value into (`pointerCellValue`): a pointer cell,
|
|
37
|
+
* when no local binds it. */
|
|
23
38
|
export function pointerCellStore(s: Stmt): string | undefined {
|
|
24
39
|
return s.k === 'assign' && s.value.k === 'cast' && s.value.to.kind === 'ptr' && s.value.to.to.kind === 'void'
|
|
25
40
|
? s.name
|
|
@@ -28,18 +43,6 @@ export function pointerCellStore(s: Stmt): string | undefined {
|
|
|
28
43
|
|
|
29
44
|
const COMPARE = new Set(['<', '<=', '>', '>=', '==', '!=']);
|
|
30
45
|
|
|
31
|
-
const assigns = (body: Stmt[]): Extract<Stmt, { k: 'assign' }>[] => {
|
|
32
|
-
const out: Extract<Stmt, { k: 'assign' }>[] = [];
|
|
33
|
-
const visit = (s: Stmt): void => {
|
|
34
|
-
if (s.k === 'assign') {
|
|
35
|
-
out.push(s);
|
|
36
|
-
}
|
|
37
|
-
stmtChildren(s).forEach(visit);
|
|
38
|
-
};
|
|
39
|
-
body.forEach(visit);
|
|
40
|
-
return out;
|
|
41
|
-
};
|
|
42
|
-
|
|
43
46
|
/** Whether `e` reads a variable `pick` takes. */
|
|
44
47
|
const readsVar = (e: Expr, pick: (name: string) => boolean): boolean =>
|
|
45
48
|
(e.k === 'var' && pick(e.name)) || exprChildren(e).some((c) => readsVar(c, pick));
|
|
@@ -59,18 +62,12 @@ export function cellOffset(e: Expr, cells: ReadonlySet<string>): boolean {
|
|
|
59
62
|
/** The globals `body` stores a pointer into: a `(void *)` store, or the bare value of another one,
|
|
60
63
|
* or that value moved by integers (`cellOffset`). */
|
|
61
64
|
export function pointerCells(body: Stmt[], isGlobal: (name: string) => boolean): Set<string> {
|
|
62
|
-
const stores =
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
out.add(s.name);
|
|
69
|
-
grew = true;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
return out;
|
|
65
|
+
const stores = assignsIn(body).filter((s) => isGlobal(s.name));
|
|
66
|
+
return taintOverAssigns(
|
|
67
|
+
stores,
|
|
68
|
+
stores.filter((s) => pointerCellStore(s) !== undefined).map((s) => s.name),
|
|
69
|
+
(s, cells) => (s.value.k === 'var' && cells.has(s.value.name)) || cellOffset(s.value, cells),
|
|
70
|
+
);
|
|
74
71
|
}
|
|
75
72
|
|
|
76
73
|
/** The bare globals a pointer cell meets bare: compared with one or with its `cellOffset`, or
|
|
@@ -83,7 +80,7 @@ function meetsBare(body: Stmt[], isGlobal: (name: string) => boolean, cells: Rea
|
|
|
83
80
|
}
|
|
84
81
|
};
|
|
85
82
|
const isCell = (e: Expr): boolean => (e.k === 'var' && cells.has(e.name)) || cellOffset(e, cells);
|
|
86
|
-
for (const s of
|
|
83
|
+
for (const s of assignsIn(body)) {
|
|
87
84
|
if (cells.has(s.name)) {
|
|
88
85
|
meets(s.value);
|
|
89
86
|
}
|
|
@@ -230,6 +227,8 @@ function mapIntegerSlots(sfn: SFn, cells: ReadonlySet<string>, rules: SlotRules)
|
|
|
230
227
|
};
|
|
231
228
|
case 'break':
|
|
232
229
|
case 'continue':
|
|
230
|
+
case 'goto':
|
|
231
|
+
case 'caseEndGoto':
|
|
233
232
|
return s;
|
|
234
233
|
}
|
|
235
234
|
};
|
|
@@ -303,8 +302,8 @@ export function pointerPartners(sfn: SFn): Set<string> {
|
|
|
303
302
|
* goes `(u32)(u8 *)g` beside the cell's `(u32)g`: under a float declaration that is no C, as the
|
|
304
303
|
* bare comparison was not. KNOWN GAP: under an array or a function declaration that is the
|
|
305
304
|
* global's address, compared with no diagnostic, and IDO takes the integer read that made it no
|
|
306
|
-
* partner (`v1 = g`) with none either; structure.ts `globalWord` says why no
|
|
307
|
-
* word there.
|
|
305
|
+
* partner (`v1 = g`) with none either; structure/pointer-spelling.ts `globalWord` says why no
|
|
306
|
+
* spelling reads the word there.
|
|
308
307
|
*
|
|
309
308
|
* Run by the C-family backend's `emit` (backend/cfamily.ts), after every respell variation, so a
|
|
310
309
|
* respell reads the uses as the structurer spelled them and a store it removes takes the
|
|
@@ -350,7 +349,7 @@ export function legalizePointerCells(sfn: SFn): SFn {
|
|
|
350
349
|
intArg: (e) =>
|
|
351
350
|
e.k === 'bin' && cellSum(e) && exprCType(e, vt)?.kind === 'ptr' ? { k: 'cast', to: T.u(32), e } : asInt(e),
|
|
352
351
|
comparedGlobal: (e) => ({ k: 'cast', to: T.u(32), e: { k: 'cast', to: T.ptr(T.u(8)), e } }),
|
|
353
|
-
intoCell: (e) => (
|
|
352
|
+
intoCell: (e) => pointerCellValue(asBytes(e)),
|
|
354
353
|
pointerOffset: asBytes,
|
|
355
354
|
storedGlobal: () => {},
|
|
356
355
|
});
|
package/src/l3/ptrfield.ts
CHANGED
|
@@ -29,10 +29,24 @@
|
|
|
29
29
|
// `*(volatile s32 *)0x040000D4/D8/DC` device writes — `volatile` restricts what may be done with
|
|
30
30
|
// THOSE accesses and does not merge their type into the pointer's alias set.
|
|
31
31
|
//
|
|
32
|
-
// SEMANTICS ARE PRESERVED BY CONSTRUCTION on a target whose pointers are 32 bits
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
32
|
+
// SEMANTICS ARE PRESERVED BY CONSTRUCTION on a target whose pointers are 32 bits — WHILE NO STORE
|
|
33
|
+
// OF ANOTHER ALIAS SET REACHES THE FIELD'S BYTES: the declaration changes, and every read is
|
|
34
|
+
// wrapped in a cast back to the field's own recovered integer type, so each use computes the same
|
|
35
|
+
// value it did. The cast keeps the VALUE, not the read's alias set: the load is still of a pointer
|
|
36
|
+
// type, which is the very freedom this variation exists to give agbcc, and which it uses to read
|
|
37
|
+
// stale past an integer store to the same bytes. Two such stores are knowable from the tree, and
|
|
38
|
+
// each has a gate. Over a local array the body IS that store: the frame audit licenses a wider
|
|
39
|
+
// runtime index into those bytes as one integer type (frontend/frame-objects.ts `indexedWidth`),
|
|
40
|
+
// so `over-local-storage` refuses a field selected out of one. And once a local's address has LEFT
|
|
41
|
+
// the function (a call argument, a stored or returned value) while the function writes local
|
|
42
|
+
// storage, a pointer it obtains from anywhere but its params or a fixed address may be that same
|
|
43
|
+
// address handed back (`k(buf); p = get(); x = p->a; buf[i] = v;`), so `frame-escaped` refuses a
|
|
44
|
+
// field read through one. Past those, a store the asm shows is tied to the field's bytes only by
|
|
45
|
+
// the caller's or a global's aliasing, which the tree cannot see — which spelling the original had
|
|
46
|
+
// is then what the differ referees, and it is the only referee there.
|
|
47
|
+
//
|
|
48
|
+
// THE 32-BIT ASSUMPTION IS ASSERTED, NOT CHECKED — there is no pointer-width field on
|
|
49
|
+
// TargetDescription to check it against, and the
|
|
36
50
|
// assumption is already tower-wide (`l3/typing.ts`'s `ptrElemBytes` returns 4 for any pointee). On
|
|
37
51
|
// a 64-bit target this variation would change a struct's LAYOUT rather than only its spelling, so the
|
|
38
52
|
// width field is what that target's first row must add, and this note is where to start.
|
|
@@ -49,9 +63,11 @@
|
|
|
49
63
|
//
|
|
50
64
|
// GATE (PTR_FIELD_GATES, read once per field): the field's recovered type must be a 32-bit
|
|
51
65
|
// integer; it must never be a store LVALUE (the write side would need a cast on the value, which
|
|
52
|
-
// is a second question);
|
|
53
|
-
// already dereferences is one the recovery typed from its own use, not from a width)
|
|
54
|
-
//
|
|
66
|
+
// is a second question); it must never stand as the BASE of another access (a field the tree
|
|
67
|
+
// already dereferences is one the recovery typed from its own use, not from a width); and it must
|
|
68
|
+
// never be selected out of a local array's bytes (`over-local-storage`, the caveat above); and
|
|
69
|
+
// while a written local's address has escaped, it must be read through a param or a fixed address
|
|
70
|
+
// (`frame-escaped`, the same caveat). Nothing qualifying ⇒ decline (null).
|
|
55
71
|
//
|
|
56
72
|
// SCOPE, because both of this table's blind spots look exactly like a gate refusing. First, THERE
|
|
57
73
|
// IS NO PAD RULE, deliberately: `plan` builds a ctx only for fields the tree ACCESSES, so "a field
|
|
@@ -67,12 +83,16 @@ import {
|
|
|
67
83
|
type SFn,
|
|
68
84
|
type Stmt,
|
|
69
85
|
type StructType,
|
|
86
|
+
assignsIn,
|
|
70
87
|
mapExprChildren,
|
|
71
88
|
mapStmtExprs,
|
|
89
|
+
mentionedName,
|
|
72
90
|
stmtChildren,
|
|
91
|
+
taintOverAssigns,
|
|
73
92
|
walkExprs,
|
|
74
93
|
} from './ast';
|
|
75
94
|
import { type Gate, firstRejection } from './gates';
|
|
95
|
+
import { localAddresses, localStorageEscapesWritten, nameStorage } from './storage';
|
|
76
96
|
import { declaredTypes, exprCType } from './typing';
|
|
77
97
|
|
|
78
98
|
/** One recovered field as the gates read it — built only for fields the tree accesses. */
|
|
@@ -82,6 +102,12 @@ interface FieldCtx {
|
|
|
82
102
|
written: boolean;
|
|
83
103
|
/** the field's value stands as the base of an `index` or another `field` */
|
|
84
104
|
dereferenced: boolean;
|
|
105
|
+
/** some access to the field selects it out of a local array's bytes */
|
|
106
|
+
overLocalStorage: boolean;
|
|
107
|
+
/** some access reads the field through a pointer that is neither a param nor a fixed address */
|
|
108
|
+
obtainedPointer: boolean;
|
|
109
|
+
/** a local's address leaves the function, and the function writes local storage */
|
|
110
|
+
frameEscaped: boolean;
|
|
85
111
|
}
|
|
86
112
|
|
|
87
113
|
export const PTR_FIELD_GATES: readonly Gate<FieldCtx>[] = [
|
|
@@ -106,6 +132,26 @@ export const PTR_FIELD_GATES: readonly Gate<FieldCtx>[] = [
|
|
|
106
132
|
guardedBy: 'ptrfield.test.ts: a field standing as an access base declines',
|
|
107
133
|
rejects: (c) => c.dereferenced,
|
|
108
134
|
},
|
|
135
|
+
{
|
|
136
|
+
id: 'over-local-storage',
|
|
137
|
+
why:
|
|
138
|
+
"a local array's bytes are typed by the frame audit, which licenses a wider runtime index only " +
|
|
139
|
+
'while every access of one width is one integer type; a pointer field over them is an alias ' +
|
|
140
|
+
'set of its own, and agbcc at -O2 reads it stale past an integer store to the same bytes',
|
|
141
|
+
sound: true,
|
|
142
|
+
guardedBy: 'ptrfield.test.ts: a field selected out of a local array declines',
|
|
143
|
+
rejects: (c) => c.overLocalStorage,
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
id: 'frame-escaped',
|
|
147
|
+
why:
|
|
148
|
+
"once a written local's address has left the function, a pointer it obtains later may be that " +
|
|
149
|
+
'address handed back, and a pointer field read through it is then read stale past the integer ' +
|
|
150
|
+
'store to the same bytes',
|
|
151
|
+
sound: true,
|
|
152
|
+
guardedBy: 'ptrfield.test.ts: a field read through a re-obtained pointer after a frame address escaped declines',
|
|
153
|
+
rejects: (c) => c.frameEscaped && c.obtainedPointer,
|
|
154
|
+
},
|
|
109
155
|
];
|
|
110
156
|
|
|
111
157
|
/** the struct a `field` node selects from, or null */
|
|
@@ -122,6 +168,10 @@ const keyOf = (structName: string, field: string): string => `${structName}.${fi
|
|
|
122
168
|
* reads cast back to. */
|
|
123
169
|
function plan(sfn: SFn): Map<string, IrType> {
|
|
124
170
|
const vt = declaredTypes(sfn);
|
|
171
|
+
const storage = localStorageNames(sfn);
|
|
172
|
+
const frameEscaped = localStorageEscapesWritten(sfn);
|
|
173
|
+
const fixed = fixedPointer(sfn);
|
|
174
|
+
const { accessAddress } = localAddresses(sfn);
|
|
125
175
|
const seen = new Map<string, FieldCtx & { type: IrType }>();
|
|
126
176
|
const note = (base: Expr, name: string, edit: (c: FieldCtx) => void): void => {
|
|
127
177
|
const st = structOf(base, vt);
|
|
@@ -134,6 +184,9 @@ function plan(sfn: SFn): Map<string, IrType> {
|
|
|
134
184
|
word: declared.kind === 'int' && declared.width === 32,
|
|
135
185
|
written: false,
|
|
136
186
|
dereferenced: false,
|
|
187
|
+
overLocalStorage: false,
|
|
188
|
+
obtainedPointer: false,
|
|
189
|
+
frameEscaped,
|
|
137
190
|
type: declared,
|
|
138
191
|
};
|
|
139
192
|
edit(cur);
|
|
@@ -141,7 +194,14 @@ function plan(sfn: SFn): Map<string, IrType> {
|
|
|
141
194
|
};
|
|
142
195
|
for (const e of walkExprs(sfn.body)) {
|
|
143
196
|
if (e.k === 'field') {
|
|
144
|
-
note(e.base, e.name, () => {
|
|
197
|
+
note(e.base, e.name, (c) => {
|
|
198
|
+
if (reachesStorage(e.base, storage)) {
|
|
199
|
+
c.overLocalStorage = true;
|
|
200
|
+
}
|
|
201
|
+
if (!fixed(accessAddress(e))) {
|
|
202
|
+
c.obtainedPointer = true;
|
|
203
|
+
}
|
|
204
|
+
});
|
|
145
205
|
}
|
|
146
206
|
// a field standing as an access base is one the recovery typed from its own use
|
|
147
207
|
const inner = e.k === 'index' ? e.base : e.k === 'field' ? e.base : null;
|
|
@@ -163,6 +223,61 @@ function plan(sfn: SFn): Map<string, IrType> {
|
|
|
163
223
|
return out;
|
|
164
224
|
}
|
|
165
225
|
|
|
226
|
+
/** The locals whose value is a LOCAL OBJECT'S ADDRESS: every local array (the bytes the frame
|
|
227
|
+
* audit typed — `u8 sp0[n]`), then every local assigned an expression that reaches one, or a
|
|
228
|
+
* local's address, to a fixpoint (`p0 = (s32 *)sp0;` carries the bytes into `p0`). Over-wide by
|
|
229
|
+
* design: a local holding `sp0[i]` (a value read OUT of the bytes) is swept in too, which only
|
|
230
|
+
* ever refuses a variation. */
|
|
231
|
+
function localStorageNames(sfn: SFn): { names: Set<string>; locals: Set<string> } {
|
|
232
|
+
const locals = new Set(sfn.locals.map((l) => l.name));
|
|
233
|
+
const names = taintOverAssigns(
|
|
234
|
+
assignsIn(sfn.body),
|
|
235
|
+
sfn.locals.filter((l) => l.type.kind === 'array').map((l) => l.name),
|
|
236
|
+
(a, names) => locals.has(a.name) && reachesStorage(a.value, { names, locals }),
|
|
237
|
+
);
|
|
238
|
+
return { names, locals };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** Does the expression hold a pointer the function did not OBTAIN — one no escaped frame address
|
|
242
|
+
* can come back through? A constant address, a global's address, or a param nothing assigns (the
|
|
243
|
+
* caller's value, which predates this frame), under casts and offsets. Anything else — a local, a
|
|
244
|
+
* call's result, a value loaded from memory — may be the frame. */
|
|
245
|
+
function fixedPointer(sfn: SFn): (e: Expr) => boolean {
|
|
246
|
+
const storage = nameStorage(sfn);
|
|
247
|
+
const assigned = new Set(assignsIn(sfn.body).map((a) => a.name));
|
|
248
|
+
const fixed = (e: Expr): boolean => {
|
|
249
|
+
switch (e.k) {
|
|
250
|
+
case 'const':
|
|
251
|
+
return true;
|
|
252
|
+
case 'addr':
|
|
253
|
+
return storage.get(e.name) !== 'local' && storage.get(e.name) !== 'param';
|
|
254
|
+
case 'var':
|
|
255
|
+
return storage.get(e.name) === 'param' && !assigned.has(e.name);
|
|
256
|
+
case 'cast':
|
|
257
|
+
return fixed(e.e);
|
|
258
|
+
// An offset from a fixed pointer stays in the object it points into, but the tree does not
|
|
259
|
+
// say which operand is the pointer: in `p + off` with `p = get()` and `off` a param, the
|
|
260
|
+
// param is the fixed side and `p` may be the frame. So every operand must be fixed.
|
|
261
|
+
case 'bin':
|
|
262
|
+
return (e.op === '+' || e.op === '-') && fixed(e.l) && fixed(e.r);
|
|
263
|
+
default:
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
return fixed;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** does the expression name local storage, or take a local's address? */
|
|
271
|
+
function reachesStorage(
|
|
272
|
+
e: Expr,
|
|
273
|
+
storage: { readonly names: ReadonlySet<string>; readonly locals: ReadonlySet<string> },
|
|
274
|
+
): boolean {
|
|
275
|
+
return [...walkExprs([{ k: 'exprstmt', value: e }])].some((x) => {
|
|
276
|
+
const n = mentionedName(x);
|
|
277
|
+
return n !== undefined && (storage.names.has(n) || (x.k === 'addr' && storage.locals.has(n)));
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
166
281
|
/** Every `store` in the tree. ORDER-FREE by construction, which is what lets it use the shared
|
|
167
282
|
* `stmtChildren` (a switch's default sits at `defaultAt`, not last): its one caller only sets
|
|
168
283
|
* `written` on entries the preceding `walkExprs` pass already created — `note` builds an entry
|
package/src/l3/regspell.ts
CHANGED
|
@@ -101,6 +101,8 @@ function renameInStmt(s: Stmt, from: string, to: string): Stmt {
|
|
|
101
101
|
};
|
|
102
102
|
case 'break':
|
|
103
103
|
case 'continue':
|
|
104
|
+
case 'goto':
|
|
105
|
+
case 'caseEndGoto':
|
|
104
106
|
return s;
|
|
105
107
|
}
|
|
106
108
|
}
|
|
@@ -189,16 +191,21 @@ function condOtherMentions(cond: Expr, E: Expr, v: string): boolean {
|
|
|
189
191
|
};
|
|
190
192
|
return other ? mentions(other) : false;
|
|
191
193
|
}
|
|
192
|
-
/** cond compares E against a pure operand → same comparison reading the named var.
|
|
194
|
+
/** cond compares E against a pure operand → same comparison reading the named var.
|
|
195
|
+
* Both rebuilds below SPREAD the compare, so its `unsignedCmp` evidence (l3/ast.ts) rides along:
|
|
196
|
+
* the copy is declared at E's type, which keeps the compare's C meaning but not its asm meaning,
|
|
197
|
+
* and rank.ts's judgment of the finished tree (typing.ts `signedOverUnsignedCompare`) is what
|
|
198
|
+
* says whether `w0 >= a1` still compares the way the machine did. The negation keeps it too: the
|
|
199
|
+
* complement of an unsigned ordering is unsigned. */
|
|
193
200
|
function rewriteCond(cond: Expr, E: Expr, name: string): Expr | null {
|
|
194
201
|
if (cond.k !== 'bin' || !(cond.op in FLIP)) {
|
|
195
202
|
return null;
|
|
196
203
|
}
|
|
197
204
|
if (exprEq(cond.l, E) && isPure(cond.r)) {
|
|
198
|
-
return {
|
|
205
|
+
return { ...cond, l: { k: 'var', name } };
|
|
199
206
|
}
|
|
200
207
|
if (exprEq(cond.r, E) && isPure(cond.l)) {
|
|
201
|
-
return {
|
|
208
|
+
return { ...cond, r: { k: 'var', name } };
|
|
202
209
|
}
|
|
203
210
|
return null;
|
|
204
211
|
}
|
|
@@ -207,7 +214,7 @@ function flipCmp(cond: Expr): Expr | null {
|
|
|
207
214
|
return null;
|
|
208
215
|
}
|
|
209
216
|
const op = FLIP[cond.op];
|
|
210
|
-
return op ? {
|
|
217
|
+
return op ? { ...cond, op: op as Extract<Expr, { k: 'bin' }>['op'] } : null;
|
|
211
218
|
}
|
|
212
219
|
/** in `upd`, the occurrence of subtree E replaced by var `name` (E was just assigned to it). */
|
|
213
220
|
function renameSubexpr(e: Expr, E: Expr, name: string): Expr {
|