@asmlift/core 0.8.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -30
- package/package.json +1 -1
- package/src/aggregate.ts +248 -0
- package/src/backend/c.ts +5 -2
- package/src/backend/cfamily.ts +236 -37
- package/src/backend/cpp.ts +114 -9
- package/src/backend/pascal.ts +39 -0
- package/src/cdecl/constant.ts +59 -0
- package/src/cdecl/lex.ts +209 -0
- package/src/cdecl/parse.ts +975 -0
- package/src/cdecl/spell.ts +69 -0
- package/src/codegen-flags.ts +38 -6
- package/src/compiler-diagnostics.ts +194 -0
- package/src/contracts.ts +615 -79
- package/src/declare.ts +75 -5
- package/src/frontend/asmdata.ts +19 -5
- package/src/frontend/call-plan.ts +686 -0
- package/src/frontend/device-pins.ts +277 -0
- package/src/frontend/errors.ts +5 -0
- package/src/frontend/flags-edge.ts +101 -0
- package/src/frontend/fpu.ts +167 -0
- package/src/frontend/frame-objects.ts +2131 -0
- package/src/frontend/high-half.ts +2 -1
- package/src/frontend/local-object.ts +490 -0
- package/src/frontend/mips.ts +363 -40
- package/src/frontend/opaque.ts +97 -18
- package/src/frontend/ppc.ts +520 -129
- package/src/frontend/reloc-symbol.ts +94 -13
- package/src/frontend/splat.ts +108 -5
- package/src/frontend/ssa.ts +669 -76
- package/src/frontend/stackargs.ts +120 -39
- package/src/frontend/thumb.ts +2764 -2018
- package/src/ir/alias.ts +29 -0
- package/src/ir/core.ts +101 -3
- package/src/ir/discipline.ts +248 -0
- package/src/ir/float-bits.ts +47 -0
- package/src/ir/opcodes.ts +84 -112
- package/src/ir/parse.ts +1 -0
- package/src/ir/types.ts +118 -2
- package/src/ir/verify.ts +150 -3
- package/src/l3/address.ts +5 -0
- package/src/l3/argcopy.ts +38 -6
- package/src/l3/ast.ts +186 -14
- package/src/l3/coalesce.ts +21 -13
- package/src/l3/dce.ts +32 -10
- package/src/l3/gates.ts +9 -3
- package/src/l3/hoist.ts +8 -7
- package/src/l3/initfirst.ts +27 -9
- package/src/l3/intcell.ts +97 -0
- package/src/l3/mentions.ts +10 -4
- package/src/l3/narrowdecl.ts +218 -0
- package/src/l3/nearbase.ts +10 -5
- package/src/l3/parkfirst.ts +8 -5
- package/src/l3/pollguard.ts +3 -2
- package/src/l3/ptrcell.ts +357 -0
- package/src/l3/regspell.ts +17 -3
- package/src/l3/reindex.ts +10 -7
- package/src/l3/symbol-refs.ts +104 -23
- package/src/l3/tailmerge.ts +6 -19
- package/src/l3/typing.ts +137 -25
- package/src/l3/unreduce.ts +4 -4
- package/src/l3/volstore.ts +96 -7
- package/src/mangle.ts +11 -1
- package/src/pattern/engine.ts +19 -16
- package/src/pipeline.ts +57 -12
- package/src/proto-context.ts +788 -0
- package/src/proto.ts +689 -50
- package/src/raise/arrays.ts +4 -3
- package/src/raise/declared-volatile.ts +131 -0
- package/src/raise/divpow2.ts +2 -2
- package/src/raise/errors.ts +17 -0
- package/src/raise/floathelpers.ts +243 -0
- package/src/raise/globalshape.ts +11 -5
- package/src/raise/gvn.ts +26 -5
- package/src/raise/narrow.ts +29 -2
- package/src/raise/narrowlocal.ts +11 -6
- package/src/raise/offsetnames.ts +510 -0
- package/src/raise/pairparams.ts +81 -0
- package/src/raise/paramwidth.ts +7 -0
- package/src/raise/pre-recovery.ts +78 -4
- package/src/raise/recover.ts +14 -10
- package/src/raise/retsink.ts +1 -1
- package/src/raise/shortcircuit.ts +41 -19
- package/src/raise/softdiv.ts +21 -23
- package/src/raise/struct-arrays.ts +5 -4
- package/src/raise/structs.ts +311 -98
- package/src/raise/truncload.ts +366 -0
- package/src/raise/widehelpers.ts +106 -0
- package/src/rank-declare.ts +30 -5
- package/src/rank-variations.ts +36 -8
- package/src/rank.ts +267 -79
- package/src/runtime-helpers.ts +287 -0
- package/src/stillborn.ts +289 -0
- package/src/structure/analysis.ts +1285 -448
- package/src/structure/hazards.ts +633 -58
- package/src/structure/local-statics.ts +381 -0
- package/src/structure/namecoalesce.ts +1 -1
- package/src/structure/redundant-test.ts +99 -0
- package/src/structure/structure.ts +1772 -395
- package/src/structure/switch-recover.ts +159 -78
- package/src/symbols.ts +38 -3
- package/src/target.ts +662 -59
- package/src/trace.ts +44 -24
- package/src/variation-definitions.ts +119 -1
- package/src/variation-tokens.ts +20 -6
package/src/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,26 @@
|
|
|
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
|
+
mentionedName,
|
|
47
|
+
stmtChildren,
|
|
48
|
+
stmtExprs,
|
|
49
|
+
walkExprs,
|
|
50
|
+
} from './ast';
|
|
51
|
+
import { arithConversionSignedness, declaredTypes, exprIntWidth, provablyNonNegative } from './typing';
|
|
43
52
|
|
|
44
53
|
const readsVar = (e: Expr, name: string): boolean =>
|
|
45
|
-
(
|
|
54
|
+
mentionedName(e) === name || exprChildren(e).some((c) => readsVar(c, name));
|
|
46
55
|
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
56
|
+
// MENTIONS in an expression — a read, an `&v` a callee may read through, a `v++` that reads before
|
|
57
|
+
// it writes. A whole-statement write is what is benign (an `assign` overwrites the minted value on
|
|
58
|
+
// every path, and any read after it is that write's business), which is why the target of one is
|
|
59
|
+
// not consulted here; touchesOutside below is TOTAL because strong mode must know the name is
|
|
60
|
+
// absent, presence of any kind included.
|
|
50
61
|
const stmtTouches = (s: Stmt, name: string): boolean =>
|
|
51
62
|
stmtExprs(s).some((e) => readsVar(e, name)) || stmtChildren(s).some((x) => stmtTouches(x, name));
|
|
52
63
|
|
|
@@ -67,10 +78,11 @@ const stripWideIntCast = (e: Expr): Expr =>
|
|
|
67
78
|
* would silently discard it. */
|
|
68
79
|
const varRooted = (e: Expr): boolean => (e.k === 'var' ? true : e.k === 'cast' ? varRooted(e.e) : false);
|
|
69
80
|
|
|
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
|
|
81
|
+
/** A guard re-spell's X: call/marker/post-increment-free, every named leaf a non-volatile param/local
|
|
82
|
+
* of THIS function (a global or `&gSym` could be project-declared volatile), and every deref
|
|
83
|
+
* `varRooted`. */
|
|
72
84
|
const hoistableRead = (e: Expr, ownNames: ReadonlySet<string>, volatileLocals: ReadonlySet<string>): boolean => {
|
|
73
|
-
if (e.k === 'call' || e.k === 'marker' || e.k === 'addr') {
|
|
85
|
+
if (e.k === 'call' || e.k === 'marker' || e.k === 'postincr' || e.k === 'addr') {
|
|
74
86
|
return false;
|
|
75
87
|
}
|
|
76
88
|
if (e.k === 'var' && (!ownNames.has(e.name) || volatileLocals.has(e.name))) {
|
|
@@ -110,6 +122,12 @@ const meaningPreserved = (
|
|
|
110
122
|
if (vt?.kind !== 'int' || vt.width !== 32) {
|
|
111
123
|
return false;
|
|
112
124
|
}
|
|
125
|
+
// …AND SO MUST X, which the premise above states and the check did not: `v = X` represents X
|
|
126
|
+
// exactly only while X fits in v, so a 64-bit X assigned to a 32-bit v truncates and every
|
|
127
|
+
// signedness argument below it is about a value the program does not have.
|
|
128
|
+
if (exprIntWidth(side === 'l' ? l : r, env) !== 32) {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
113
131
|
if (provablyNonNegative(l, env) && provablyNonNegative(r, env)) {
|
|
114
132
|
return true;
|
|
115
133
|
}
|
|
@@ -0,0 +1,97 @@
|
|
|
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 operand-order rule restores to a byte pointer
|
|
42
|
+
const sum = stored.k === 'cast' && isPtrTo(stored.to, 'u8') ? stored.e : stored;
|
|
43
|
+
if (sum.k === 'bin' && (sum.op === '+' || sum.op === '-') && isCell(sum.l, name)) {
|
|
44
|
+
return { ...sum, l: { k: 'var', name } };
|
|
45
|
+
}
|
|
46
|
+
return stored.k === 'var' ? stored : { k: 'cast', to: T.u(32), e: stored };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The `/int-cell` candidate: every pointer cell no declaration types, spelled as an integer.
|
|
50
|
+
* `mapDeclared` says the symbol map declares a name something other than an integer. */
|
|
51
|
+
export function integerCells(
|
|
52
|
+
sfn: SFn,
|
|
53
|
+
mapDeclared: (name: string) => boolean,
|
|
54
|
+
): { sfn: SFn; needsProof: boolean } | null {
|
|
55
|
+
const vt = declaredTypes(sfn);
|
|
56
|
+
const cells = new Set([...pointerCellsOf(sfn)].filter((n) => vt(n) === undefined && !mapDeclared(n)));
|
|
57
|
+
if (cells.size === 0) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
const expr = (e0: Expr): Expr => {
|
|
61
|
+
const e = mapExprChildren(e0, expr);
|
|
62
|
+
if (e.k === 'cast' && isWord(e.to)) {
|
|
63
|
+
const inner = e.e.k === 'cast' && isPtrTo(e.e.to, 'u8') ? e.e.e : e.e;
|
|
64
|
+
if (inner.k === 'var' && cells.has(inner.name)) {
|
|
65
|
+
return inner;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return e;
|
|
69
|
+
};
|
|
70
|
+
const stmt = (s: Stmt): Stmt => {
|
|
71
|
+
switch (s.k) {
|
|
72
|
+
case 'assign': {
|
|
73
|
+
const value = expr(s.value);
|
|
74
|
+
return cells.has(s.name) && pointerCellStore(s) !== undefined
|
|
75
|
+
? { ...s, value: integerValue(value, s.name) }
|
|
76
|
+
: { ...s, value };
|
|
77
|
+
}
|
|
78
|
+
case 'if':
|
|
79
|
+
return { ...s, cond: expr(s.cond), then: s.then.map(stmt), else: s.else.map(stmt) };
|
|
80
|
+
case 'while':
|
|
81
|
+
case 'dowhile':
|
|
82
|
+
return { ...s, cond: expr(s.cond), body: s.body.map(stmt) };
|
|
83
|
+
case 'for':
|
|
84
|
+
return { ...s, init: stmt(s.init), cond: expr(s.cond), inc: stmt(s.inc), body: s.body.map(stmt) };
|
|
85
|
+
case 'switch':
|
|
86
|
+
return {
|
|
87
|
+
...s,
|
|
88
|
+
scrutinee: expr(s.scrutinee),
|
|
89
|
+
cases: s.cases.map((c) => ({ ...c, body: c.body.map(stmt) })),
|
|
90
|
+
default: s.default?.map(stmt),
|
|
91
|
+
};
|
|
92
|
+
default:
|
|
93
|
+
return mapStmtExprs(s, expr);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
return { sfn: { ...sfn, body: sfn.body.map(stmt) }, needsProof: true };
|
|
97
|
+
}
|
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,218 @@
|
|
|
1
|
+
// L3 stacked variations `/narrow-decl` and `/narrow-read`: a named narrow value declared at its
|
|
2
|
+
// width. `/narrow-decl` takes the narrowing at the local's one write, `/narrow-read` at every one of
|
|
3
|
+
// its reads:
|
|
4
|
+
//
|
|
5
|
+
// s32 v; v = (u8)(x - 1); … v … becomes u8 v; v = x - 1; … v … (/narrow-decl)
|
|
6
|
+
// s32 v; v = f(); … (u8)v … (u8)v … becomes u8 v; v = f(); … v … v … (/narrow-read)
|
|
7
|
+
//
|
|
8
|
+
// 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 promoted to `int`, which is what an `s32` read
|
|
10
|
+
// already is. They are not the same program to gcc 2.9's front end. `get_narrower`
|
|
11
|
+
// (gcc/tree.c:4516) sees through the promotion of a narrow VARIABLE and not through an `int` that
|
|
12
|
+
// a cast was assigned to, so for an unsigned narrow one `shorten_compare` (gcc/c-common.c:1158)
|
|
13
|
+
// makes a compare against a constant unsigned and folds `>= 0`, `/` and `%` are shortened to their
|
|
14
|
+
// unsigned helpers (gcc/c-typeck.c:2036-2041, :2079-2090), and `>>` becomes a logical shift
|
|
15
|
+
// (gcc/c-typeck.c:2119). Without such an operator the object can still differ: `convert_to_integer`
|
|
16
|
+
// pushes a narrow store's truncation into an AND's operands (gcc/convert.c:278-284), and a `u8`
|
|
17
|
+
// local is a pseudo PROMOTE_MODE already zero-extended (gcc/thumb.h:344, gcc/stmt.c:3318-3323),
|
|
18
|
+
// whose low byte is free to read (gcc/expr.c:830-836), where an `s32` one takes a fresh byte pseudo
|
|
19
|
+
// and a register copy. Compiled with agbcc `-O2 -mthumb-interwork -fhex-asm -fprologue-bugfix`,
|
|
20
|
+
//
|
|
21
|
+
// v = g.c - 1; h.d = (v & 1) + rnd() % (5 - v) + 1;
|
|
22
|
+
//
|
|
23
|
+
// copies `v` before the AND under `s32 v` (`mov r1, #1; add r4, r5, #0; and r4, r4, r1`) and
|
|
24
|
+
// operates on it in place under `u8 v` (`mov r5, #1; and r5, r5, r4`), which is what
|
|
25
|
+
// `kleod:sub_0803E8CC` holds; over `v | 2`, `v ^ 2`, `v + 3` and `v * 3` the two compile alike.
|
|
26
|
+
// IDO, KMC gcc and gcc 2.7.2 each emit different code for that pair as well, and every mwcc
|
|
27
|
+
// build does for a compare of the narrowed value. Which one the
|
|
28
|
+
// source declared is not in the asm, so the differ referees, and the candidate it is derived onto
|
|
29
|
+
// stays in the fan.
|
|
30
|
+
//
|
|
31
|
+
// At the reads, `(u8)(s32)e`, `(u8)(u32)e` and `(u8)e` keep the same low byte of any integer `e`,
|
|
32
|
+
// so dropping each read's cast keeps its value, whichever write stored it. The pair compiles
|
|
33
|
+
// differently by where the extension lands: a `u8` local holding a call's result is zero-extended
|
|
34
|
+
// as it is stored, right after the `bl`, where `(u8)v` extends at the read, after whatever ran in
|
|
35
|
+
// between (`pokeemerald:RtcGetDayCount`, three results passed on as `u8`s).
|
|
36
|
+
//
|
|
37
|
+
// Two STACKED variations (rank-variations.ts), derived onto every other candidate's tree: the width
|
|
38
|
+
// of a declaration is orthogonal to what every respell variation outside that table changes, and
|
|
39
|
+
// the row that needs it needs it on top of `/offmember`. Two, not one, because a function holding a
|
|
40
|
+
// local narrowed at its write and a local narrowed at its reads may need either one narrowed alone.
|
|
41
|
+
// Each rewrites every local it admits. The two do not commute: a cast that is one local's write and
|
|
42
|
+
// another's read is taken by whichever runs first. So each alone is its own candidate, and in the
|
|
43
|
+
// all-together candidate `/narrow-decl` runs first and takes that cast as the write.
|
|
44
|
+
//
|
|
45
|
+
// KNOWN GAP, and it is the price of deciding at L3 rather than where the value is named: which
|
|
46
|
+
// spelling a call's result had shows only in where its extension lands, right after its `bl` or
|
|
47
|
+
// just before its read, and structure/analysis.ts settles that per local when it names the call and
|
|
48
|
+
// inlines the extension into the read. So two read-side locals declared differently (`s32 y` cast
|
|
49
|
+
// at its read beside `u8 m`) have no candidate, as each entry narrows both or neither; here that
|
|
50
|
+
// would take 2^k candidates over k such locals. Nor has `s32 v = (u8)f()` once another such value
|
|
51
|
+
// is held across its call, which then compiles like neither spelling; held alone it compiles like
|
|
52
|
+
// `u8 v`, which /narrow-read reaches.
|
|
53
|
+
//
|
|
54
|
+
// REFUSED, each because the two spellings would stop computing the same value or would not build:
|
|
55
|
+
// • a local that is not a 32-bit integer;
|
|
56
|
+
// • a local whose address is taken, or that is volatile, a frame object, uninitialized, or homed
|
|
57
|
+
// in a stack slot. Each of those is an object in memory, whose width is its access width;
|
|
58
|
+
// • a write of a constant that fits neither the signed nor the unsigned type of the narrow width,
|
|
59
|
+
// which gcc warns of when it converts one implicitly (gcc/c-common.c:849-861, :870-895) and a
|
|
60
|
+
// `-Werror` build refuses;
|
|
61
|
+
// • /narrow-decl: a `u32` local. Its reads stay bare, and a `u32` read is unsigned, so narrowing
|
|
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 could store a
|
|
64
|
+
// value the narrow declaration would truncate and a bare read would not;
|
|
65
|
+
// • /narrow-decl: a local whose one write is a `for` loop's init, or that is not an integer
|
|
66
|
+
// narrowed to a narrower integer. A call's operand is not known to be an integer (its callee
|
|
67
|
+
// may return a pointer), so `(u8)f()` is refused too;
|
|
68
|
+
// • /narrow-read: a read that is not a narrowing cast (a bare `v`, an index base, a `v++`), reads
|
|
69
|
+
// cast to two widths or two signednesses, a write in a `for` loop's init or step, and a write
|
|
70
|
+
// whose value is a pointer, a float or of no known type. A call is admitted: its value already
|
|
71
|
+
// converts implicitly to the 32-bit local, and narrowing changes only the width of that
|
|
72
|
+
// conversion, never whether C performs one. A `u32` local and one written more than once are
|
|
73
|
+
// admitted, because every read already narrows each value stored to the one width.
|
|
74
|
+
import type { IrType } from '../ir/types';
|
|
75
|
+
import {
|
|
76
|
+
type Expr,
|
|
77
|
+
type SFn,
|
|
78
|
+
type Stmt,
|
|
79
|
+
mapExprChildren,
|
|
80
|
+
mapStmtExprs,
|
|
81
|
+
mapStmtLists,
|
|
82
|
+
stmtLists,
|
|
83
|
+
walkExprs,
|
|
84
|
+
} from './ast';
|
|
85
|
+
import { type Mentions, localMentions, readsOf } from './mentions';
|
|
86
|
+
import { declaredTypes, exprCType } from './typing';
|
|
87
|
+
|
|
88
|
+
type Narrowing = Extract<Expr, { k: 'cast' }> & { to: Extract<IrType, { kind: 'int' }> };
|
|
89
|
+
|
|
90
|
+
/** An integer narrowed to a narrower integer. A pointer or a float operand keeps its cast: C converts
|
|
91
|
+
* neither to an integer by assignment alone. */
|
|
92
|
+
const isNarrowing = (e: Expr, env: ReturnType<typeof declaredTypes>): e is Narrowing =>
|
|
93
|
+
e.k === 'cast' && !e.volatile && e.to.kind === 'int' && e.to.width < 32 && exprCType(e.e, env)?.kind === 'int';
|
|
94
|
+
|
|
95
|
+
/** `(T)v`, the read of `name` narrowed to the narrower integer T. */
|
|
96
|
+
const isReadNarrowing = (e: Expr, name: string): e is Narrowing =>
|
|
97
|
+
e.k === 'cast' && !e.volatile && e.to.kind === 'int' && e.to.width < 32 && e.e.k === 'var' && e.e.name === name;
|
|
98
|
+
|
|
99
|
+
/** The one type every read of `name` is narrowed to, or undefined when a read is not narrowed
|
|
100
|
+
* or two reads are narrowed differently. `reads` counts every read however spelled, so a read
|
|
101
|
+
* that is not a narrowing leaves the count short. */
|
|
102
|
+
function readNarrowing(body: Stmt[], name: string, reads: number): Narrowing['to'] | undefined {
|
|
103
|
+
let to: Narrowing['to'] | undefined;
|
|
104
|
+
let narrowed = 0;
|
|
105
|
+
for (const e of walkExprs(body)) {
|
|
106
|
+
if (!isReadNarrowing(e, name)) {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (to !== undefined && (to.width !== e.to.width || to.signed !== e.to.signed)) {
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
to = e.to;
|
|
113
|
+
narrowed++;
|
|
114
|
+
}
|
|
115
|
+
return narrowed === reads ? to : undefined;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** The value of every assignment statement to `name`, found the way the write-side rewrite finds
|
|
119
|
+
* them: through statement lists, so a `for` init or step is not one. */
|
|
120
|
+
function writtenValues(body: Stmt[], name: string): Expr[] {
|
|
121
|
+
const values: Expr[] = [];
|
|
122
|
+
const walk = (list: Stmt[]): void => {
|
|
123
|
+
for (const s of list) {
|
|
124
|
+
if (s.k === 'assign' && s.name === name) {
|
|
125
|
+
values.push(s.value);
|
|
126
|
+
}
|
|
127
|
+
stmtLists(s).forEach(walk);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
walk(body);
|
|
131
|
+
return values;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** `body` with every `(T)name` read spelled `name`. */
|
|
135
|
+
const dropReadCasts = (body: Stmt[], name: string): Stmt[] => {
|
|
136
|
+
const expr = (e: Expr): Expr => (isReadNarrowing(e, name) ? e.e : mapExprChildren(e, expr));
|
|
137
|
+
return body.map((s) => mapStmtExprs(s, expr));
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
/** The local's admission common to both sides: a 32-bit integer in no memory. */
|
|
141
|
+
const admissible = (l: SFn['locals'][number], m: Mentions | undefined): m is Mentions =>
|
|
142
|
+
m !== undefined &&
|
|
143
|
+
m.addrTaken === 0 &&
|
|
144
|
+
l.type.kind === 'int' &&
|
|
145
|
+
l.type.width === 32 &&
|
|
146
|
+
!l.volatile &&
|
|
147
|
+
!l.frame &&
|
|
148
|
+
!l.uninit &&
|
|
149
|
+
!l.slots;
|
|
150
|
+
|
|
151
|
+
/** Whether storing `value` into an `int` of `to`'s width converts without gcc's constant warning: a
|
|
152
|
+
* constant must fit the signed or the unsigned type of that width. */
|
|
153
|
+
const storesQuietly = (value: Expr, to: Narrowing['to']): boolean =>
|
|
154
|
+
value.k !== 'const' || (value.value >= -(2 ** (to.width - 1)) && value.value < 2 ** to.width);
|
|
155
|
+
|
|
156
|
+
/** `sfn` with each local in `narrowed` declared at its width, or null when it is empty. */
|
|
157
|
+
const declared = (sfn: SFn, body: Stmt[], narrowed: Map<string, IrType>): SFn | null =>
|
|
158
|
+
narrowed.size === 0
|
|
159
|
+
? null
|
|
160
|
+
: { ...sfn, body, locals: sfn.locals.map((l) => ({ ...l, type: narrowed.get(l.name) ?? l.type })) };
|
|
161
|
+
|
|
162
|
+
/** `/narrow-decl`: the tree with every admitted local narrowed at its write, or null when none is. */
|
|
163
|
+
export function narrowDeclarations(sfn: SFn): SFn | null {
|
|
164
|
+
const mentions = localMentions(sfn);
|
|
165
|
+
const env = declaredTypes(sfn);
|
|
166
|
+
let body = sfn.body;
|
|
167
|
+
const narrowed = new Map<string, IrType>();
|
|
168
|
+
for (const l of sfn.locals) {
|
|
169
|
+
const m = mentions.get(l.name);
|
|
170
|
+
if (!admissible(l, m) || m.assigns !== 1 || l.type.kind !== 'int' || !l.type.signed) {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
let to: IrType | undefined;
|
|
174
|
+
const rewrite = (list: Stmt[]): Stmt[] =>
|
|
175
|
+
list.map((s) => {
|
|
176
|
+
if (
|
|
177
|
+
s.k === 'assign' &&
|
|
178
|
+
s.name === l.name &&
|
|
179
|
+
isNarrowing(s.value, env) &&
|
|
180
|
+
storesQuietly(s.value.e, s.value.to)
|
|
181
|
+
) {
|
|
182
|
+
to = s.value.to;
|
|
183
|
+
return { ...s, value: s.value.e };
|
|
184
|
+
}
|
|
185
|
+
return mapStmtLists(s, rewrite);
|
|
186
|
+
});
|
|
187
|
+
const next = rewrite(body);
|
|
188
|
+
if (to !== undefined) {
|
|
189
|
+
body = next;
|
|
190
|
+
narrowed.set(l.name, to);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return declared(sfn, body, narrowed);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** `/narrow-read`: the tree with every admitted local narrowed at its reads, or null when none is. */
|
|
197
|
+
export function narrowReadDeclarations(sfn: SFn): SFn | null {
|
|
198
|
+
const mentions = localMentions(sfn);
|
|
199
|
+
const env = declaredTypes(sfn);
|
|
200
|
+
let body = sfn.body;
|
|
201
|
+
const narrowed = new Map<string, IrType>();
|
|
202
|
+
for (const l of sfn.locals) {
|
|
203
|
+
const m = mentions.get(l.name);
|
|
204
|
+
if (!admissible(l, m)) {
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
const written = writtenValues(body, l.name);
|
|
208
|
+
if (written.length !== m.assigns || written.some((w) => w.k !== 'call' && exprCType(w, env)?.kind !== 'int')) {
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
const read = readNarrowing(body, l.name, readsOf(m));
|
|
212
|
+
if (read !== undefined && written.every((w) => storesQuietly(w, read))) {
|
|
213
|
+
body = dropReadCasts(body, l.name);
|
|
214
|
+
narrowed.set(l.name, read);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return declared(sfn, body, narrowed);
|
|
218
|
+
}
|
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;
|
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
|
@@ -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. */
|