@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/raise/structs.ts
CHANGED
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
// gap fields the compiler had but this function never touched)
|
|
19
19
|
// base @ {off2 w4} -> LOUD decline (a 4-byte field at offset 2 is not 4-aligned —
|
|
20
20
|
// natural C alignment cannot place it there)
|
|
21
|
+
// base @ {off0 w4, off2 w2} -> struct { union { s32 word; s16 half[2]; } field_0; } (the same
|
|
22
|
+
// bytes at two widths — `buildUnionStruct`)
|
|
21
23
|
// base @ {off0 w4, off4 w4} -> array (uniform stride — untouched)
|
|
22
24
|
// base @ {off8 w4} -> array (single aligned access — no struct evidence)
|
|
23
25
|
// base @ aload(index) -> array (variable index — untouched)
|
|
@@ -42,19 +44,98 @@
|
|
|
42
44
|
// struct reproduces the observed offsets byte-for-byte and is self-describing (the same
|
|
43
45
|
// discipline raise/struct-arrays.ts withPadding uses). Each accessed field must still be
|
|
44
46
|
// naturally aligned to ITS OWN width (`off % width === 0`) — a genuinely packed layout (a field
|
|
45
|
-
// at an offset natural C alignment could not place it at) is rejected LOUD
|
|
46
|
-
//
|
|
47
|
-
|
|
47
|
+
// at an offset natural C alignment could not place it at) is rejected LOUD. An overlap is a union
|
|
48
|
+
// member on a base whose layout only this function's accesses describe, and no struct at all on
|
|
49
|
+
// one whose address is declared elsewhere (`recognizeStructs`).
|
|
50
|
+
import { constAddressOf, globalCellOf } from '../ir/alias';
|
|
51
|
+
import { Fn, Op, Value, defOpMap } from '../ir/core';
|
|
48
52
|
import { nextStructIndex } from '../ir/struct-names';
|
|
49
53
|
import { IrType, StructField, T, scalarTypeForAccess } from '../ir/types';
|
|
50
54
|
import type { StructType } from '../l3/ast';
|
|
51
|
-
import { RaiseUnsupportedError } from './errors';
|
|
55
|
+
import { RaiseUnsupportedError, StructOverlapError } from './errors';
|
|
52
56
|
|
|
53
|
-
|
|
54
|
-
|
|
57
|
+
/** A single observed constant-offset access to a base: the op itself and its program point, its
|
|
58
|
+
* byte offset, its access width (bytes), and the signedness a recovered field would take from it.
|
|
59
|
+
*
|
|
60
|
+
* SHARED WITH raise/truncload.ts, which folds a narrow read into a cast of a wider one before this
|
|
61
|
+
* pass runs. The two ask the same question of a function — "what does this base's access set look
|
|
62
|
+
* like" — and a second copy of the walk would let them come to disagree about the answer while
|
|
63
|
+
* each looked right on its own.
|
|
64
|
+
*
|
|
65
|
+
* SIGNEDNESS ON THE STORE HALF IS A POLICY, not a reading: a store carries none, and a word store
|
|
66
|
+
* makes an `s32` field where a narrower one makes an unsigned one. `buildStruct` then prefers a
|
|
67
|
+
* LOAD's own signedness at the same offset, which is the reading. raise/truncload.ts never sees
|
|
68
|
+
* this field — its `covering-store` rule refuses a store cover before the signedness is used. */
|
|
69
|
+
export interface Access {
|
|
70
|
+
op: Op;
|
|
71
|
+
/** index into `fn.blocks`, for the dominance question raise/truncload.ts asks. */
|
|
72
|
+
block: number;
|
|
73
|
+
/** index into that block's `ops`, as they stood when this was collected. */
|
|
74
|
+
at: number;
|
|
55
75
|
off: number;
|
|
56
76
|
width: number;
|
|
57
77
|
signed: boolean;
|
|
78
|
+
isLoad: boolean;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** What one walk of a function yields about its constant-offset memory accesses. */
|
|
82
|
+
export interface ConstOffsetAccesses {
|
|
83
|
+
/** every base's constant-offset accesses, in program order. */
|
|
84
|
+
accessesOf: Map<Value, Access[]>;
|
|
85
|
+
/** the bases, in FIRST-APPEARANCE order → deterministic struct names. */
|
|
86
|
+
order: Value[];
|
|
87
|
+
/** bases a VARIABLE-index access also reaches (`aload`/`astore`) — arrays, never struct-recovered. */
|
|
88
|
+
arrayBases: Set<Value>;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Collect every base's constant-offset accesses, plus the bases a variable-index access reaches.
|
|
92
|
+
* ONE walk, two consumers: `recognizeStructs` below and raise/truncload.ts. */
|
|
93
|
+
export function constOffsetAccesses(fn: Fn): ConstOffsetAccesses {
|
|
94
|
+
const accessesOf = new Map<Value, Access[]>();
|
|
95
|
+
const arrayBases = new Set<Value>();
|
|
96
|
+
const order: Value[] = [];
|
|
97
|
+
const note = (base: Value, a: Access) => {
|
|
98
|
+
let list = accessesOf.get(base);
|
|
99
|
+
if (!list) {
|
|
100
|
+
list = [];
|
|
101
|
+
accessesOf.set(base, list);
|
|
102
|
+
order.push(base);
|
|
103
|
+
}
|
|
104
|
+
list.push(a);
|
|
105
|
+
};
|
|
106
|
+
fn.blocks.forEach((b, block) => {
|
|
107
|
+
(b.ops as Op[]).forEach((op, at) => {
|
|
108
|
+
switch (op.opcode) {
|
|
109
|
+
case 'load':
|
|
110
|
+
note(op.operands[0], {
|
|
111
|
+
op,
|
|
112
|
+
block,
|
|
113
|
+
at,
|
|
114
|
+
off: op.attrs.off as number,
|
|
115
|
+
width: op.attrs.width as number,
|
|
116
|
+
signed: op.attrs.signed as boolean,
|
|
117
|
+
isLoad: true,
|
|
118
|
+
});
|
|
119
|
+
break;
|
|
120
|
+
case 'store':
|
|
121
|
+
note(op.operands[0], {
|
|
122
|
+
op,
|
|
123
|
+
block,
|
|
124
|
+
at,
|
|
125
|
+
off: op.attrs.off as number,
|
|
126
|
+
width: op.attrs.width as number,
|
|
127
|
+
signed: (op.attrs.width as number) === 4,
|
|
128
|
+
isLoad: false,
|
|
129
|
+
});
|
|
130
|
+
break;
|
|
131
|
+
case 'aload':
|
|
132
|
+
case 'astore':
|
|
133
|
+
arrayBases.add(op.operands[0]);
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
return { accessesOf, order, arrayBases };
|
|
58
139
|
}
|
|
59
140
|
|
|
60
141
|
// Natural C size/alignment of a recovered scalar field type (all fields here are int/ptr ≤ 4 bytes,
|
|
@@ -78,10 +159,30 @@ function isArray(accesses: Access[]): boolean {
|
|
|
78
159
|
|
|
79
160
|
/** Build the struct type for a base whose accesses are NOT array-shaped. Unaccessed leading/
|
|
80
161
|
* interior gaps are FILLED with `u8[N]` pads so the declared struct reproduces the observed
|
|
81
|
-
* offsets. Throws LOUD only on a layout
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
162
|
+
* offsets. Throws LOUD only on a layout no plain struct reproduces: two accesses overlapping in
|
|
163
|
+
* bytes (same offset with differing widths, OR distinct offsets whose ranges collide), or a field
|
|
164
|
+
* at an offset its own natural alignment could not place it at (a PACKED layout).
|
|
165
|
+
*
|
|
166
|
+
* ONE CLASS OF OVERLAP NEVER REACHES HERE: a narrow LOAD covering the low-order end of a wider
|
|
167
|
+
* access that ran before it on every path is a cast of that access rather than a second field, and
|
|
168
|
+
* raise/truncload.ts folds it into one before this pass runs. What is left is what the asm does
|
|
169
|
+
* not settle — a narrow STORE, a read above the low-order end, a cell at a constant address, and a
|
|
170
|
+
* cover on a path the narrow read does not run — and every one of them throws here.
|
|
171
|
+
*
|
|
172
|
+
* A THROW IS NOT THE FUNCTION'S VERDICT, AND WHICH THROW IT IS DECIDES THAT. Three throw sites
|
|
173
|
+
* below reject a layout and they fall into two CLASSES, which is the split a caller reads:
|
|
174
|
+
* • `StructOverlapError` — two accesses whose byte ranges collide. TWO of the three sites raise
|
|
175
|
+
* it, and they carry the same "unions not modelled" text at different offsets: a second access
|
|
176
|
+
* at an offset already taken with a different width, and one whose range straddles the field
|
|
177
|
+
* before it (the `aligned > f.off` arm, which is the only one that can see `{s32@0, s16@2}`).
|
|
178
|
+
* Each access alone is spellable at its own offset and width, so a base whose ADDRESS is
|
|
179
|
+
* declared outside this function's access set can keep its untyped spelling and lift
|
|
180
|
+
* (`recognizeStructs` below). There is no struct to synthesize, and nothing is lost by not
|
|
181
|
+
* synthesizing one. Any other base is declared with a union member (`buildUnionStruct`).
|
|
182
|
+
* • `RaiseUnsupportedError` — a field at an offset its own natural alignment could not place it
|
|
183
|
+
* at. This one has NO per-access spelling downstream: an access at `off` under a wider access's
|
|
184
|
+
* width folds to the element index `off / width` (structure/structure.ts), which for a
|
|
185
|
+
* misaligned pair is a FRACTION and not C. It declines whatever the base is. */
|
|
85
186
|
function buildStruct(name: string, accesses: Access[]): IrType {
|
|
86
187
|
// One field per distinct offset; a load's signedness wins over a store's (more information).
|
|
87
188
|
const byOff = new Map<number, Access>();
|
|
@@ -92,7 +193,7 @@ function buildStruct(name: string, accesses: Access[]): IrType {
|
|
|
92
193
|
continue;
|
|
93
194
|
}
|
|
94
195
|
if (prev.width !== a.width) {
|
|
95
|
-
throw new
|
|
196
|
+
throw new StructOverlapError(
|
|
96
197
|
`cannot recover struct '${name}': overlapping fields at offset ${a.off} (widths ${prev.width} and ${a.width}) — unions not modelled`,
|
|
97
198
|
);
|
|
98
199
|
}
|
|
@@ -108,31 +209,38 @@ function buildStruct(name: string, accesses: Access[]): IrType {
|
|
|
108
209
|
// cannot reproduce, so it is rejected LOUD (never a silently-wrong struct). The GAP before a
|
|
109
210
|
// field (an unaccessed leading/interior member) is legal: it is filled with a `u8[N]` pad below.
|
|
110
211
|
for (const f of dataFields) {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
212
|
+
refusePacked(name, f.off, fieldSize(f.type));
|
|
213
|
+
}
|
|
214
|
+
return placeFields(name, dataFields);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function refusePacked(name: string, off: number, size: number): void {
|
|
218
|
+
if (off % sizeAlign(size) !== 0) {
|
|
219
|
+
throw new RaiseUnsupportedError(
|
|
220
|
+
`cannot recover struct '${name}': field at offset ${off} (width ${size}) is not naturally aligned — packed layout not modelled`,
|
|
221
|
+
);
|
|
117
222
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Seat `dataFields` (sorted by offset) under natural C alignment, inserting an explicit `u8[N]`
|
|
226
|
+
* PAD only for a gap the alignment itself does NOT already cover (the same self-describing
|
|
227
|
+
* discipline as raise/struct-arrays.ts withPadding). For each field, `aligned` = where natural C
|
|
228
|
+
* alignment would put it after the running cursor:
|
|
229
|
+
* • aligned === off — natural padding lands it exactly (`{s8@0, s32@4}`): no explicit pad,
|
|
230
|
+
* C's own inter-field alignment reproduces the layout.
|
|
231
|
+
* • aligned < off — a leading/interior gap alignment can't fill (`{s16@2, s32@4}`, byte 0–1
|
|
232
|
+
* never read): insert a `u8[off - cursor]` pad so the field lands exactly.
|
|
233
|
+
* • aligned > off — the field's offset precedes where alignment would force it: it OVERLAPS
|
|
234
|
+
* the prior field (`{s32@0, s16@2}` — a union view the same-offset byOff check cannot see):
|
|
235
|
+
* reject LOUD, never a silently-mislaid field. */
|
|
236
|
+
function placeFields(name: string, dataFields: StructField[]): IrType {
|
|
129
237
|
const fields: StructField[] = [];
|
|
130
238
|
let cursor = 0;
|
|
131
239
|
let pad = 0;
|
|
132
240
|
for (const f of dataFields) {
|
|
133
|
-
const aligned = roundUp(cursor, sizeAlign(
|
|
241
|
+
const aligned = roundUp(cursor, sizeAlign(fieldSize(f.type)));
|
|
134
242
|
if (aligned > f.off) {
|
|
135
|
-
throw new
|
|
243
|
+
throw new StructOverlapError(
|
|
136
244
|
`cannot recover struct '${name}': field at offset ${f.off} overlaps the prior field (aligned to ${aligned}) — unions not modelled`,
|
|
137
245
|
);
|
|
138
246
|
}
|
|
@@ -140,69 +248,144 @@ function buildStruct(name: string, accesses: Access[]): IrType {
|
|
|
140
248
|
fields.push({ off: cursor, type: T.array(T.u(8), f.off - cursor), name: `_pad${pad++}` });
|
|
141
249
|
}
|
|
142
250
|
fields.push(f);
|
|
143
|
-
cursor = f.off +
|
|
251
|
+
cursor = f.off + fieldSize(f.type);
|
|
144
252
|
}
|
|
145
253
|
return T.struct(name, fields);
|
|
146
254
|
}
|
|
147
255
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
256
|
+
/** The member name of each view a union holds, by access width. A width with no entry has no view
|
|
257
|
+
* (the deref contract admits no other scalar width either), and its overlap still declines. */
|
|
258
|
+
const VIEW_NAMES: Readonly<Record<number, string>> = { 1: 'byte', 2: 'half', 4: 'word' };
|
|
152
259
|
|
|
153
|
-
/**
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
for
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
260
|
+
/** The struct for a base whose accesses OVERLAP — `buildStruct` refused it with
|
|
261
|
+
* `StructOverlapError` — with each overlap declared as a UNION member: the same bytes read or
|
|
262
|
+
* written at more than one width are views of one cell, and a union is the C that says so.
|
|
263
|
+
*
|
|
264
|
+
* NOT A CAST, and the compiler is why. `((u16 *)p)[k]` beside `*p` spells the same two accesses,
|
|
265
|
+
* but a cast-punned access is outside C's aliasing rules and agbcc (-O2) applies them: a narrow
|
|
266
|
+
* read, a word store and the same narrow read again compiles, through casts, to ONE `ldrh` whose
|
|
267
|
+
* value is reused, where the union spelling reloads after the `str` — as the asm the union was
|
|
268
|
+
* compiled from does (`synthetic:ureread`). gcc2.7.2kmc, ido7.1 and mwcc_242_81 reload for both.
|
|
269
|
+
* The direction is one-way: a union access aliases every view of its cell, so the union spelling
|
|
270
|
+
* keeps every access the asm performed and the cast cannot be the better candidate — which is why
|
|
271
|
+
* this is the default and not a variation.
|
|
272
|
+
*
|
|
273
|
+
* THE LAYOUT. Every access must be naturally aligned (a packed one declines, as in
|
|
274
|
+
* `buildStruct`), and access widths are powers of two, so an aligned access never straddles the
|
|
275
|
+
* boundary of a wider aligned one: two accesses overlap only when the wider CONTAINS the narrower.
|
|
276
|
+
* A cell is therefore one widest access and every access inside it, and it gets a VIEW per width —
|
|
277
|
+
* `word`, `half`, `byte` — and, for a narrow width LOADED with both extensions, per extension —
|
|
278
|
+
* `uhalf` and `shalf` — because a view's type IS its extension and one view for both reads one of
|
|
279
|
+
* them wrong. A store carries no extension and writes through its width's only view, or the
|
|
280
|
+
* unsigned one where there are two. A view that holds
|
|
281
|
+
* one element at the cell's start is a scalar; any other is an array reaching the furthest
|
|
282
|
+
* element accessed (`u16 half[2]` for the halfword at +2). A cell with ONE view is a plain field;
|
|
283
|
+
* any other is a member `field_<off>` holding a union of its views — including a single-width cell
|
|
284
|
+
* loaded both ways. KNOWN GAP: on a base with no overlap anywhere, `buildStruct` never hands over,
|
|
285
|
+
* and that same cell there is one field typed by its signed load, so its unsigned read is spelled sign-extended.
|
|
286
|
+
*
|
|
287
|
+
* THE COMPILER DECIDES THE UNION'S SIZE, not its widest view. `aggregateBoundary` is the size it
|
|
288
|
+
* aligns and rounds every struct and union to (target.ts `compilerBehaviors.aggregateBoundary`):
|
|
289
|
+
* agbcc makes `union { u16 h; u8 b; }` four bytes, four-aligned, where the other compilers make
|
|
290
|
+
* it two. A union that boundary would move — seated off it, or with the next field inside its
|
|
291
|
+
* rounded size — declines rather than mislaying every access at or after it. An UNMEASURED
|
|
292
|
+
* compiler (undefined) declines every union, since no boundary is safe to assume: one too small
|
|
293
|
+
* mislays the fields after the union on agbcc, one too large drops the pad in front of them
|
|
294
|
+
* everywhere else.
|
|
295
|
+
*
|
|
296
|
+
* A cell at a width no view is named for declines; the frontends emit none. */
|
|
297
|
+
function buildUnionStruct(structName: string, accesses: Access[], aggregateBoundary: number | undefined): IrType {
|
|
298
|
+
for (const a of accesses) {
|
|
299
|
+
refusePacked(structName, a.off, a.width);
|
|
300
|
+
}
|
|
301
|
+
const clusters: Access[][] = [];
|
|
302
|
+
for (const a of [...accesses].sort((x, y) => x.off - y.off || y.width - x.width)) {
|
|
303
|
+
const cur = clusters.at(-1);
|
|
304
|
+
if (cur !== undefined && a.off < cur[0].off + cur[0].width) {
|
|
305
|
+
cur.push(a);
|
|
306
|
+
} else {
|
|
307
|
+
clusters.push([a]);
|
|
193
308
|
}
|
|
194
309
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
310
|
+
const dataFields = clusters.map((c): StructField => {
|
|
311
|
+
const start = c[0].off;
|
|
312
|
+
const members = [...new Set(c.map((a) => a.width))].flatMap((width): StructField[] => {
|
|
313
|
+
const group = c.filter((a) => a.width === width);
|
|
314
|
+
const name = VIEW_NAMES[width];
|
|
315
|
+
if (name === undefined) {
|
|
316
|
+
throw new RaiseUnsupportedError(
|
|
317
|
+
`cannot recover struct '${structName}': no union view for a ${width}-byte access at offset ${group[0].off}`,
|
|
318
|
+
);
|
|
203
319
|
}
|
|
320
|
+
// The extensions this width is LOADED with, unsigned first; a word has one type whatever its
|
|
321
|
+
// loads say, and a width only stored gets the unsigned view.
|
|
322
|
+
const loaded = new Set(group.filter((a) => a.isLoad).map((a) => a.signed));
|
|
323
|
+
const signs = width === 4 ? [true] : [false, true].filter((sg) => loaded.has(sg));
|
|
324
|
+
const views = signs.length === 0 ? [false] : signs;
|
|
325
|
+
const viewOf = (a: Access): boolean => (views.length === 1 ? views[0] : a.isLoad ? a.signed : false);
|
|
326
|
+
return views.map((signed): StructField => {
|
|
327
|
+
const reads = group.filter((a) => viewOf(a) === signed);
|
|
328
|
+
const count = Math.max(...reads.map((a) => a.off - start)) / width + 1;
|
|
329
|
+
const elem = scalarTypeForAccess(width, signed);
|
|
330
|
+
const viewName = views.length > 1 ? `${signed ? 's' : 'u'}${name}` : name;
|
|
331
|
+
return { off: 0, type: count === 1 ? elem : T.array(elem, count), name: viewName };
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
if (members.length === 1) {
|
|
335
|
+
return { off: start, type: members[0].type, name: `field_${start}` };
|
|
204
336
|
}
|
|
337
|
+
if (aggregateBoundary === undefined) {
|
|
338
|
+
throw new RaiseUnsupportedError(
|
|
339
|
+
`cannot recover struct '${structName}': the union at offset ${start} needs this compiler's aggregate boundary, which is unmeasured`,
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
return { off: start, type: T.union(members, aggregateBoundary), name: `field_${start}` };
|
|
343
|
+
});
|
|
344
|
+
dataFields.forEach((f, i) => {
|
|
345
|
+
if (f.type.kind !== 'union') {
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
const next = dataFields[i + 1];
|
|
349
|
+
if (f.off % f.type.size !== 0 || (next !== undefined && next.off < f.off + f.type.size)) {
|
|
350
|
+
throw new RaiseUnsupportedError(
|
|
351
|
+
`cannot recover struct '${structName}': the union at offset ${f.off} does not fit this compiler's ${aggregateBoundary}-byte aggregate boundary`,
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
return placeFields(structName, dataFields);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** Size in bytes of a recovered field's type (int width/8; pointer is word-sized 4; a union the
|
|
359
|
+
* size its compiler gives it). */
|
|
360
|
+
function fieldSize(t: IrType): number {
|
|
361
|
+
switch (t.kind) {
|
|
362
|
+
case 'int':
|
|
363
|
+
return t.width / 8;
|
|
364
|
+
case 'array':
|
|
365
|
+
return t.count * fieldSize(t.elem);
|
|
366
|
+
case 'union':
|
|
367
|
+
return t.size;
|
|
368
|
+
default:
|
|
369
|
+
return 4;
|
|
205
370
|
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** Recover struct-pointer types from access-pattern evidence. Runs after array legalization and
|
|
374
|
+
* before type recovery, so `recoverTypes` sees the base already typed and does not flatten it to a
|
|
375
|
+
* plain pointer. Returns the number of bases recovered as structs. `aggregateBoundary` is the
|
|
376
|
+
* compiler's struct/union size boundary, undefined where nobody measured it (`buildUnionStruct`). */
|
|
377
|
+
export function recognizeStructs(fn: Fn, aggregateBoundary: number | undefined): number {
|
|
378
|
+
const { accessesOf, order, arrayBases } = constOffsetAccesses(fn);
|
|
379
|
+
|
|
380
|
+
// Does this base's address have a source of truth OUTSIDE this function's access set? Consulted
|
|
381
|
+
// only when synthesis refuses an OVERLAP: see the caller below. Two kinds qualify, and both are
|
|
382
|
+
// read with the helpers raise/truncload.ts asks the same question with — one concept, one
|
|
383
|
+
// reading, and `globalCellOf` is the whole of that reading: it answers for `&gSym + K` as well as
|
|
384
|
+
// for the bare `&gSym`, and an interior address is its own base here (`constOffsetAccesses` keys
|
|
385
|
+
// on the operand Value), so a narrower opcode test would decline the shape `fixed-cell` admits.
|
|
386
|
+
const defs = defOpMap(fn);
|
|
387
|
+
const addressDeclaredElsewhere = (base: Value): boolean =>
|
|
388
|
+
globalCellOf(defs, base, 0) !== null || constAddressOf(defs, base, 0) !== null;
|
|
206
389
|
|
|
207
390
|
// The NAME counter is seeded from the names already in the graph, not from this pass's own
|
|
208
391
|
// success count: raise/memberarrays.ts runs first and mints `Struct<N>` types of its own, and two
|
|
@@ -224,19 +407,47 @@ export function recognizeStructs(fn: Fn): number {
|
|
|
224
407
|
try {
|
|
225
408
|
base.type = T.ptr(buildStruct(`Struct${name}`, accesses));
|
|
226
409
|
} catch (e) {
|
|
227
|
-
// A
|
|
228
|
-
//
|
|
229
|
-
//
|
|
230
|
-
//
|
|
231
|
-
//
|
|
232
|
-
//
|
|
233
|
-
//
|
|
234
|
-
//
|
|
235
|
-
//
|
|
236
|
-
|
|
410
|
+
// A base whose ADDRESS is already declared somewhere other than this function's access set is
|
|
411
|
+
// not a struct this pass has to synthesize, so failing to synthesize one for it is not a
|
|
412
|
+
// reason to decline the function — PROVIDED the refusal is the OVERLAP one (its own class,
|
|
413
|
+
// never a substring of the message), whose residue is per-access spellings that stand on their
|
|
414
|
+
// own. Two kinds of base qualify:
|
|
415
|
+
// • a NAMED global (`globalCellOf`) — its declaration belongs to the project's own headers,
|
|
416
|
+
// and its constant-offset accesses render at L3 through the symbol context (member
|
|
417
|
+
// spelling when the map knows the layout, the honest cast spelling when it does not). The
|
|
418
|
+
// inhabitant is agbcc FUSING two adjacent u8 compares into one ldrh — `s.level == 8 &&
|
|
419
|
+
// s.world == 6` reads offset 12 at widths 1 AND 2, which is not a union, just two
|
|
420
|
+
// spellings of declared bytes.
|
|
421
|
+
// • a LITERAL ADDRESS (`constAddressOf`) — a cell the hardware placed, which L3 renders as
|
|
422
|
+
// a cast at each access's OWN width (`*(u16 *)0x4000004`). There is no layout to
|
|
423
|
+
// reconcile: two widths at one literal address are two casts of one address, which is
|
|
424
|
+
// what the source wrote. The inhabitant is a memory-mapped I/O register the source writes
|
|
425
|
+
// as a halfword and as a word (`sa3:Sio32MultiLoadMain`, `synthetic:unidev`).
|
|
426
|
+
// This is the DUAL of raise/truncload.ts's `fixed-cell` gate, not a relaxation of it: that
|
|
427
|
+
// gate refuses to FOLD the two accesses into one cast of a wider load, because a device read
|
|
428
|
+
// is not a read of the bytes around it. Both readings agree that the two accesses stay two.
|
|
429
|
+
//
|
|
430
|
+
// THE PACKED REFUSAL IS NOT FORGIVEN FOR ANY BASE. Its residue is not two spellings: a
|
|
431
|
+
// misaligned access renders through the element index `off / width`, which is a fraction and
|
|
432
|
+
// not C, so forgiving it would trade a loud decline for an uncompilable candidate. THIS layer
|
|
433
|
+
// is the one that can name the layout, which is why the refusal a reader sees is this one.
|
|
434
|
+
// structure/structure.ts `displacementIndex` refuses the fraction a second time where it
|
|
435
|
+
// would be computed, for the bases that never reach this pass at all (already typed, or a
|
|
436
|
+
// forgiven overlap); that message can only describe the access, not the layout.
|
|
437
|
+
//
|
|
438
|
+
// An ANONYMOUS base (a loaded pointer, a parameter) has no other source of truth: its layout
|
|
439
|
+
// is what this function's accesses say, and an overlap there says UNION. It is declared as
|
|
440
|
+
// one (`buildUnionStruct`), which declines in turn on what it cannot lay out. The two
|
|
441
|
+
// declared kinds above keep their casts although `buildUnionStruct`'s aliasing argument
|
|
442
|
+
// applies to them too: no row re-reads across a store there, and a union is the wrong
|
|
443
|
+
// declaration for both inhabitants (docs/level-tower.md, "A union").
|
|
444
|
+
if (!(e instanceof StructOverlapError)) {
|
|
445
|
+
throw e;
|
|
446
|
+
}
|
|
447
|
+
if (addressDeclaredElsewhere(base)) {
|
|
237
448
|
continue;
|
|
238
449
|
}
|
|
239
|
-
|
|
450
|
+
base.type = T.ptr(buildUnionStruct(`Struct${name}`, accesses, aggregateBoundary));
|
|
240
451
|
}
|
|
241
452
|
name++;
|
|
242
453
|
count++;
|
|
@@ -249,13 +460,15 @@ export function recognizeStructs(fn: Fn): number {
|
|
|
249
460
|
* `ir/struct-names.ts`, which the three struct minters share; this names the prefix. */
|
|
250
461
|
export function firstFreeStructIndex(fn: Fn): number {
|
|
251
462
|
return nextStructIndex(
|
|
252
|
-
[...collectStructs(fn)].map((s) => s.name),
|
|
463
|
+
[...collectStructs(fn, true)].map((s) => s.name),
|
|
253
464
|
'Struct',
|
|
254
465
|
);
|
|
255
466
|
}
|
|
256
467
|
|
|
257
468
|
/** The distinct struct types this function's L2 GRAPH mentions (unwrapping struct pointers on every
|
|
258
|
-
* value), deduped by name and sorted, for the backend to declare above the function.
|
|
469
|
+
* value), deduped by name and sorted, for the backend to declare above the function. A struct the
|
|
470
|
+
* headers own (`declared`, ir/types.ts) is left out — the declarations block defines it — unless
|
|
471
|
+
* `withDeclared` asks for every name the graph uses, which is what a name allocator needs.
|
|
259
472
|
*
|
|
260
473
|
* "Mentions", not "references": this walks `fn.blocks` at the moment structuring runs, and the
|
|
261
474
|
* result is CACHED on the SFn (`structure.ts`) and carried by every later `{...sfn}` pass, so a
|
|
@@ -266,11 +479,11 @@ export function firstFreeStructIndex(fn: Fn): number {
|
|
|
266
479
|
* is recorded here rather than fixed speculatively. The fix, if a pass ever makes it reachable, is
|
|
267
480
|
* the one l3/symbol-refs.ts already applies to symbol references: derive at the consumption point
|
|
268
481
|
* (`backend/cfamily.ts` structDecls) instead of caching. */
|
|
269
|
-
export function collectStructs(fn: Fn): StructType[] {
|
|
482
|
+
export function collectStructs(fn: Fn, withDeclared = false): StructType[] {
|
|
270
483
|
const seen = new Map<string, StructType>();
|
|
271
484
|
const consider = (t: IrType) => {
|
|
272
485
|
const s = t.kind === 'ptr' && t.to.kind === 'struct' ? t.to : t.kind === 'struct' ? t : null;
|
|
273
|
-
if (s && s.kind === 'struct' && !seen.has(s.name)) {
|
|
486
|
+
if (s && s.kind === 'struct' && (withDeclared || s.declared === undefined) && !seen.has(s.name)) {
|
|
274
487
|
seen.set(s.name, { name: s.name, fields: s.fields, size: s.size });
|
|
275
488
|
}
|
|
276
489
|
};
|