@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/parkfirst.ts
CHANGED
|
@@ -28,15 +28,17 @@
|
|
|
28
28
|
// gcc2.7.2 schedules it last — so a second inhabitant consolidates both into one entry-prefix
|
|
29
29
|
// ordering variation rather than growing a sibling.
|
|
30
30
|
import type { Expr, SFn, Stmt } from './ast';
|
|
31
|
-
import { exprChildren, exprHasEffect } from './ast';
|
|
31
|
+
import { exprChildren, exprHasEffect, mentionedName } from './ast';
|
|
32
32
|
|
|
33
33
|
type Assign = Extract<Stmt, { k: 'assign' }>;
|
|
34
34
|
|
|
35
|
-
//
|
|
36
|
-
// park must treat
|
|
35
|
+
// Every mention counts as touching its name: `foo(&v)` may read or write v through the pointer,
|
|
36
|
+
// and `v++` writes it outright, so a park must treat either exactly like a direct read-and-write
|
|
37
|
+
// of v.
|
|
37
38
|
const readVars = (e: Expr, acc: Set<string> = new Set()): Set<string> => {
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
const n = mentionedName(e);
|
|
40
|
+
if (n !== undefined) {
|
|
41
|
+
acc.add(n);
|
|
40
42
|
}
|
|
41
43
|
for (const c of exprChildren(e)) {
|
|
42
44
|
readVars(c, acc);
|
|
@@ -49,6 +51,7 @@ const pureOverParams = (e: Expr, params: ReadonlySet<string>): boolean => {
|
|
|
49
51
|
case 'var':
|
|
50
52
|
return params.has(e.name);
|
|
51
53
|
case 'const':
|
|
54
|
+
case 'fconst':
|
|
52
55
|
return true;
|
|
53
56
|
case 'un':
|
|
54
57
|
case 'cast':
|
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;
|
|
@@ -58,8 +58,9 @@ const countVar = (e: Expr, n: string): number =>
|
|
|
58
58
|
const countAddr = (e: Expr, n: string): number =>
|
|
59
59
|
(e.k === 'addr' && e.name === n ? 1 : 0) + exprChildren(e).reduce((a, c) => a + countAddr(c, n), 0);
|
|
60
60
|
|
|
61
|
-
/** call- and
|
|
62
|
-
const pure = (e: Expr): boolean =>
|
|
61
|
+
/** call-, marker- and post-increment-free: no effect the fold could move or duplicate. */
|
|
62
|
+
const pure = (e: Expr): boolean =>
|
|
63
|
+
e.k !== 'call' && e.k !== 'marker' && e.k !== 'postincr' && exprChildren(e).every(pure);
|
|
63
64
|
|
|
64
65
|
/** The var a deref's base stands on, through casts only — null for anything else (a raw address, an
|
|
65
66
|
* arithmetic base), which is exactly the set `condDerefsPlain` refuses. */
|
|
@@ -139,7 +140,13 @@ export function pollReads(sfn: SFn): SFn | null {
|
|
|
139
140
|
condFoldable(w.cond, volatileLocals) &&
|
|
140
141
|
ownsEveryOccurrence(sfn.body, a.name)
|
|
141
142
|
) {
|
|
142
|
-
|
|
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
|
+
});
|
|
143
150
|
dropped.add(a.name);
|
|
144
151
|
i++;
|
|
145
152
|
continue;
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
// L3: a POINTER CELL — a global the body stores a pointer into, and the one place that spelling is
|
|
2
|
+
// recognised.
|
|
3
|
+
//
|
|
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:
|
|
9
|
+
//
|
|
10
|
+
// • l3/symbol-refs.ts declares a name-only cell `void *` in the candidate's own world, the type
|
|
11
|
+
// that store needs, and so each global the cell meets bare that nothing reads as an integer
|
|
12
|
+
// (`pointerPartners`);
|
|
13
|
+
// • `legalizePointerCells` below converts the cell's other uses to match that declaration;
|
|
14
|
+
// • l3/intcell.ts spells the cell as the integer a project may have declared instead.
|
|
15
|
+
//
|
|
16
|
+
// The fact is derived from the final tree at each reader rather than carried on SFn, for the reason
|
|
17
|
+
// l3/symbol-refs.ts's header gives: a rewrite that drops the store must drop the fact with it.
|
|
18
|
+
import { type IrType, T } from '../ir/types';
|
|
19
|
+
import { declaredWidth } from '../proto';
|
|
20
|
+
import {
|
|
21
|
+
type Expr,
|
|
22
|
+
type SFn,
|
|
23
|
+
type Stmt,
|
|
24
|
+
assignsIn,
|
|
25
|
+
exprChildren,
|
|
26
|
+
mapExprChildren,
|
|
27
|
+
taintOverAssigns,
|
|
28
|
+
walkExprs,
|
|
29
|
+
} from './ast';
|
|
30
|
+
import { declaredTypes, exprCType } from './typing';
|
|
31
|
+
|
|
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. */
|
|
38
|
+
export function pointerCellStore(s: Stmt): string | undefined {
|
|
39
|
+
return s.k === 'assign' && s.value.k === 'cast' && s.value.to.kind === 'ptr' && s.value.to.to.kind === 'void'
|
|
40
|
+
? s.name
|
|
41
|
+
: undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const COMPARE = new Set(['<', '<=', '>', '>=', '==', '!=']);
|
|
45
|
+
|
|
46
|
+
/** Whether `e` reads a variable `pick` takes. */
|
|
47
|
+
const readsVar = (e: Expr, pick: (name: string) => boolean): boolean =>
|
|
48
|
+
(e.k === 'var' && pick(e.name)) || exprChildren(e).some((c) => readsVar(c, pick));
|
|
49
|
+
|
|
50
|
+
/** A cell's value moved by integers (`g + 4`, `g + a0 - 8`): pointer arithmetic, a pointer as the
|
|
51
|
+
* cell is. Whatever is added is an integer, as no pointer is; what is subtracted is a constant,
|
|
52
|
+
* since a pointer's difference is no pointer. */
|
|
53
|
+
export function cellOffset(e: Expr, cells: ReadonlySet<string>): boolean {
|
|
54
|
+
if (e.k !== 'bin' || (e.op !== '+' && e.op !== '-')) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
const base = (x: Expr): boolean => (x.k === 'var' && cells.has(x.name)) || cellOffset(x, cells);
|
|
58
|
+
const offset = (x: Expr): boolean => !readsVar(x, (n) => e.op === '-' || cells.has(n));
|
|
59
|
+
return (base(e.l) && offset(e.r)) || (e.op === '+' && base(e.r) && offset(e.l));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The globals `body` stores a pointer into: a `(void *)` store, or the bare value of another one,
|
|
63
|
+
* or that value moved by integers (`cellOffset`). */
|
|
64
|
+
export function pointerCells(body: Stmt[], isGlobal: (name: string) => boolean): Set<string> {
|
|
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
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The bare globals a pointer cell meets bare: compared with one or with its `cellOffset`, or
|
|
74
|
+
* stored into one. */
|
|
75
|
+
function meetsBare(body: Stmt[], isGlobal: (name: string) => boolean, cells: ReadonlySet<string>): Set<string> {
|
|
76
|
+
const out = new Set<string>();
|
|
77
|
+
const meets = (e: Expr): void => {
|
|
78
|
+
if (e.k === 'var' && isGlobal(e.name) && !cells.has(e.name)) {
|
|
79
|
+
out.add(e.name);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
const isCell = (e: Expr): boolean => (e.k === 'var' && cells.has(e.name)) || cellOffset(e, cells);
|
|
83
|
+
for (const s of assignsIn(body)) {
|
|
84
|
+
if (cells.has(s.name)) {
|
|
85
|
+
meets(s.value);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
for (const e of walkExprs(body)) {
|
|
89
|
+
if (e.k === 'bin' && COMPARE.has(e.op)) {
|
|
90
|
+
if (isCell(e.l)) {
|
|
91
|
+
meets(e.r);
|
|
92
|
+
}
|
|
93
|
+
if (isCell(e.r)) {
|
|
94
|
+
meets(e.l);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Whether a name `sfn` reads is a global: no param or local binds it. */
|
|
102
|
+
const globalOf = (sfn: SFn): ((name: string) => boolean) => {
|
|
103
|
+
const bound = new Set([...sfn.params.map((p) => p.name), ...sfn.locals.map((l) => l.name)]);
|
|
104
|
+
return (name) => !bound.has(name);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/** The globals `sfn` stores a pointer into (`pointerCells`). */
|
|
108
|
+
export function pointerCellsOf(sfn: SFn): Set<string> {
|
|
109
|
+
return pointerCells(sfn.body, globalOf(sfn));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Whether a declared parameter spelling is an integer of at most a word: the conversion a pointer
|
|
113
|
+
* argument needs to reach it. A spelling this cannot read is no opinion. */
|
|
114
|
+
const integerParam = (t: string | undefined): boolean => {
|
|
115
|
+
if (t === undefined) {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
const w = declaredWidth(t);
|
|
119
|
+
return w !== undefined && w <= 32 && !t.includes('*') && !t.includes('(');
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/** What `mapIntegerSlots` does at each place a pointer cell meets an integer. */
|
|
123
|
+
interface SlotRules {
|
|
124
|
+
/** An operand a cell meets bare, which no conversion takes. */
|
|
125
|
+
pointerTyped: (e: Expr) => boolean;
|
|
126
|
+
/** An operand the context reads as an integer. */
|
|
127
|
+
int: (e: Expr) => Expr;
|
|
128
|
+
/** An argument an integer parameter takes. */
|
|
129
|
+
intArg: (e: Expr) => Expr;
|
|
130
|
+
/** A bare global compared with a cell that is no partner. */
|
|
131
|
+
comparedGlobal: (e: Expr) => Expr;
|
|
132
|
+
/** A value stored into a cell. */
|
|
133
|
+
intoCell: (e: Expr) => Expr;
|
|
134
|
+
/** A `cellOffset` a pointer meets. */
|
|
135
|
+
pointerOffset: (e: Expr) => Expr;
|
|
136
|
+
/** A global stored into that is no cell, which is an integer slot. */
|
|
137
|
+
storedGlobal: (name: string) => void;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** `sfn` with `rules` applied at every place its context states an integer type: an operand of a
|
|
141
|
+
* non-additive operator, of `-`/`~`, an index, a switch, a comparison with no pointer, a call
|
|
142
|
+
* argument whose callee's declared parameter (`SFn.declaredArgs`) is an integer, and a store, an
|
|
143
|
+
* assignment or a return into an integer slot. A global stored into that is no cell is an integer
|
|
144
|
+
* slot: the candidate's own world declares it one. */
|
|
145
|
+
function mapIntegerSlots(sfn: SFn, cells: ReadonlySet<string>, rules: SlotRules): SFn {
|
|
146
|
+
const isGlobal = globalOf(sfn);
|
|
147
|
+
const vt = declaredTypes(sfn);
|
|
148
|
+
const isCell = (e: Expr): boolean => (e.k === 'var' && cells.has(e.name)) || cellOffset(e, cells);
|
|
149
|
+
const bareGlobal = (e: Expr): boolean => e.k === 'var' && isGlobal(e.name) && !cells.has(e.name);
|
|
150
|
+
const compare = (x: Expr, other: Expr): Expr =>
|
|
151
|
+
isCell(other) && bareGlobal(x) && !rules.pointerTyped(x)
|
|
152
|
+
? rules.comparedGlobal(x)
|
|
153
|
+
: rules.pointerTyped(other) || cellOffset(other, cells)
|
|
154
|
+
? cellOffset(x, cells)
|
|
155
|
+
? rules.pointerOffset(x)
|
|
156
|
+
: x
|
|
157
|
+
: rules.int(x);
|
|
158
|
+
const expr = (e0: Expr): Expr => {
|
|
159
|
+
const e = mapExprChildren(e0, expr);
|
|
160
|
+
switch (e.k) {
|
|
161
|
+
case 'bin':
|
|
162
|
+
if (e.op === '&&' || e.op === '||' || e.op === '+' || e.op === '-') {
|
|
163
|
+
return e;
|
|
164
|
+
}
|
|
165
|
+
if (COMPARE.has(e.op)) {
|
|
166
|
+
return { ...e, l: compare(e.l, e.r), r: compare(e.r, e.l) };
|
|
167
|
+
}
|
|
168
|
+
return { ...e, l: rules.int(e.l), r: rules.int(e.r) };
|
|
169
|
+
case 'un':
|
|
170
|
+
return e.op === '-' || e.op === '~' ? { ...e, e: rules.int(e.e) } : e;
|
|
171
|
+
case 'call': {
|
|
172
|
+
const params =
|
|
173
|
+
typeof e.fn === 'string' && sfn.declaredArgs && Object.hasOwn(sfn.declaredArgs, e.fn)
|
|
174
|
+
? sfn.declaredArgs[e.fn]
|
|
175
|
+
: undefined;
|
|
176
|
+
if (params === undefined) {
|
|
177
|
+
return e;
|
|
178
|
+
}
|
|
179
|
+
return { ...e, args: e.args.map((a, i) => (integerParam(params[i]) ? rules.intArg(a) : a)) };
|
|
180
|
+
}
|
|
181
|
+
case 'index':
|
|
182
|
+
return { ...e, idx: rules.int(e.idx), ...(e.lead ? { lead: e.lead.map(rules.int) } : {}) };
|
|
183
|
+
default:
|
|
184
|
+
return e;
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const stmt = (s: Stmt): Stmt => {
|
|
189
|
+
switch (s.k) {
|
|
190
|
+
case 'assign': {
|
|
191
|
+
const value = expr(s.value);
|
|
192
|
+
if (cells.has(s.name)) {
|
|
193
|
+
return { ...s, value: rules.pointerTyped(value) ? value : rules.intoCell(value) };
|
|
194
|
+
}
|
|
195
|
+
const t = vt(s.name);
|
|
196
|
+
if (t === undefined && isGlobal(s.name)) {
|
|
197
|
+
rules.storedGlobal(s.name);
|
|
198
|
+
return { ...s, value: rules.int(value) };
|
|
199
|
+
}
|
|
200
|
+
return { ...s, value: t?.kind === 'int' ? rules.int(value) : value };
|
|
201
|
+
}
|
|
202
|
+
case 'store': {
|
|
203
|
+
const value = expr(s.value);
|
|
204
|
+
return {
|
|
205
|
+
...s,
|
|
206
|
+
lval: expr(s.lval),
|
|
207
|
+
value: exprCType(s.lval, vt)?.kind === 'int' ? rules.int(value) : value,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
case 'exprstmt':
|
|
211
|
+
return { ...s, value: expr(s.value) };
|
|
212
|
+
case 'return':
|
|
213
|
+
return s.value ? { ...s, value: sfn.retType.kind === 'int' ? rules.int(expr(s.value)) : expr(s.value) } : s;
|
|
214
|
+
case 'if':
|
|
215
|
+
return { ...s, cond: expr(s.cond), then: s.then.map(stmt), else: s.else.map(stmt) };
|
|
216
|
+
case 'while':
|
|
217
|
+
case 'dowhile':
|
|
218
|
+
return { ...s, cond: expr(s.cond), body: s.body.map(stmt) };
|
|
219
|
+
case 'for':
|
|
220
|
+
return { ...s, init: stmt(s.init), cond: expr(s.cond), inc: stmt(s.inc), body: s.body.map(stmt) };
|
|
221
|
+
case 'switch':
|
|
222
|
+
return {
|
|
223
|
+
...s,
|
|
224
|
+
scrutinee: rules.int(expr(s.scrutinee)),
|
|
225
|
+
cases: s.cases.map((c) => ({ ...c, body: c.body.map(stmt) })),
|
|
226
|
+
default: s.default?.map(stmt),
|
|
227
|
+
};
|
|
228
|
+
case 'break':
|
|
229
|
+
case 'continue':
|
|
230
|
+
return s;
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
return { ...sfn, body: sfn.body.map(stmt) };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** A sum no operand of which renders a pointer: read as an integer, each bare name in it is. */
|
|
237
|
+
const integerSum = (e: Expr, vt: (name: string) => IrType | undefined): e is Extract<Expr, { k: 'bin' }> =>
|
|
238
|
+
e.k === 'bin' &&
|
|
239
|
+
(e.op === '+' || e.op === '-') &&
|
|
240
|
+
exprCType(e.l, vt)?.kind !== 'ptr' &&
|
|
241
|
+
exprCType(e.r, vt)?.kind !== 'ptr';
|
|
242
|
+
|
|
243
|
+
/** The globals a pointer cell meets bare (compared with it or its offset, or stored into it) that hold a pointer
|
|
244
|
+
* too: the candidate's own world declares each `void *` beside the cells, and they are left as
|
|
245
|
+
* spelled, since converting one would convert whatever the project declares it, where bare a
|
|
246
|
+
* float, an array or a function meets the cell as a constraint violation.
|
|
247
|
+
*
|
|
248
|
+
* A global the body also reads as an integer is not one: `void *` takes none of those reads, so its
|
|
249
|
+
* own world declares it the integer it is read as, and the cell converts to meet it. */
|
|
250
|
+
export function pointerPartners(sfn: SFn): Set<string> {
|
|
251
|
+
const isGlobal = globalOf(sfn);
|
|
252
|
+
const cells = pointerCells(sfn.body, isGlobal);
|
|
253
|
+
const meets = meetsBare(sfn.body, isGlobal, cells);
|
|
254
|
+
if (meets.size === 0) {
|
|
255
|
+
return meets;
|
|
256
|
+
}
|
|
257
|
+
const vt = declaredTypes(sfn);
|
|
258
|
+
const read = new Set<string>();
|
|
259
|
+
const int = (e: Expr): Expr => {
|
|
260
|
+
if (e.k === 'var' && meets.has(e.name)) {
|
|
261
|
+
read.add(e.name);
|
|
262
|
+
} else if (integerSum(e, vt)) {
|
|
263
|
+
int(e.l);
|
|
264
|
+
int(e.r);
|
|
265
|
+
}
|
|
266
|
+
return e;
|
|
267
|
+
};
|
|
268
|
+
mapIntegerSlots(sfn, cells, {
|
|
269
|
+
pointerTyped: (e) => e.k === 'var' && (cells.has(e.name) || meets.has(e.name)),
|
|
270
|
+
int,
|
|
271
|
+
intArg: int,
|
|
272
|
+
comparedGlobal: int,
|
|
273
|
+
intoCell: (e) => e,
|
|
274
|
+
pointerOffset: (e) => e,
|
|
275
|
+
storedGlobal: (name) => {
|
|
276
|
+
if (meets.has(name)) {
|
|
277
|
+
read.add(name);
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
return new Set([...meets].filter((n) => !read.has(n)));
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** `sfn` with every integer use of a pointer cell converted to the integer it reads as, and every
|
|
285
|
+
* integer stored into one converted to a pointer.
|
|
286
|
+
*
|
|
287
|
+
* A cell the body stores a pointer into is a pointer under every declaration that store compiles
|
|
288
|
+
* against, so the cell's integer readers (`return g;`, `if (g >= a0)`, `v0 = g;`, `g + 4` returned
|
|
289
|
+
* as an integer, an integer parameter's argument) and its integer writers (`g = a0;`) need the
|
|
290
|
+
* conversion C leaves implicit and CodeWarrior and IDO reject: `(u32)g`, `g = (void *)a0`. The
|
|
291
|
+
* conversion of a word is no instruction, so the bytes are the implicit one's. A cell's offset
|
|
292
|
+
* (`cellOffset`) stored into a cell or compared with a pointer is the pointer it is, spelled in
|
|
293
|
+
* bytes: `gOut = (void *)((u8 *)g + 4)`, `(u8 *)g + 4 == gP`.
|
|
294
|
+
*
|
|
295
|
+
* SCOPE: a use whose context states an integer type (`mapIntegerSlots`); elsewhere the use is left
|
|
296
|
+
* as the structurer spelled it. A truth test, a sum that renders a pointer (the structurer's own
|
|
297
|
+
* byte sum, which the backend converts at an integer slot: `legalizePointerWrites`), and a
|
|
298
|
+
* comparison with a pointer (a call by its declared return, `SFn.declaredReturns`), with 0 or with
|
|
299
|
+
* a partner (`pointerPartners`) are left alone. A global compared with the cell that is no partner
|
|
300
|
+
* goes `(u32)(u8 *)g` beside the cell's `(u32)g`: under a float declaration that is no C, as the
|
|
301
|
+
* bare comparison was not. KNOWN GAP: under an array or a function declaration that is the
|
|
302
|
+
* global's address, compared with no diagnostic, and IDO takes the integer read that made it no
|
|
303
|
+
* partner (`v1 = g`) with none either; structure/pointer-spelling.ts `globalWord` says why no
|
|
304
|
+
* spelling reads the word there.
|
|
305
|
+
*
|
|
306
|
+
* Run by the C-family backend's `emit` (backend/cfamily.ts), after every respell variation, so a
|
|
307
|
+
* respell reads the uses as the structurer spelled them and a store it removes takes the
|
|
308
|
+
* conversions with it. */
|
|
309
|
+
export function legalizePointerCells(sfn: SFn): SFn {
|
|
310
|
+
const cells = pointerCellsOf(sfn);
|
|
311
|
+
if (cells.size === 0) {
|
|
312
|
+
return sfn;
|
|
313
|
+
}
|
|
314
|
+
const partners = pointerPartners(sfn);
|
|
315
|
+
const vt = declaredTypes(sfn);
|
|
316
|
+
const isCell = (e: Expr): boolean => e.k === 'var' && cells.has(e.name);
|
|
317
|
+
const returnsPointer = (fn: string | Expr): boolean =>
|
|
318
|
+
typeof fn === 'string' &&
|
|
319
|
+
sfn.declaredReturns !== undefined &&
|
|
320
|
+
Object.hasOwn(sfn.declaredReturns, fn) &&
|
|
321
|
+
sfn.declaredReturns[fn].includes('*');
|
|
322
|
+
const asInt = (e: Expr): Expr =>
|
|
323
|
+
isCell(e) ? { k: 'cast', to: T.u(32), e } : integerSum(e, vt) ? { ...e, l: asInt(e.l), r: asInt(e.r) } : e;
|
|
324
|
+
// A cell's offset as the pointer it is: byte arithmetic on the cell, `(u8 *)g + 4`, which no
|
|
325
|
+
// pointee scales and which a `void *` takes.
|
|
326
|
+
const asBytes = (e: Expr): Expr =>
|
|
327
|
+
isCell(e)
|
|
328
|
+
? { k: 'cast', to: T.ptr(T.u(8)), e }
|
|
329
|
+
: e.k === 'bin' && cellOffset(e, cells)
|
|
330
|
+
? { ...e, l: asBytes(e.l), r: e.op === '+' ? asBytes(e.r) : e.r }
|
|
331
|
+
: e;
|
|
332
|
+
// The structurer's byte sum on a cell's value (`(u8 *)g + 8`) is a pointer too, which an integer
|
|
333
|
+
// parameter takes converted as a bare cell does; the backend converts it at every other integer
|
|
334
|
+
// slot (`legalizePointerWrites`).
|
|
335
|
+
const cellSum = (e: Expr): boolean =>
|
|
336
|
+
(e.k === 'cast' && e.to.kind === 'ptr' && isCell(e.e)) ||
|
|
337
|
+
(e.k === 'bin' && (e.op === '+' || e.op === '-') && (cellSum(e.l) || cellSum(e.r)));
|
|
338
|
+
return mapIntegerSlots(sfn, cells, {
|
|
339
|
+
pointerTyped: (e) =>
|
|
340
|
+
isCell(e) ||
|
|
341
|
+
(e.k === 'var' && partners.has(e.name)) ||
|
|
342
|
+
e.k === 'addr' ||
|
|
343
|
+
(e.k === 'call' && returnsPointer(e.fn)) ||
|
|
344
|
+
(e.k === 'const' && e.value === 0) ||
|
|
345
|
+
exprCType(e, vt)?.kind === 'ptr',
|
|
346
|
+
int: asInt,
|
|
347
|
+
intArg: (e) =>
|
|
348
|
+
e.k === 'bin' && cellSum(e) && exprCType(e, vt)?.kind === 'ptr' ? { k: 'cast', to: T.u(32), e } : asInt(e),
|
|
349
|
+
comparedGlobal: (e) => ({ k: 'cast', to: T.u(32), e: { k: 'cast', to: T.ptr(T.u(8)), e } }),
|
|
350
|
+
intoCell: (e) => pointerCellValue(asBytes(e)),
|
|
351
|
+
pointerOffset: asBytes,
|
|
352
|
+
storedGlobal: () => {},
|
|
353
|
+
});
|
|
354
|
+
}
|
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
|