@asmlift/core 0.5.0 → 0.7.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 +22 -16
- package/package.json +1 -1
- package/src/backend/c.ts +1 -0
- package/src/backend/cfamily.ts +270 -171
- package/src/backend/cpp.ts +1 -0
- package/src/backend/pascal.ts +26 -12
- package/src/contracts.ts +243 -39
- package/src/declare.ts +41 -4
- package/src/frontend/mips.ts +11 -0
- package/src/frontend/ppc.ts +43 -7
- package/src/frontend/ssa.ts +404 -29
- package/src/frontend/thumb.ts +2176 -686
- package/src/ir/alias.ts +78 -0
- package/src/ir/bits.ts +75 -0
- package/src/ir/core.ts +345 -2
- package/src/ir/opcodes.ts +176 -21
- package/src/ir/parse.ts +19 -2
- package/src/ir/print.ts +27 -2
- package/src/ir/simplify.ts +190 -3
- package/src/ir/struct-names.ts +42 -0
- package/src/ir/verify.ts +43 -49
- package/src/l3/address.ts +62 -0
- package/src/l3/advance.ts +373 -0
- package/src/l3/argbase.ts +6 -5
- package/src/l3/ast.ts +510 -59
- package/src/l3/basecse.ts +686 -78
- package/src/l3/coalesce.ts +432 -46
- package/src/l3/dce.ts +31 -9
- package/src/l3/gates.ts +96 -1
- package/src/l3/hoist.ts +293 -14
- package/src/l3/homesplit.ts +285 -0
- package/src/l3/initfirst.ts +301 -0
- package/src/l3/inlinebase.ts +193 -0
- package/src/l3/mentions.ts +176 -0
- package/src/l3/mulfirst.ts +42 -0
- package/src/l3/nearbase.ts +152 -0
- package/src/l3/offmember.ts +371 -0
- package/src/l3/parkfirst.ts +96 -0
- package/src/l3/pollguard.ts +154 -0
- package/src/l3/ptrfield.ts +227 -0
- package/src/l3/regspell.ts +114 -89
- package/src/l3/reindex.ts +722 -80
- package/src/l3/scopebase.ts +649 -220
- package/src/l3/sinkinit.ts +40 -0
- package/src/l3/slotorder.ts +123 -0
- package/src/l3/storage.ts +48 -0
- package/src/l3/symbol-refs.ts +41 -8
- package/src/l3/tailmerge.ts +16 -1
- package/src/l3/typing.ts +198 -9
- package/src/l3/unmerge.ts +687 -0
- package/src/l3/unreduce.ts +971 -0
- package/src/l3/volatileptr.ts +207 -0
- package/src/l3/volatileval.ts +130 -0
- package/src/l3/volstore.ts +229 -0
- package/src/l3/zerosub.ts +62 -0
- package/src/pattern/engine.ts +239 -16
- package/src/pipeline.ts +173 -60
- package/src/proto.ts +112 -14
- package/src/raise/arrays.ts +6 -1
- package/src/raise/const.ts +203 -3
- package/src/raise/divpow2.ts +4 -4
- package/src/raise/extscale.ts +342 -0
- package/src/raise/globalshape.ts +1058 -0
- package/src/raise/gvn.ts +33 -18
- package/src/raise/latch.ts +126 -0
- package/src/raise/magicdiv.ts +2 -2
- package/src/raise/memberarrays.ts +594 -0
- package/src/raise/narrow.ts +124 -0
- package/src/raise/narrowlocal.ts +572 -0
- package/src/raise/paramwidth.ts +201 -0
- package/src/raise/pre-recovery.ts +169 -21
- package/src/raise/recover.ts +56 -23
- package/src/raise/retsink.ts +585 -19
- package/src/raise/shortcircuit.ts +1050 -89
- package/src/raise/struct-arrays.ts +19 -2
- package/src/raise/structs.ts +34 -4
- package/src/raise/tailsink.ts +126 -0
- package/src/rank-declare.ts +256 -0
- package/src/rank-variations.ts +760 -0
- package/src/rank.ts +2122 -326
- package/src/structure/analysis.ts +1398 -150
- package/src/structure/bitfields.ts +432 -0
- package/src/structure/globalaccess.ts +300 -0
- package/src/structure/hazards.ts +411 -20
- package/src/structure/loops.ts +2 -49
- package/src/structure/namecoalesce.ts +454 -0
- package/src/structure/structure.ts +3979 -612
- package/src/structure/switch-recover.ts +710 -145
- package/src/symbols.ts +188 -6
- package/src/target.ts +495 -32
- package/src/trace.ts +112 -33
- package/src/variation-definitions.ts +1540 -0
- package/src/variation-gates.ts +89 -0
- package/src/variation-tokens.ts +355 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// L3 respell variation: sink each leading pointer-base INIT to the statement that first uses it.
|
|
2
|
+
//
|
|
3
|
+
// `l3/basecse.ts`'s COMMITTED call emits every base hoist at the head of `sfn.body`, so a base
|
|
4
|
+
// first touched halfway down the function is live across everything above it — a live range the
|
|
5
|
+
// original never had, and agbcc pays for it with a callee-saved register. (Its roster's hoists
|
|
6
|
+
// ask for this placement directly, through the same `l3/hoist.ts` mechanism this variation uses; what
|
|
7
|
+
// the variation adds is reaching the run on a tree the roster did not build — one NOTHING re-hoisted,
|
|
8
|
+
// or one `l3/nearbase.ts` prepended into, which is the `/nearbase/sinkinit` pairing.)
|
|
9
|
+
// Compiled pair on `synthetic:basehome`:
|
|
10
|
+
// assigning at the top adds `push {r4, lr}` / `pop {r4}` / `pop {r0}` / `bx r0` where assigning at
|
|
11
|
+
// the first use keeps a plain `bx lr`. The ladder that row records is the argument for the variation
|
|
12
|
+
// being a PLACEMENT rather than a wider hoist — top-placed 11, not hoisted at all 9, placed at
|
|
13
|
+
// first use 0. A hoist at the wrong place is worse than no hoist.
|
|
14
|
+
//
|
|
15
|
+
// A VARIATION, NOT THE DEFAULT. Which placement the source used is per-function knowledge the asm does
|
|
16
|
+
// not carry: moving basecse's own head placement to first use moves 8 benchmark rows, 4 better and
|
|
17
|
+
// 4 worse, two of the losses being matches. The UNSUNK spelling always rides beside this one —
|
|
18
|
+
// head-placed wherever basecse built the run, prepend-placed under `/nearbase` — and the differ
|
|
19
|
+
// referees, so ADDING this candidate can never cost a match. Withholding one is a different
|
|
20
|
+
// question and not a free one (l3/basecse.ts's fold-evidence note).
|
|
21
|
+
//
|
|
22
|
+
// SCOPE (decline over approximate). Only the LEADING run of base inits moves — the run basecse.ts
|
|
23
|
+
// and nearbase.ts mint into, placed by the same `l3/hoist.ts` mechanism, so no two of the three can
|
|
24
|
+
// disagree about where the run ends or how a body carrying it is rebuilt. Each init sinks to
|
|
25
|
+
// immediately before the first TOP-LEVEL statement mentioning its name and never INTO a nested
|
|
26
|
+
// scope, so it still dominates every use — planning a hoist inside a scope is `l3/scopebase.ts`'s
|
|
27
|
+
// job and it does the domination work. `placeBaseLocals` carries the refusals; a run where none of
|
|
28
|
+
// them moves is this variation declining.
|
|
29
|
+
//
|
|
30
|
+
// SEMANTICS BY CONSTRUCTION: the moved value is a pure address leaf — it reads nothing, writes its
|
|
31
|
+
// own plain cell and cannot fault — and every statement it crosses mentions the name nowhere, an
|
|
32
|
+
// `&p` escape included (mentions.ts counts `addr` as a mention). All later evaluation can change
|
|
33
|
+
// is where the allocator puts it.
|
|
34
|
+
import type { SFn } from './ast';
|
|
35
|
+
import { placeBaseLocals } from './hoist';
|
|
36
|
+
|
|
37
|
+
export function sinkInitsToFirstUse(sfn: SFn): SFn | null {
|
|
38
|
+
const { body, moved } = placeBaseLocals(sfn, [], 'first-use');
|
|
39
|
+
return moved.length === 0 ? null : { ...sfn, body };
|
|
40
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// asmlift — declare slot-homed locals in the TARGET'S OWN FRAME ORDER.
|
|
2
|
+
//
|
|
3
|
+
// THE LAW. gcc 2.9 hands a spilled user local its frame slot by DECLARATION RANK: reload walks
|
|
4
|
+
// pseudos in ascending number handing each global-allocation loser a fresh stack slot, a user
|
|
5
|
+
// local's pseudo number is fixed at its `expand_decl` — i.e. its position in the declaration list
|
|
6
|
+
// — and the Thumb frame grows upward. So under agbcc the earlier-declared spilled local takes the
|
|
7
|
+
// LOWER `[sp,#k]`, and a source whose two spilled locals are declared the other way round compiles
|
|
8
|
+
// to the same object with those two operands swapped and nothing else moved.
|
|
9
|
+
//
|
|
10
|
+
// A COMPILER BEHAVIOR, NOT A RANKED VARIATION. The asm does not underdetermine the answer: a
|
|
11
|
+
// `[sp,#k]` operand NAMES the slot, and slot → declaration rank is a FUNCTION once the compiler is
|
|
12
|
+
// fixed. A variation exists where two source spellings collapse to the same object and only the differ
|
|
13
|
+
// can choose between them; here the object chooses. The fan cost is zero — this rewrites the one
|
|
14
|
+
// tree every candidate already carries, adding no candidate, no `structure()` call and no compile.
|
|
15
|
+
//
|
|
16
|
+
// WHY `emit` OWNS IT. Two reasons, and they point the same way. It must run AFTER every L3 respell variation,
|
|
17
|
+
// because each of those rebuilds the declaration list (appends: basecse, scopebase, argbase,
|
|
18
|
+
// nearbase, regspell, reindex; filters: unmerge, coalesce, dce, inlinebase, pollguard, unreduce),
|
|
19
|
+
// and `emit` is last by construction. And it must NOT run at a `.emit(` call site: there are seven
|
|
20
|
+
// of those, the web Playground reaches two of them for one function (the headline source and the
|
|
21
|
+
// Pipeline tab), and the score probe reaches a third — a call-site ordering would print an ordered
|
|
22
|
+
// headline beside an unordered pipeline dump and measure the probe's scoreDelta on a source the
|
|
23
|
+
// ranked path never compiles.
|
|
24
|
+
//
|
|
25
|
+
// WHAT DEPENDS ON THE ORDER THIS DOES NOT CHANGE. `l3/coalesce.ts` picks the arm-disjoint survivor
|
|
26
|
+
// by `declIdx` — the earlier declaration, matching how a shared source local reads. It runs before
|
|
27
|
+
// this and therefore reads the UNSORTED list, which is exactly right and is what the emit-time
|
|
28
|
+
// placement preserves. Sorting any earlier would silently change which local survives every
|
|
29
|
+
// arm-disjoint merge.
|
|
30
|
+
import type { SFn } from './ast';
|
|
31
|
+
|
|
32
|
+
/** Refill the positions held by slot-carrying locals with those same locals in frame order.
|
|
33
|
+
*
|
|
34
|
+
* PURE: the structurer's own list is never mutated, so `emit` cannot leak an ordered list back to
|
|
35
|
+
* a caller that expects the structurer's order.
|
|
36
|
+
*
|
|
37
|
+
* REFUSAL CONDITIONS, all of them — 1, 2 and 6 are checked below; 3, 4 and 5 hold by construction
|
|
38
|
+
* (see each):
|
|
39
|
+
* 1. `fn.slotOrder` absent — the target's direction is unknown, or it ships `'unknown'`: the
|
|
40
|
+
* ordering is the IDENTITY. There is no default direction.
|
|
41
|
+
* 2. Fewer than two sortable locals: nothing to order.
|
|
42
|
+
* 3. A local with no `slots`, or an empty list, keeps its position exactly. Only the positions
|
|
43
|
+
* the sortable locals already occupy are refilled, so an unslotted local never moves and no
|
|
44
|
+
* local is ever inserted or removed.
|
|
45
|
+
* 4. `frame` and `uninit` locals are never sortable, because the structurer never stamps
|
|
46
|
+
* `slots` on them — the refusal, and the measurement behind it, live at that stamp site.
|
|
47
|
+
* 5. Parameters are never touched: their storage is the caller's question.
|
|
48
|
+
* 6. TWO DECLARED LOCALS SHARING ONE OFFSET REFUSE THE WHOLE FUNCTION. Reload hands each
|
|
49
|
+
* spilled pseudo a FRESH slot (`reload1.c:769-770`, the premise this file's law rests on),
|
|
50
|
+
* so a frame in which the slot -> local map is not injective is not a frame this law
|
|
51
|
+
* describes at all: something upstream — a decomposed stack aggregate, a coalesce, a naming
|
|
52
|
+
* walk that put two spilled values under two names at one address — produced the evidence,
|
|
53
|
+
* and none of those is a declaration rank. It is NOT enough to leave that PAIR alone: the
|
|
54
|
+
* shared offset is still used to rank both of them against every other sortable local, so
|
|
55
|
+
* the whole ordering is unlicensed and the identity is the only sound answer.
|
|
56
|
+
*
|
|
57
|
+
* MEASURED, both sides. It costs nothing: `spillorder` (v6@4 v12@0), `uninit_spill` (v4@0
|
|
58
|
+
* v5@4 v6@8), `dma_fill_uninit` (v2@16 v4@4 v6@8 v8@12) and the `agbcc-u8spill.s` fixture
|
|
59
|
+
* (v0@4 v10@8 v11@12) are all injective, so every shipped inhabitant keeps its ordering. And
|
|
60
|
+
* it is what the wild corpus asks for: of 2,463 real agbcc functions (158 sa3 + klonoa
|
|
61
|
+
* listings) exactly ONE carries two slot-carrying locals at all, and it is non-injective —
|
|
62
|
+
* `sa3 enemies/hariisen_proj.s` `sub_80617E0`, `v8@12 v12@8 v13@12`, which without this
|
|
63
|
+
* refusal emitted `… v7 v12 v9 v10 v11 v8 v13 …` against the declaration order `… v7 v8 v9
|
|
64
|
+
* v10 v11 v12 v13 …`. Its slots are four words of one declared stack array (see the stamp
|
|
65
|
+
* site's aggregate note), which is exactly the class that mints a duplicate. So the one real
|
|
66
|
+
* function this default reached was one whose evidence contradicts its own premise, and the
|
|
67
|
+
* wild reach is zero functions changed rather than one changed wrongly.
|
|
68
|
+
*
|
|
69
|
+
* A consequence, so nothing downstream relies on the sort's stability: two sortable locals
|
|
70
|
+
* can no longer TIE. Equal ranks means an equal minimum offset under `ascending` and an
|
|
71
|
+
* equal maximum under `descending`, and either is a shared offset, which this refuses first.
|
|
72
|
+
*
|
|
73
|
+
* AND THIS IS THE ONE PLACE A SET OF HOMES IS REDUCED TO A RANK. A local can carry several
|
|
74
|
+
* offsets, and every site that merged them took the union and chose nothing, because the choice
|
|
75
|
+
* is direction-dependent and none of them holds a target (ir/core.ts `SlotHomes`). The earliest
|
|
76
|
+
* declaration rank is the LOWEST offset under an ascending frame and the HIGHEST under a
|
|
77
|
+
* descending one: one comparator, applied where `slotOrder` is in hand.
|
|
78
|
+
*
|
|
79
|
+
* The `descending` half of that comparator has NO SHIPPED INHABITANT: agbcc is the only target
|
|
80
|
+
* that ships a direction and it is `ascending`. It is reached only through the public
|
|
81
|
+
* `StructureOptions.spillSlotOrder` and by the tests, and it exists because ido7.1's
|
|
82
|
+
* `TargetDescription` carries a MEASURED `descending` with a written flip condition (target.ts).
|
|
83
|
+
*
|
|
84
|
+
* REACH. The two inhabitants are `spillorder` (6 → MATCH) and `dma_fill_uninit` (12 → MATCH, a
|
|
85
|
+
* row this capability did not author). On the REAL agbcc tier it is ZERO, and the cause is named
|
|
86
|
+
* rather than assumed: of 126 cases, four carry any L1 slot home, two carry a slot-carrying local
|
|
87
|
+
* at L3, and NONE carries two. So the blocker is neither the frame order nor the census — the
|
|
88
|
+
* structurer's naming walk INLINES spilled values instead of declaring them (`PackSaveSector`
|
|
89
|
+
* spills to 18 distinct slots, 108 stamped values, and reaches L3 with zero slot-carrying
|
|
90
|
+
* locals), which is also why counting stores is a bad proxy for reach. */
|
|
91
|
+
export function orderSlotLocals(fn: SFn): SFn {
|
|
92
|
+
const dir = fn.slotOrder;
|
|
93
|
+
if (dir === undefined) {
|
|
94
|
+
return fn;
|
|
95
|
+
}
|
|
96
|
+
// the earliest declaration rank among a local's homes
|
|
97
|
+
const rank = (l: SFn['locals'][number]): number =>
|
|
98
|
+
dir === 'ascending' ? Math.min(...l.slots!) : Math.max(...l.slots!);
|
|
99
|
+
const at: number[] = [];
|
|
100
|
+
fn.locals.forEach((l, i) => {
|
|
101
|
+
if (l.slots !== undefined && l.slots.length > 0) {
|
|
102
|
+
at.push(i);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
if (at.length < 2) {
|
|
106
|
+
return fn;
|
|
107
|
+
}
|
|
108
|
+
// refusal 6: the slot -> local map must be INJECTIVE, or this frame is not one the law
|
|
109
|
+
// describes and no local in it can be ranked. Over every offset of every sortable local, not
|
|
110
|
+
// just their ranks: a duplicate anywhere means some slot was not a fresh reload assignment.
|
|
111
|
+
const offsets = at.flatMap((i) => fn.locals[i].slots!);
|
|
112
|
+
if (new Set(offsets).size !== offsets.length) {
|
|
113
|
+
return fn;
|
|
114
|
+
}
|
|
115
|
+
const inFrameOrder = at
|
|
116
|
+
.map((i) => fn.locals[i])
|
|
117
|
+
.sort((a, b) => (dir === 'ascending' ? rank(a) - rank(b) : rank(b) - rank(a)));
|
|
118
|
+
const locals = [...fn.locals];
|
|
119
|
+
at.forEach((i, k) => {
|
|
120
|
+
locals[i] = inFrameOrder[k];
|
|
121
|
+
});
|
|
122
|
+
return { ...fn, locals };
|
|
123
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Which STORAGE CLASS an L3 name denotes.
|
|
2
|
+
//
|
|
3
|
+
// `assign` names its target with a bare string, and structure.ts spells a store to a local, to a
|
|
4
|
+
// param and to a bare scalar global identically — so every pass that must tell them apart has to
|
|
5
|
+
// re-derive the distinction from `SFn`'s three declaration lists. Four did, privately, and the one
|
|
6
|
+
// that got it wrong (l3/reindex.ts) read a plausible type off a global and re-spelled away a write
|
|
7
|
+
// another translation unit observes.
|
|
8
|
+
//
|
|
9
|
+
// SHADOWING is what the copies disagreed on, and the disagreement is REAL rather than drift: a
|
|
10
|
+
// name can be declared both a global and a local, because `SFn.globals` records what the code
|
|
11
|
+
// REFERENCES (structure.ts `noteGlobal`) independently of what it declares. Which answer is right
|
|
12
|
+
// depends on how the asking pass will SPELL the name, so the two questions are two functions
|
|
13
|
+
// here instead of one set each caller filters its own way:
|
|
14
|
+
// • it keeps the reference verbatim (`(u8 *)g`) — the shadow is harmless, the spelling denotes
|
|
15
|
+
// whatever the original access denoted: `declaredGlobals`;
|
|
16
|
+
// • it re-spells the reference as an ADDRESS (`&g`) — a shadow then names the LOCAL's storage,
|
|
17
|
+
// silently a different object: `addressableGlobals`;
|
|
18
|
+
// • it asks which of the three classes a bare `assign` name denotes at all: `nameStorage`, the
|
|
19
|
+
// primitive the other two are the shadow-sensitive projections over.
|
|
20
|
+
import type { SFn } from './ast';
|
|
21
|
+
|
|
22
|
+
export type Storage = 'local' | 'param' | 'global';
|
|
23
|
+
|
|
24
|
+
/** Every declared name, classified. A local or a param SHADOWS a same-named global — that is what
|
|
25
|
+
* the name binds to. A name absent from the map is declared nowhere. */
|
|
26
|
+
export function nameStorage(sfn: SFn): Map<string, Storage> {
|
|
27
|
+
const t = new Map<string, Storage>();
|
|
28
|
+
for (const g of sfn.globals ?? []) {
|
|
29
|
+
t.set(g.name, 'global');
|
|
30
|
+
}
|
|
31
|
+
for (const p of sfn.params) {
|
|
32
|
+
t.set(p.name, 'param');
|
|
33
|
+
}
|
|
34
|
+
for (const l of sfn.locals) {
|
|
35
|
+
t.set(l.name, 'local');
|
|
36
|
+
}
|
|
37
|
+
return t;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Names declared as globals, INCLUDING one a local or param shadows (see the header). */
|
|
41
|
+
export function declaredGlobals(sfn: SFn): Set<string> {
|
|
42
|
+
return new Set((sfn.globals ?? []).map((g) => g.name));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Names whose ADDRESS is the global's — a shadowed one excluded (see the header). */
|
|
46
|
+
export function addressableGlobals(sfn: SFn): Set<string> {
|
|
47
|
+
return new Set([...nameStorage(sfn)].filter(([, s]) => s === 'global').map(([n]) => n));
|
|
48
|
+
}
|
package/src/l3/symbol-refs.ts
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
// asmlift — SELF-DECLARING CANDIDATES: the pure map-reference query
|
|
2
2
|
// (research/self-declaring-candidates-2026-07-26.md).
|
|
3
3
|
//
|
|
4
|
-
// `collectSymbolRefs` derives, from a FINAL structured tree, every
|
|
4
|
+
// `collectSymbolRefs` derives, from a FINAL structured tree, every DECLARABLE symbol the body
|
|
5
5
|
// references in a VALUE context — the input to the scoring layer's declaration synthesis
|
|
6
|
-
// (
|
|
6
|
+
// (core declare.ts). "Declarable" and not "map-derived": the dictionary this is called with is
|
|
7
|
+
// the caller's, and rank.ts hands it a symbol map UNIONED with the names read straight out of
|
|
8
|
+
// the asm's own literal pool / relocations (`bareGlobalSymbols`), so a candidate compiled
|
|
9
|
+
// outside project headers declares what it spells even with no map at all. The map's facts win
|
|
10
|
+
// per NAME where it has them; the rest come back marked `synthesized`.
|
|
11
|
+
//
|
|
12
|
+
// It is a pure tree query with no pipeline state: the enumeration
|
|
7
13
|
// layer (rank.ts) calls it exactly once per candidate, on the tree the candidate's source was
|
|
8
14
|
// emitted from, at the moment the candidate is finalized. There is deliberately NO cached
|
|
9
15
|
// `symbolRefs` field on `SFn` — a carried field would oblige every future l3 pass to remember
|
|
@@ -13,7 +19,7 @@
|
|
|
13
19
|
import type { SymbolInfo } from '../symbols';
|
|
14
20
|
import { Expr, Stmt, exprChildren, stmtChildren, stmtExprs } from './ast';
|
|
15
21
|
|
|
16
|
-
/** One recorded
|
|
22
|
+
/** One recorded VALUE reference — a name the tree references plus the facts to declare it. */
|
|
17
23
|
export interface SymbolRef {
|
|
18
24
|
name: string;
|
|
19
25
|
info: SymbolInfo;
|
|
@@ -21,19 +27,36 @@ export interface SymbolRef {
|
|
|
21
27
|
* own IR — attached by the enumeration (rank.ts bareGlobalAccessFacts), consumed by the
|
|
22
28
|
* declaration synthesis (declare.ts) as the width/signedness authority for `extern T name;`. */
|
|
23
29
|
access?: { width: number; signed: boolean };
|
|
30
|
+
/** NO SYMBOL MAP KNOWS THIS NAME — the declaration for it is a HYPOTHESIS, not a fact. Both the
|
|
31
|
+
* name and (through `access`) its width and signedness were read out of the candidate's own
|
|
32
|
+
* asm: the very bytes the candidate is then scored against. A fitted declaration cannot LOSE
|
|
33
|
+
* score, so it is not covered by declare.ts's only-loses-score argument — it can manufacture
|
|
34
|
+
* agreement instead. Which is legitimate (the artifact is decls + source, and it does compile
|
|
35
|
+
* to those bytes) exactly as long as the consumer SHOWS the declarations with the verdict.
|
|
36
|
+
* Marked here so a consumer can tell the two provenances apart inside one block. */
|
|
37
|
+
synthesized?: true;
|
|
24
38
|
}
|
|
25
39
|
|
|
26
|
-
/** The
|
|
40
|
+
/** The declarable symbols a structured body references in a VALUE context — the input to the
|
|
27
41
|
* scoring layer's declaration synthesis. A name counts when it appears as a `var`/`addr` leaf
|
|
28
|
-
* and the
|
|
42
|
+
* and the caller's dictionary knows it (bare `gSym`, `&gSym`, `(u32)Func`, a `field` base — all reduce to
|
|
29
43
|
* those leaves). A name that is ANY call's target is excluded entirely, even if also
|
|
30
44
|
* value-referenced: prototyping a called symbol `void F(void);` hard-errors under gcc-2.9
|
|
31
45
|
* when the call passes args, while leaving it undeclared keeps today's implicit-declaration
|
|
32
46
|
* behavior (the one honest option without arity knowledge). The function's OWN name
|
|
33
47
|
* (`selfName`) is excluded too — the candidate's definition IS its declaration, and a
|
|
34
48
|
* synthesized `void F(void);` above `s32 F(...)` is a conflicting-types hard error (a
|
|
35
|
-
* self-address reference resolves against the definition itself).
|
|
36
|
-
|
|
49
|
+
* self-address reference resolves against the definition itself).
|
|
50
|
+
*
|
|
51
|
+
* Both exclusions are REPORTED through `onRefused`, on the caller's own refusal channel: they
|
|
52
|
+
* leave a name undeclared for asmlift's own reason, and a consumer's list of those reasons is
|
|
53
|
+
* incomplete without them. */
|
|
54
|
+
export function collectSymbolRefs(
|
|
55
|
+
body: Stmt[],
|
|
56
|
+
symbols: Map<string, SymbolInfo>,
|
|
57
|
+
selfName: string,
|
|
58
|
+
onRefused?: (name: string, reason: 'call-target' | 'self-name') => void,
|
|
59
|
+
): SymbolRef[] {
|
|
37
60
|
const called = new Set<string>();
|
|
38
61
|
const valueRefs = new Set<string>();
|
|
39
62
|
const visitExpr = (e: Expr): void => {
|
|
@@ -55,7 +78,17 @@ export function collectSymbolRefs(body: Stmt[], symbols: Map<string, SymbolInfo>
|
|
|
55
78
|
};
|
|
56
79
|
body.forEach(visitStmt);
|
|
57
80
|
return [...valueRefs]
|
|
58
|
-
.filter((n) => !called.has(n) && n !== selfName)
|
|
59
81
|
.sort()
|
|
82
|
+
.filter((n) => {
|
|
83
|
+
if (called.has(n)) {
|
|
84
|
+
onRefused?.(n, 'call-target');
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
if (n === selfName) {
|
|
88
|
+
onRefused?.(n, 'self-name');
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
return true;
|
|
92
|
+
})
|
|
60
93
|
.map((n) => ({ name: n, info: symbols.get(n)! }));
|
|
61
94
|
}
|
package/src/l3/tailmerge.ts
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
// above covers only below-vs-in-arms.
|
|
26
26
|
//
|
|
27
27
|
// KNOWN INTERACTIONS, both byte-level rather than soundness. This pass is unconditional like
|
|
28
|
-
// `dce.ts` and `basecse.ts` rather than a differ-refereed
|
|
28
|
+
// `dce.ts` and `basecse.ts` rather than a differ-refereed variation, and the argument those files each
|
|
29
29
|
// state for themselves applies here too and was missing: a wrong merge changes recompiled bytes and
|
|
30
30
|
// surfaces as a LOST match under the zero-lost gate, never as wrong C.
|
|
31
31
|
//
|
|
@@ -37,6 +37,21 @@
|
|
|
37
37
|
// of the arms hides the common tail, and DCE only removes it afterwards, so the shape this pass
|
|
38
38
|
// exists for is missed. The fix is a fixpoint of the pair, not one extra call — a lone second
|
|
39
39
|
// pass leaves an empty `if` behind.
|
|
40
|
+
// - THE SAME HIDING HAPPENS WITHOUT DCE, and the edge-copy sort decides when. An arm ends in the
|
|
41
|
+
// copies of one CFG edge, ordered by the predecessor's write record or by the def-position
|
|
42
|
+
// proxy (`structure.ts`); an arm-varying copy ordered LAST hides an agreeing one behind it.
|
|
43
|
+
// Measured on klonoa `CountCollectedGems` (map-less, agbcc): the record's order leaves six
|
|
44
|
+
// copies of `v22 = (s32 *)50345232;` where the proxy's order merges them into one, and with
|
|
45
|
+
// THIS PASS disabled both orders emit all six — so the duplication is this peel not firing,
|
|
46
|
+
// not something the sort creates. Reaching the hidden statement is not a matter of peeling
|
|
47
|
+
// further: it would have to move ACROSS the differing one, which needs the independence
|
|
48
|
+
// argument this pass deliberately does not have (see the soundness note above). Pinned both
|
|
49
|
+
// ways in `test/tailmerge.test.ts`.
|
|
50
|
+
// A ranked row is not stuck with either order: `/copy-defpos` (rank.ts) enumerates the
|
|
51
|
+
// def-position spelling beside the record's, so the merged form is a candidate the differ can
|
|
52
|
+
// pick on bytes — on `CountCollectedGems` its gate admits the variation on the map-ful and map-less
|
|
53
|
+
// lifts alike. What no arm of the fan spells is the third form, the hidden statement merged
|
|
54
|
+
// while KEEPING the record's order.
|
|
40
55
|
//
|
|
41
56
|
// SCOPE. Only `assign`/`store`/`exprstmt` merge, compared structurally through `exprEquals`.
|
|
42
57
|
// Control flow (`break`/`continue`/`return`) is excluded: moving one out of an arm changes which
|
package/src/l3/typing.ts
CHANGED
|
@@ -18,10 +18,10 @@
|
|
|
18
18
|
// provably a pointer" (adds a cast — valid C either way); the deref contract treats `undefined`
|
|
19
19
|
// as "not provably wrong" (no error).
|
|
20
20
|
import { IrType, T, scalarTypeForAccess } from '../ir/types';
|
|
21
|
-
import type
|
|
21
|
+
import { type Expr, type SFn, exprChildren } from './ast';
|
|
22
22
|
|
|
23
23
|
/** The declared type of a printed variable — the env `exprCType` judges rendered C against.
|
|
24
|
-
* THE one copy of the SFn→env derivation
|
|
24
|
+
* THE one copy of the SFn→env derivation — printers, contracts and L3 respell variations alike: each
|
|
25
25
|
* consumer judging against anything but the declarations it emits would let them disagree. */
|
|
26
26
|
export type VarTypes = (name: string) => IrType | undefined;
|
|
27
27
|
|
|
@@ -48,22 +48,211 @@ export function ptrElemBytes(to: IrType): number {
|
|
|
48
48
|
return to.kind === 'int' ? to.width / 8 : to.kind === 'ptr' ? 4 : 0;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
/**
|
|
52
|
-
* a pointer
|
|
51
|
+
/** The env a C-family printer judges rendered C against: the declared types, plus which printed
|
|
52
|
+
* vars render as a pointer to VOLATILE. IrType models no cv-qualifier (see the `cast` node doc in
|
|
53
|
+
* l3/ast.ts), so a printer that RE-TYPES an access — the deref legalization — has to read the
|
|
54
|
+
* qualifier from here or the re-typing silently drops it. */
|
|
55
|
+
export interface PrintEnv {
|
|
56
|
+
readonly type: VarTypes;
|
|
57
|
+
readonly volatilePointee: (name: string) => boolean;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function printEnv(fn: SFn): PrintEnv {
|
|
61
|
+
// both flags, because the C declarator prints ONE `volatile ` prefix for a pointer local and it
|
|
62
|
+
// binds to the pointee either way (the declaration loop in backend/cfamily.ts)
|
|
63
|
+
const vol = new Set(
|
|
64
|
+
fn.locals.filter((l) => l.type.kind === 'ptr' && (l.pointeeVolatile || l.volatile)).map((l) => l.name),
|
|
65
|
+
);
|
|
66
|
+
return { type: declaredTypes(fn), volatilePointee: (n) => vol.has(n) };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Does anything under `e` assert a VOLATILE POINTEE? The two spellings that carry the qualifier
|
|
70
|
+
* into printed C are a `volatile` cast node and a volatile-pointee declaration. The whole subtree
|
|
71
|
+
* is searched, not the pointer spine: a cast to an integer type between the assertion and the
|
|
72
|
+
* access — `(u32)p + 4` — renders the qualifier no less dropped, and over-qualifying an access
|
|
73
|
+
* only restricts the compiler further, where under-qualifying is a lie about MMIO. */
|
|
74
|
+
export function assertsVolatile(e: Expr, env: PrintEnv): boolean {
|
|
75
|
+
return (
|
|
76
|
+
(e.k === 'cast' && e.volatile === true) ||
|
|
77
|
+
(e.k === 'var' && env.volatilePointee(e.name)) ||
|
|
78
|
+
exprChildren(e).some((c) => assertsVolatile(c, env))
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** May a `width`-byte access of the given signedness dereference a base of rendered C type `rt` AS
|
|
83
|
+
* SPELLED — i.e. is `rt` a pointer/array whose element size equals the access width, and whose
|
|
84
|
+
* element extends the way the access does? THE one copy of the stride rule:
|
|
53
85
|
* the C-family printer decides cast insertion from it, the Pascal backend decides declining from
|
|
54
86
|
* it, and exprCType types the access result from it. `false` for a non-pointer, an unknowable
|
|
55
87
|
* base (undefined), or a pointer of the WRONG stride — a wrong-stride deref would make C read
|
|
56
88
|
* the wrong width and scale the index by the wrong element size. */
|
|
57
|
-
export function derefStrideOk(rt: IrType | undefined, width: number): boolean {
|
|
89
|
+
export function derefStrideOk(rt: IrType | undefined, width: number, signed: boolean): boolean {
|
|
90
|
+
// SIGNEDNESS COUNTS WHEREVER THE ACCESS EXTENDS. A sub-word load fills the top bits from either
|
|
91
|
+
// the sign bit or zero, and in the emitted C the pointee type is the ONLY thing that says which —
|
|
92
|
+
// so a base of the wrong signedness has to take the reinterpret cast exactly as a base of the
|
|
93
|
+
// wrong stride does. Read `*(u8 *)p` where the machine did `ldrsb` and `p[9] < 0` is not merely a
|
|
94
|
+
// different value: an `unsigned char` promotes to a non-negative `int`, so the arm goes dead.
|
|
95
|
+
// A word access extends nothing, and a POINTER pointee is a word.
|
|
96
|
+
const extendsOk = (to: IrType) => width === 4 || (to.kind === 'int' && to.signed === signed);
|
|
58
97
|
if (rt?.kind === 'ptr') {
|
|
59
|
-
return rt.to.kind !== 'struct' && ptrElemBytes(rt.to) === width;
|
|
98
|
+
return rt.to.kind !== 'struct' && ptrElemBytes(rt.to) === width && extendsOk(rt.to);
|
|
60
99
|
}
|
|
61
100
|
if (rt?.kind === 'array') {
|
|
62
|
-
return rt.elem.kind === 'int' && rt.elem.width === width * 8;
|
|
101
|
+
return rt.elem.kind === 'int' && rt.elem.width === width * 8 && extendsOk(rt.elem);
|
|
63
102
|
}
|
|
64
103
|
return false;
|
|
65
104
|
}
|
|
66
105
|
|
|
106
|
+
/** The USUAL ARITHMETIC CONVERSIONS over two rendered operands: at equal rank, unsigned wins.
|
|
107
|
+
* Either side unknown leaves the result unknown — EXCEPT when the known side is unsigned, which
|
|
108
|
+
* already decides it.
|
|
109
|
+
*
|
|
110
|
+
* That exception is the one place this returns a DEFINITE answer from an unknown operand, and it
|
|
111
|
+
* is sound only because every integer here is rank `int`: at UNEQUAL rank C converts to the wider
|
|
112
|
+
* type first, so `unsigned int & long long` is SIGNED. Core has no 64-bit integer type at all
|
|
113
|
+
* (the decomp typedef vocabulary stops at 32 — see contracts.ts SCALAR_WIDTHS), so the
|
|
114
|
+
* unequal-rank case cannot arise. Adding one would invalidate this.
|
|
115
|
+
*
|
|
116
|
+
* Exported because two consumers ask it about an operator whose own rendering they are deciding,
|
|
117
|
+
* so they cannot ask `renderedIntSignedness` about the node: the C-family backend's operand pin,
|
|
118
|
+
* and initfirst's compare-meaning gate over a substitution not yet made. */
|
|
119
|
+
export function arithConversionSignedness(l: Expr, r: Expr, varType: VarTypes): boolean | undefined {
|
|
120
|
+
const ls = renderedIntSignedness(l, varType);
|
|
121
|
+
const rs = renderedIntSignedness(r, varType);
|
|
122
|
+
if (ls === false || rs === false) {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
return ls === true && rs === true ? true : undefined;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* The C SIGNEDNESS a rendered integer expression actually has — `true`/`false`, or `undefined`
|
|
130
|
+
* when it is not determinable here. The deliberate complement to `exprCType`, which is
|
|
131
|
+
* pointer-ness-accurate and reports every integer as `s32` by contract; this models the two C
|
|
132
|
+
* rules that contract omits, integer PROMOTION and the usual arithmetic CONVERSIONS.
|
|
133
|
+
*
|
|
134
|
+
* THE one rendered-signedness judgment, and it lives beside the declarations it judges against
|
|
135
|
+
* because every consumer shares it: the C-family backend's operand pin, structure.ts's
|
|
136
|
+
* unsigned-compare gate (the /uns-cmp variation) and its signed-compare pin, and — through
|
|
137
|
+
* `arithConversionSignedness` above — initfirst's compare-meaning gate. Two of those models
|
|
138
|
+
* disagreeing about one expression is the drift this placement prevents.
|
|
139
|
+
*
|
|
140
|
+
* The question is byte-load-bearing: C spells both `>>>` and `>>` as `>>` and chooses between them
|
|
141
|
+
* from the left operand's type, so a logical shift rendered over a signed expression recompiles to
|
|
142
|
+
* `asr` where the target has `lsr` and evaluates to a different value.
|
|
143
|
+
*
|
|
144
|
+
* `undefined` means the model did not reach, never "possibly signed": what a `call` renders as is
|
|
145
|
+
* decided by the prototype in scope, and the prototypes in scope are the PROJECT's — the scoring
|
|
146
|
+
* context prepends its headers, and those do declare unsigned-returning callees. So every consumer
|
|
147
|
+
* whose operator needs a definite signedness takes a cast on `undefined` exactly as on the wrong
|
|
148
|
+
* definite answer. A redundant cast is codegen-identical (`(s32)f() / a` and `f() / a` assemble
|
|
149
|
+
* instruction-for-instruction the same where `f` really returns `int`); a missing one is a
|
|
150
|
+
* miscompile — with `u32 f(void);` in scope `f() / a` calls `__udivsi3` where the machine called
|
|
151
|
+
* `__divsi3`, and `if (f() >= 0)` compiles to `bl f; mov r0, #0`, comparison and both arms gone.
|
|
152
|
+
* The one consumer that reads `undefined` the other way is the /uns-cmp variation's own gate, which is
|
|
153
|
+
* asking whether an operand is ALREADY unsigned.
|
|
154
|
+
*
|
|
155
|
+
* Anything narrower than 32 bits promotes to `int` and is therefore SIGNED, whatever it was
|
|
156
|
+
* declared. Pointers, calls and markers are `undefined`.
|
|
157
|
+
*/
|
|
158
|
+
export function renderedIntSignedness(e: Expr, varType: VarTypes): boolean | undefined {
|
|
159
|
+
const rec = (x: Expr): boolean | undefined => renderedIntSignedness(x, varType);
|
|
160
|
+
// an lvalue-ish leaf: its C type is a declaration / an explicit cast / a carried access width
|
|
161
|
+
const promoted = (t: IrType | undefined): boolean | undefined =>
|
|
162
|
+
t?.kind !== 'int' ? undefined : t.width < 32 ? true : t.width === 32 ? t.signed : undefined;
|
|
163
|
+
switch (e.k) {
|
|
164
|
+
case 'var':
|
|
165
|
+
case 'cast':
|
|
166
|
+
case 'index':
|
|
167
|
+
case 'field':
|
|
168
|
+
return promoted(exprCType(e, varType));
|
|
169
|
+
// A decimal literal is `int` when it fits in one, and C89 gives a larger one an UNSIGNED type
|
|
170
|
+
// — agbcc says so itself ("decimal constant is so large that it is unsigned"). INT_MIN is in
|
|
171
|
+
// that class despite fitting: the backend prints it as `-2147483648`, which C lexes as unary
|
|
172
|
+
// minus applied to `2147483648`. Definite, not undetermined, and it drags the whole
|
|
173
|
+
// expression with it: `a / -2147483648` compiles to `lsr r0, r0, #0x1f` — an unsigned divide
|
|
174
|
+
// folded to a shift — where `a / (s32)-2147483648` calls `__divsi3`.
|
|
175
|
+
case 'const':
|
|
176
|
+
return e.value > -2147483648 && e.value <= 2147483647;
|
|
177
|
+
// `-x` / `~x` carry the PROMOTED type of the operand; `!x` is `int`.
|
|
178
|
+
case 'un':
|
|
179
|
+
return e.op === '!' ? true : rec(e.e);
|
|
180
|
+
case 'bin': {
|
|
181
|
+
// The SIGNEDNESS-CARRYING pairs — the ops the tower keeps apart because C spells each pair
|
|
182
|
+
// with one token and picks between them from the operand types (l3/ast.ts BinOp). The
|
|
183
|
+
// C-family backend PINS their operands, so what gets printed renders as the op says
|
|
184
|
+
// whatever the operands would have rendered as on their own. Reporting the operands here
|
|
185
|
+
// instead would answer about an expression nobody prints: a `>>` over a u32 var prints
|
|
186
|
+
// `(s32)v >> 3` and is signed, and a consumer told otherwise omits its own cast.
|
|
187
|
+
if (e.op === '>>' || e.op === '/' || e.op === '%') {
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
if (e.op === '>>>' || e.op === '/u' || e.op === '%u') {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
// `<<` is sign-blind, so it keeps its left operand's type — nothing pins it.
|
|
194
|
+
if (e.op === '<<') {
|
|
195
|
+
return rec(e.l);
|
|
196
|
+
}
|
|
197
|
+
// Comparisons and the logical connectives yield `int`.
|
|
198
|
+
if (['<', '<=', '>', '>=', '==', '!=', '&&', '||'].includes(e.op)) {
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
return arithConversionSignedness(e.l, e.r, varType);
|
|
202
|
+
}
|
|
203
|
+
case 'call':
|
|
204
|
+
case 'marker':
|
|
205
|
+
case 'addr':
|
|
206
|
+
return undefined;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Is the rendered expression provably in [0, 2^31) — the range where a signed and an unsigned
|
|
211
|
+
* compare agree on every input (and where gcc itself emits the unsigned branch for the signed
|
|
212
|
+
* spelling)? Narrow unsigned values are the everyday case: a `(u8)x` cast or a `u8`/`u16`
|
|
213
|
+
* deref promotes to a non-negative `int`. Conservative false elsewhere. */
|
|
214
|
+
export function provablyNonNegative(e: Expr, varType: VarTypes): boolean {
|
|
215
|
+
switch (e.k) {
|
|
216
|
+
case 'const':
|
|
217
|
+
return e.value >= 0 && e.value < 0x80000000;
|
|
218
|
+
case 'un':
|
|
219
|
+
return e.op === '!';
|
|
220
|
+
case 'bin':
|
|
221
|
+
return ['<', '<=', '>', '>=', '==', '!=', '&&', '||'].includes(e.op);
|
|
222
|
+
case 'cast':
|
|
223
|
+
return e.to.kind === 'int' && e.to.width < 32 && !e.to.signed;
|
|
224
|
+
default: {
|
|
225
|
+
const t = exprCType(e, varType);
|
|
226
|
+
return t?.kind === 'int' && t.width < 32 && !t.signed;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Does this write put a value that renders as a DEFINITELY-NON-POINTER C expression into a
|
|
232
|
+
* pointer-declared slot? — `v2 = a1 + v0` with `v2: u8 *`, a `return a0 + v0` from a
|
|
233
|
+
* pointer-returning function, a store through a pointer-element lvalue. The machine's register
|
|
234
|
+
* move says nothing about types; C does, and mwcc makes it an ERROR (gcc merely warns).
|
|
235
|
+
*
|
|
236
|
+
* ONE question, TWO answers: the C family legalizes it with the reinterpret cast to the declared
|
|
237
|
+
* type (backend/cfamily.ts), Pascal has no reinterpret cast and declines LOUD (backend/pascal.ts).
|
|
238
|
+
* The question is shared so the two backends cannot come to disagree about WHICH writes are the
|
|
239
|
+
* hazard while still disagreeing about what to do with one.
|
|
240
|
+
*
|
|
241
|
+
* A rendering the model cannot reach (a call, whose C type comes from a prototype outside the
|
|
242
|
+
* emitted function) is NOT the hazard: the answer has to be definite in the non-pointer
|
|
243
|
+
* direction. A type predicate so the cast site keeps its narrowing on the declared type. */
|
|
244
|
+
export function writesNonPointerIntoPointer(
|
|
245
|
+
declared: IrType | undefined,
|
|
246
|
+
value: Expr,
|
|
247
|
+
varType: VarTypes,
|
|
248
|
+
): declared is Extract<IrType, { kind: 'ptr' }> {
|
|
249
|
+
if (declared?.kind !== 'ptr') {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
const ct = exprCType(value, varType);
|
|
253
|
+
return ct !== undefined && ct.kind !== 'ptr' && ct.kind !== 'array';
|
|
254
|
+
}
|
|
255
|
+
|
|
67
256
|
export function exprCType(e: Expr, varType: (name: string) => IrType | undefined): IrType | undefined {
|
|
68
257
|
const rec = (x: Expr): IrType | undefined => exprCType(x, varType);
|
|
69
258
|
switch (e.k) {
|
|
@@ -121,10 +310,10 @@ export function exprCType(e: Expr, varType: (name: string) => IrType | undefined
|
|
|
121
310
|
if (bt?.kind === 'ptr' && bt.to.kind === 'struct' && (bt.to.size === undefined || bt.to.size === e.width)) {
|
|
122
311
|
return bt.to;
|
|
123
312
|
}
|
|
124
|
-
if (bt?.kind === 'ptr' && derefStrideOk(bt, e.width)) {
|
|
313
|
+
if (bt?.kind === 'ptr' && derefStrideOk(bt, e.width, e.signed)) {
|
|
125
314
|
return bt.to;
|
|
126
315
|
}
|
|
127
|
-
if (bt?.kind === 'array' && derefStrideOk(bt, e.width)) {
|
|
316
|
+
if (bt?.kind === 'array' && derefStrideOk(bt, e.width, e.signed)) {
|
|
128
317
|
return bt.elem;
|
|
129
318
|
}
|
|
130
319
|
return scalarTypeForAccess(e.width, e.signed);
|