@asmlift/core 0.6.0 → 0.8.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.
Files changed (88) hide show
  1. package/README.md +48 -24
  2. package/package.json +1 -1
  3. package/src/backend/cfamily.ts +39 -11
  4. package/src/backend/pascal.ts +2 -2
  5. package/src/codegen-flags.ts +640 -0
  6. package/src/contracts.ts +60 -11
  7. package/src/frontend/disasm.ts +141 -11
  8. package/src/frontend/high-half.ts +149 -0
  9. package/src/frontend/mips.ts +458 -209
  10. package/src/frontend/ppc.ts +332 -67
  11. package/src/frontend/reloc-symbol.ts +109 -0
  12. package/src/frontend/splat.ts +56 -18
  13. package/src/frontend/ssa.ts +127 -30
  14. package/src/frontend/stackargs.ts +420 -0
  15. package/src/frontend/thumb.ts +209 -232
  16. package/src/ir/alias.ts +24 -0
  17. package/src/ir/core.ts +70 -3
  18. package/src/ir/opcodes.ts +52 -7
  19. package/src/ir/parse.ts +7 -1
  20. package/src/ir/simplify.ts +1 -1
  21. package/src/l3/address.ts +2 -2
  22. package/src/l3/advance.ts +373 -0
  23. package/src/l3/argbase.ts +6 -6
  24. package/src/l3/argcopy.ts +269 -0
  25. package/src/l3/ast.ts +110 -22
  26. package/src/l3/basecse.ts +50 -30
  27. package/src/l3/coalesce.ts +118 -61
  28. package/src/l3/gates.ts +75 -1
  29. package/src/l3/hoist.ts +1 -1
  30. package/src/l3/homesplit.ts +13 -13
  31. package/src/l3/initfirst.ts +3 -3
  32. package/src/l3/inlinebase.ts +16 -16
  33. package/src/l3/mentions.ts +68 -5
  34. package/src/l3/mulfirst.ts +3 -3
  35. package/src/l3/nearbase.ts +4 -4
  36. package/src/l3/offmember.ts +5 -5
  37. package/src/l3/parkfirst.ts +6 -6
  38. package/src/l3/pollguard.ts +3 -3
  39. package/src/l3/ptrfield.ts +4 -4
  40. package/src/l3/regspell.ts +8 -8
  41. package/src/l3/reindex.ts +22 -17
  42. package/src/l3/scopebase.ts +32 -29
  43. package/src/l3/sinkinit.ts +7 -7
  44. package/src/l3/slotorder.ts +3 -3
  45. package/src/l3/storage.ts +1 -1
  46. package/src/l3/tailmerge.ts +2 -2
  47. package/src/l3/tailret.ts +70 -0
  48. package/src/l3/typing.ts +3 -3
  49. package/src/l3/unmerge.ts +483 -59
  50. package/src/l3/unreduce.ts +15 -14
  51. package/src/l3/volatileptr.ts +11 -11
  52. package/src/l3/volatileval.ts +11 -11
  53. package/src/l3/volstore.ts +16 -16
  54. package/src/l3/zerosub.ts +6 -6
  55. package/src/mangle.ts +49 -0
  56. package/src/pattern/engine.ts +132 -17
  57. package/src/pipeline.ts +39 -16
  58. package/src/proto.ts +2 -2
  59. package/src/raise/const.ts +203 -3
  60. package/src/raise/divpow2.ts +2 -2
  61. package/src/raise/extscale.ts +345 -0
  62. package/src/raise/globalshape.ts +32 -12
  63. package/src/raise/gvn.ts +2 -2
  64. package/src/raise/magicdiv.ts +2 -2
  65. package/src/raise/memberarrays.ts +4 -4
  66. package/src/raise/narrowlocal.ts +18 -2
  67. package/src/raise/paramwidth.ts +133 -3
  68. package/src/raise/pre-recovery.ts +100 -25
  69. package/src/raise/retsink.ts +389 -19
  70. package/src/raise/shortcircuit.ts +595 -34
  71. package/src/raise/structs.ts +4 -4
  72. package/src/raise/tailsink.ts +141 -0
  73. package/src/rank-declare.ts +21 -13
  74. package/src/{rank-axes.ts → rank-variations.ts} +319 -189
  75. package/src/rank.ts +1176 -805
  76. package/src/structure/analysis.ts +87 -90
  77. package/src/structure/bitfields.ts +130 -30
  78. package/src/structure/globalaccess.ts +30 -4
  79. package/src/structure/namecoalesce.ts +32 -13
  80. package/src/structure/retspell.ts +95 -0
  81. package/src/structure/structure.ts +1425 -201
  82. package/src/structure/switch-recover.ts +101 -8
  83. package/src/symbols.ts +127 -6
  84. package/src/target.ts +374 -44
  85. package/src/trace.ts +28 -19
  86. package/src/variation-definitions.ts +1590 -0
  87. package/src/variation-gates.ts +92 -0
  88. package/src/variation-tokens.ts +356 -0
@@ -0,0 +1,640 @@
1
+ // asmlift — compiler flags, parsed into the codegen profile a function was compiled at.
2
+ //
3
+ // A function's compile and its decompile read one flag set. The compile receives the argv verbatim,
4
+ // and every flag set of a toolchain decompiles against that toolchain's compiler behaviors
5
+ // (target.ts). The profile says what the flags make the compiler do, for display, grouping and
6
+ // derivation: each option that can change what the compiler emits, as a named slot holding the value
7
+ // the compiler acts on (`O=1`, `g=3`, `-fhex-asm=on`), next to the build's own words for it.
8
+ //
9
+ // Every word is one of two kinds, per compiler family:
10
+ // inert cannot change what the compiler emits for a preprocessed translation unit: include
11
+ // paths, defines, diagnostics. A build's flags are stored without them.
12
+ // codegen may change the object or the assembly text. A table entry names its slot; a word no
13
+ // table names is codegen too, reported as unclassified and never assumed inert.
14
+ //
15
+ // WHERE A TABLE SAYS HOW A FAMILY READS ITS FLAGS, IT CITES THE COMPILED PAIR IT WAS READ OFF. Each
16
+ // pair compares `.text` after `objcopy -j .text` (agbcc: the whole `.s`; mwcc: the whole object), on
17
+ // these probes:
18
+ // loop.c int arr[64];
19
+ // int sum(int n){ int i, s = 0; for (i = 0; i < n; i++) { s += arr[i] * 3 + i; } return s; }
20
+ // int mul(int a, int b){ int x = a * b; int y = x + a; return y - b + x * 2; }
21
+ // call.c int g(int);
22
+ // int f(int a, int b) { int i, s = 0; for (i = 0; i < a; i++) { s += b * 4 + g(i); } return s; }
23
+ // hoist.c extern unsigned char *gp; void A(int); void B(int);
24
+ // void f(int c) { if (c) A(*gp); else B(*gp); }
25
+ // common.c int counter; int bump(void){ return ++counter; }
26
+ // sq.c static int sq(int x) { return x * x; }
27
+ // int f(int *a, int n) { int s = 0, i; for (i = 0; i < n; i++) s += sq(a[i]) + a[i] * 3; return s; }
28
+ // inl.c static int f(int x){ return x * 3 + 1; } int g(int y){ return f(y) + f(y + 2); }
29
+ // str.c const char* a(void){ return "hello"; } const char* b(void){ return "hello"; }
30
+ // big.c int tbl[100]; int glob;
31
+ // static int add3(int a, int b, int c) { return a + b + c; }
32
+ // int g(int *a, int *b, int n, int k) { int i, j, s = 0;
33
+ // for (i = 0; i < n; i++) { a[i] = b[k * 2 + 1] + i * 4; s += add3(a[i], k, i); }
34
+ // for (i = 0; i < 8; i++) for (j = 0; j < 8; j++) s += tbl[i * 8 + j] * j;
35
+ // return s; }
36
+ // int h(int a, int b) { int x = a * b + 3; int y = a * b + 5; glob = x; return x * y + (a * b) + glob; }
37
+ // float fl(float *v, int n) { float t = 0.0f; int i; for (i = 0; i < n; i++) t += v[i] * v[i]; return t; }
38
+ // int sw(int x) { switch (x) { case 0: return 7; case 1: return 9; case 2: return 11; case 3: return 13;
39
+ // case 4: return 2; case 5: return 5; default: return -1; } }
40
+ // void cp(char *d, const char *s, int n) { while (n--) *d++ = *s++; }
41
+ // agbcc at `-mthumb-interwork -fhex-asm`, IDO 7.1 at `-mips2 -32 -non_shared -G 0`, KMC gcc at its
42
+ // canonical flags, and mwcc_242_81 at `-proc gekko`, each with the words named at the entry.
43
+ //
44
+ // Browser-pure: string work only. Deriving flags from a build reads files and lives outside core.
45
+
46
+ export type FlagFamily = 'agbcc' | 'ido' | 'gcc' | 'mwcc';
47
+
48
+ const FAMILIES: readonly FlagFamily[] = ['agbcc', 'ido', 'gcc', 'mwcc'];
49
+
50
+ /** The characters a double-quoted backslash escapes in POSIX sh; before any other it is literal. */
51
+ const DQ_ESCAPABLE = new Set(['$', '`', '"', '\\', '\n']);
52
+
53
+ /** POSIX shell word splitting with quotes and backslashes, and no expansion. dtk projects spell
54
+ * `-pragma "cats off"`, which is one argument; a backslash-newline continues the line, as in a
55
+ * Makefile recipe. An unterminated quote throws, naming its column. */
56
+ export function tokenizeFlags(text: string): string[] {
57
+ const out: string[] = [];
58
+ let word = '';
59
+ let inWord = false;
60
+ let quote: '"' | "'" | null = null;
61
+ let quoteAt = 0;
62
+ for (let i = 0; i < text.length; i++) {
63
+ const c = text[i];
64
+ if (quote) {
65
+ if (c === quote) {
66
+ quote = null;
67
+ } else if (c === '\\' && quote === '"' && DQ_ESCAPABLE.has(text[i + 1])) {
68
+ if (text[++i] !== '\n') {
69
+ word += text[i];
70
+ }
71
+ } else {
72
+ word += c;
73
+ }
74
+ } else if (c === '"' || c === "'") {
75
+ quote = c;
76
+ quoteAt = i + 1;
77
+ inWord = true;
78
+ } else if (c === '\\' && i + 1 < text.length) {
79
+ if (text[++i] !== '\n') {
80
+ word += text[i];
81
+ inWord = true;
82
+ }
83
+ } else if (/\s/.test(c)) {
84
+ if (inWord) {
85
+ out.push(word);
86
+ }
87
+ word = '';
88
+ inWord = false;
89
+ } else {
90
+ word += c;
91
+ inWord = true;
92
+ }
93
+ }
94
+ if (quote) {
95
+ throw new Error(`unterminated ${quote} quote at column ${quoteAt}: ${text}`);
96
+ }
97
+ if (inWord) {
98
+ out.push(word);
99
+ }
100
+ return out;
101
+ }
102
+
103
+ type Match = readonly (string | undefined)[];
104
+
105
+ interface OptionSpec {
106
+ /** the exact word, or a pattern over it */
107
+ match: string | RegExp;
108
+ /** the slot a codegen option assigns, or how its match names one; options sharing a slot override
109
+ * each other and the last wins. Absent ⇒ inert. */
110
+ slot?: string | ((m: Match) => string);
111
+ /** the value is the next word */
112
+ takesArg?: boolean;
113
+ /** the value is a comma list the shell may have split into several words (`-str reuse, readonly`) */
114
+ commaList?: boolean;
115
+ /** each occurrence adds to what the earlier ones set, so none overrides another */
116
+ accumulates?: boolean;
117
+ /** the value the compiler acts on, from the spelled one (the argument, else the pattern's last
118
+ * capture group, else empty) */
119
+ value?: (spelled: string, m: Match) => string;
120
+ }
121
+
122
+ const toggle = (_: string, m: Match) => (m[1] ? 'off' : 'on');
123
+
124
+ const cppAndDiagnostics: OptionSpec[] = [
125
+ { match: /^-[IDU](.+)$/ },
126
+ { match: /^-(I|D|U|iquote|isystem|include)$/, takesArg: true },
127
+ // `-W<pass>,<option>` hands an option to another pass (gcc `-Wa,`, IDO `-Wo,`), so it is codegen:
128
+ // IDO loop.c at -O2 is 256 bytes of `.text`, and 96 with `-Wo,-loopunroll,0`. Every other `-W` is a
129
+ // diagnostic.
130
+ { match: /^-W(?![a-z]+,)/ },
131
+ { match: /^-(w|quiet|nostdinc|c)$/ },
132
+ ];
133
+
134
+ const gccFamily: OptionSpec[] = [
135
+ // toplev.c assigns `optimize` at every -O it scans, so the last level wins (agbcc hoist.c: `-O2 -O1`
136
+ // and `-O1`, same `.s`). Bare -O is level 1 (`-O` and `-O1`, same `.s`), and a level above 3 is 3
137
+ // (`-O9` and `-O3`, same `.s`).
138
+ { match: '-O', slot: 'O', value: () => '1' },
139
+ { match: /^-O(\d+)$/, slot: 'O', value: (v) => String(Math.min(Number(v), 3)) },
140
+ // -Os sets optimize_size beside level 2, so it is its own level: agbcc hoist.c differs at `-O2` and
141
+ // `-Os`, where the address load moves above the branch.
142
+ { match: '-Os', slot: 'O', value: () => 's' },
143
+ ...cppAndDiagnostics,
144
+ // -B tells the driver where its own programs are: which compiler runs is the toolchain, not a flag
145
+ { match: /^-B(.+)$/ },
146
+ { match: '-B', takesArg: true },
147
+ // agbcc -g is codegen: over the 181 vendored agbcc translation units `.text` is byte-identical with
148
+ // and without it at -O0, -O2, -O3 and -Os, and differs at -O1 on one (pokeemerald
149
+ // `Cmd_tryconversiontypechange`).
150
+ { match: /^-g(\d?)$/, slot: 'g', value: (v) => v || '2' },
151
+ // KMC loop.c: `-G 0` and `-G0`, same `.text`
152
+ { match: '-G', slot: 'G', takesArg: true },
153
+ { match: /^-G(\d+)$/, slot: 'G' },
154
+ { match: /^-mips(\d)$/, slot: 'mips' },
155
+ { match: /^-E([BL])$/, slot: 'endian' },
156
+ { match: '-x', slot: 'x', takesArg: true },
157
+ // the driver hands every `-Wa,` list to the assembler, so the occurrences add up
158
+ { match: /^-Wa,(.+)$/, slot: 'Wa', accumulates: true },
159
+ { match: /^-m([A-Za-z][\w-]*)=(.*)$/, slot: (m) => `-m${m[1]}=` },
160
+ // gcc keeps one variable per -f/-m switch, set by the switch and cleared by its no- form, so the
161
+ // last wins: agbcc common.c at -O2 puts `counter` in .bss with `-fcommon -fno-common` and emits
162
+ // `.comm counter` with `-fno-common -fcommon`.
163
+ { match: /^-f(no-)?([A-Za-z][\w-]*)$/, slot: (m) => `-f${m[2]}`, value: toggle },
164
+ { match: /^-m(no-)?([A-Za-z][\w-]*)$/, slot: (m) => `-m${m[2]}`, value: toggle },
165
+ ];
166
+
167
+ const TABLES: Record<FlagFamily, readonly OptionSpec[]> = {
168
+ agbcc: gccFamily,
169
+ ido: [
170
+ // the last level wins (`-O1 -O2` and `-O2`, same `.text` on loop.c and call.c), and bare -O is
171
+ // -O2 (`-O` and `-O2`, same `.text` on both)
172
+ { match: '-O', slot: 'O', value: () => '2' },
173
+ { match: /^-O([0-3])$/, slot: 'O' },
174
+ // `-Olimit 3000 -O2` and `-O2 -Olimit 3000` both compile to `-O2`'s `.text` on both probes
175
+ { match: '-Olimit', slot: 'Olimit', takesArg: true },
176
+ ...cppAndDiagnostics,
177
+ // -g alone is -g2; `-O2 -g3` and `-O2` differ on both probes
178
+ { match: /^-g([0-3]?)$/, slot: 'g', value: (v) => v || '2' },
179
+ { match: '-woff', takesArg: true },
180
+ { match: /^-(Xcpluscomm|Xfullwarn|fullwarn|verbose)$/ },
181
+ { match: /^-mips([1-4])$/, slot: 'mips' },
182
+ { match: '-32', slot: 'abi', value: () => '32' },
183
+ { match: '-non_shared', slot: 'pic', value: () => 'non_shared' },
184
+ { match: '-signed', slot: 'char', value: () => 'signed' },
185
+ { match: '-G', slot: 'G', takesArg: true },
186
+ { match: /^-G(\d+)$/, slot: 'G' },
187
+ ],
188
+ gcc: gccFamily,
189
+ mwcc: [
190
+ // mwcceppc takes -O, -O0…-O4, -Op, -Os and -O<0-4>,<p|s>, and refuses anything else: `-O+p` is
191
+ // "Unknown option '+p'; expected one of '0, 1, 2, 3, 4, p, or s'". A word sets a level, a mode or
192
+ // both, and `mwccLevel` reads what a sequence of them compiles at.
193
+ { match: /^-O((?:[0-4](?:,[ps])?|[ps])?)$/, slot: 'O' },
194
+ { match: /^-[ID](.+)$/ },
195
+ { match: /^-(d|D|i|I|ir|maxerrors|w|msgstyle)$/, takesArg: true },
196
+ { match: /^-(nodefaults|nosyspath|nostdinc|stderr|c|multibyte|requireprotos)$/ },
197
+ // `-dialect | -lang keyword` is one option under two names (mwcceppc -help), and all four
198
+ // spellings compile the same source to the same object in the container.
199
+ { match: /^-(?:lang|dialect)=(.+)$/, slot: 'lang' },
200
+ { match: '-dialect', slot: 'lang', takesArg: true },
201
+ { match: '-fp', slot: 'fp', takesArg: true, value: (v) => (v === 'hardware' ? 'hard' : v) },
202
+ // These add to what earlier occurrences enabled. At -O4,p inl.c with `-inline auto -inline
203
+ // deferred` differs from both `-inline auto` and `-inline deferred`, and str.c with `-str noreuse
204
+ // -str pool,readonly` differs from `-str pool,readonly`.
205
+ ...['str', 'inline'].map((o): OptionSpec => ({
206
+ match: `-${o}`,
207
+ slot: o,
208
+ takesArg: true,
209
+ commaList: true,
210
+ accumulates: true,
211
+ })),
212
+ { match: '-rostr', slot: 'rostr', value: () => 'on' },
213
+ ...[
214
+ 'lang',
215
+ 'proc',
216
+ 'sdata',
217
+ 'sdata2',
218
+ 'sdatathreshold',
219
+ 'char',
220
+ 'fp_contract',
221
+ 'use_lmw_stmw',
222
+ 'pool',
223
+ 'enum',
224
+ 'common',
225
+ 'func_align',
226
+ 'RTTI',
227
+ 'Cpp_exceptions',
228
+ 'align',
229
+ 'sym',
230
+ 'vector',
231
+ ].map((o): OptionSpec => ({ match: `-${o}`, slot: o, takesArg: true })),
232
+ ],
233
+ };
234
+
235
+ interface Implication {
236
+ /** each slot's values, one of which the profile must carry */
237
+ when: Readonly<Record<string, readonly string[]>>;
238
+ set: Readonly<Record<string, string>>;
239
+ note: string;
240
+ }
241
+
242
+ /** What a family's compiler does where the flags are silent, and what one slot does to another. */
243
+ const EFFECTIVE: Record<FlagFamily, { defaults: Readonly<Record<string, string>>; implies: readonly Implication[] }> = {
244
+ // agbcc hoist.c with no -O emits `-O0`'s `.s`
245
+ agbcc: { defaults: { O: '0' }, implies: [] },
246
+ ido: {
247
+ // cc with no -O compiles loop.c and call.c to `-O1`'s `.text`
248
+ defaults: { O: '1' },
249
+ implies: [
250
+ // uopt warns "file not optimized; use -g3 if both optimization and debug wanted": on both
251
+ // probes `-O2 -g` compiles to `-O1 -g`'s `.text` and `-O2 -g1` to `-g1`'s. `-O3 -g` does so on
252
+ // call.c only, so it stays -O3.
253
+ {
254
+ when: { O: ['2'], g: ['1', '2'] },
255
+ set: { O: '1' },
256
+ note: 'ido does not optimise with -g or -g1: -O2 compiles as -O1 (-g3 keeps both)',
257
+ },
258
+ ],
259
+ },
260
+ gcc: { defaults: {}, implies: [] },
261
+ mwcc: { defaults: {}, implies: [] },
262
+ };
263
+
264
+ /** A `-O` word a family cannot read. `spelledBy` is the family whose levels are spelled that way,
265
+ * when the word is a toolchain mix-up rather than a typo. */
266
+ export class UnreadableLevelError extends Error {
267
+ constructor(
268
+ readonly word: string,
269
+ readonly family: FlagFamily,
270
+ readonly spelledBy: FlagFamily | undefined,
271
+ ) {
272
+ super(
273
+ spelledBy === undefined
274
+ ? `${word} is not an optimisation level ${family} accepts`
275
+ : `${word} is not an optimisation level ${family} accepts; ${spelledBy} spells its levels that way`,
276
+ );
277
+ }
278
+ }
279
+
280
+ /** A `-O` word the family cannot read: mwcc refuses every level it does not know, and a word another
281
+ * family spells its levels with (`-O4,p` given to agbcc) is a toolchain mix-up. Any other `-O` word
282
+ * is left to the compiler. */
283
+ function refuseLevel(family: FlagFamily, word: string): void {
284
+ if (!word.startsWith('-O')) {
285
+ return;
286
+ }
287
+ if (family === 'mwcc') {
288
+ throw new UnreadableLevelError(word, family, undefined);
289
+ }
290
+ const other = FAMILIES.find((f) => f !== family && specFor(f, word)?.spec.slot === 'O');
291
+ if (other !== undefined && !/^-O(\d*|s)$/.test(word)) {
292
+ throw new UnreadableLevelError(word, family, other);
293
+ }
294
+ }
295
+
296
+ export interface CodegenProfile {
297
+ family: FlagFamily;
298
+ /** slot → the value the compiler acts on: the last spelled value (every value, for an option whose
299
+ * occurrences add up), the family's default where no flag names the slot, then what other slots imply */
300
+ slots: Readonly<Record<string, string>>;
301
+ /** slot → the build's own words for it (`-fhex-asm`, `-inline auto -inline deferred`); absent for a default */
302
+ spelled: Readonly<Record<string, string>>;
303
+ /** codegen words no table names, in build order, an exact repeat kept at its last position only;
304
+ * the compiler still receives every one */
305
+ unclassified: readonly string[];
306
+ /** argv indices of every codegen word a later word overrode or repeated */
307
+ overriddenAt: readonly number[];
308
+ /** argv indices of every inert word and its argument */
309
+ inertAt: readonly number[];
310
+ /** argv indices of every word that is neither an option nor an option's argument, such as the file
311
+ * a compile command names or `-` for standard input; each is also unclassified */
312
+ operandAt: readonly number[];
313
+ /** argv positions by option, in build order: an option with its argument words, a word no table names
314
+ * with the operand right after it (the table cannot say whether it takes one), every other word alone */
315
+ spans: readonly (readonly number[])[];
316
+ /** one line per override that changed a value, `-O1 overridden by later -O2` */
317
+ overrides: readonly string[];
318
+ /** one line per value the compiler reads other than as spelled: a level word it reads as another level,
319
+ * or a value another slot implies */
320
+ implied: readonly string[];
321
+ }
322
+
323
+ function specFor(family: FlagFamily, word: string): { spec: OptionSpec; m: Match } | undefined {
324
+ for (const spec of TABLES[family]) {
325
+ const m = typeof spec.match === 'string' ? (word === spec.match ? [word] : null) : word.match(spec.match);
326
+ if (m) {
327
+ return { spec, m };
328
+ }
329
+ }
330
+ return undefined;
331
+ }
332
+
333
+ /** A word that is not an option: a file, or `-`, which names standard input. */
334
+ const isOperand = (word: string): boolean => word === '-' || !word.startsWith('-');
335
+
336
+ /** A `-pragma` whose name only steers diagnostics. `cats` is NOT one of them: `-pragma "cats off"`
337
+ * suppresses the `.mwcats.text` section CodeWarrior otherwise writes — one ADDR32 record per
338
+ * function — so a build stored without it emits a section the build itself does not. (Measured on
339
+ * Pikmin's `nlibmath.cpp`: the section appears and disappears with the word, and the function's
340
+ * own bytes are identical either way.) */
341
+ const INERT_PRAGMA = /^(warn_\w+|msg_show_realref)\b/;
342
+
343
+ interface LevelReading {
344
+ value: string;
345
+ /** the words still in force, and where they are */
346
+ spelled: string;
347
+ at: number[];
348
+ overriddenAt: number[];
349
+ /** each note, at the position of the word that overrode */
350
+ overrides: { at: number; line: string }[];
351
+ }
352
+
353
+ /** The level mwcc compiles at after its `-O` words. Each pair below compiles to one object on sq.c, loop.c
354
+ * and big.c:
355
+ * - a word sets a level (bare -O is -O2: `-O` and `-O2`), a mode (p for speed, s for size), or both;
356
+ * - a mode holds until another mode: `-O4 -Op`, `-Op -O4` and `-O4,p`; `-O4,p -O0` and `-O0,p`;
357
+ * - a mode with no level is level 0: `-Op` and `-O0,p`; `-Os` and `-O0`;
358
+ * - a level replaces the one before it: `-O3 -O2` and `-O2`; `-O1 -O3` and `-O3`; `-O2 -O1 -O4` and `-O4`;
359
+ * `-O2 -O1 -O0` and `-O0`; `-O4 -O0 -O1` and `-O1`;
360
+ * - except that a lower level keeps what a higher one turned on. A level from 1 to 3 after -O4 adds to it:
361
+ * `-O4 -O2`, `-O4,p -O2,p` and `-O2 -O4 -O2` compile alike, to neither `-O4` nor `-O2`, and `-O4 -O2 -O1`
362
+ * to `-O4 -O1`. -O1 after -O2 or -O3 adds to it: `-O2 -O1`, `-O3 -O2 -O1` and `-O1 -O2 -O1` compile alike,
363
+ * to neither `-O2` nor `-O1` on big.c, while `-O2 -O1 -O2` is `-O2`.
364
+ * A level is otherwise kept as spelled: sq.c differs at `-O4` and `-O4,p`, and at `-O0` and `-O0,p`.
365
+ * `-O4` and `-O4,s` compile alike and stay apart anyway: a value that splits two equal objects costs
366
+ * grouping, and one that merged two different objects would be wrong. The value names the higher level
367
+ * a lower one keeps and the lower level (`4 -O2,p`, `2 -O1`), and a word is overridden once later words
368
+ * set everything it set. */
369
+ function mwccLevel(words: readonly { at: number; word: string }[]): LevelReading {
370
+ const read = words.map(({ at, word }) => {
371
+ const m = /^-O(?:([0-4])(?:,([ps]))?|([ps]))?$/.exec(word);
372
+ return { at, word, level: word === '-O' ? '2' : m?.[1], mode: m?.[2] ?? m?.[3], levelSet: true, modeSet: true };
373
+ });
374
+ let levels: typeof read = [];
375
+ let mode: (typeof read)[number] | undefined;
376
+ const value = () =>
377
+ `${levels.length === 0 ? '0' : levels.map((l) => l.level).join(' -O')}${mode ? `,${mode.mode}` : ''}`;
378
+ const overriddenAt: number[] = [];
379
+ const overrides: LevelReading['overrides'] = [];
380
+ read.forEach((w, k) => {
381
+ const before = k === 0 ? undefined : value();
382
+ const earlier = read.slice(0, k);
383
+ const inForce = (r: (typeof read)[number]) =>
384
+ (r.level !== undefined && r.levelSet) || (r.mode !== undefined && r.modeSet);
385
+ const live = earlier.filter(inForce);
386
+ if (w.level !== undefined) {
387
+ const kept =
388
+ w.level === '0' || w.level === '4'
389
+ ? undefined
390
+ : (levels.find((l) => l.level === '4') ??
391
+ (w.level === '1' ? levels.find((l) => l.level === '2' || l.level === '3') : undefined));
392
+ earlier.forEach((r) => (r.levelSet = r === kept && r.levelSet));
393
+ levels = kept === undefined ? [w] : [kept, w];
394
+ }
395
+ if (w.mode !== undefined) {
396
+ earlier.forEach((r) => (r.modeSet = false));
397
+ mode = w;
398
+ }
399
+ const after = value();
400
+ for (const r of live.filter((r) => !inForce(r))) {
401
+ overriddenAt.push(r.at);
402
+ if (before !== after && r.word !== w.word) {
403
+ overrides.push({ at: w.at, line: `${r.word} overridden by later ${w.word}` });
404
+ }
405
+ }
406
+ });
407
+ const kept = read.filter((r) => !overriddenAt.includes(r.at));
408
+ return {
409
+ value: value(),
410
+ spelled: kept.map((r) => r.word).join(' '),
411
+ at: kept.map((r) => r.at),
412
+ overriddenAt,
413
+ overrides,
414
+ };
415
+ }
416
+
417
+ /** The profile of one flag set. Throws on a `-O` word the family cannot read. */
418
+ export function parseFlags(family: FlagFamily, argv: readonly string[]): CodegenProfile {
419
+ const slots: Record<string, string> = {};
420
+ const unknown: number[] = [];
421
+ const lastAt = new Map<string, { at: number[]; spelled: string }>();
422
+ const overriddenAt: number[] = [];
423
+ const inertAt: number[] = [];
424
+ const overrides: LevelReading['overrides'] = [];
425
+ const mwccLevelWords: { at: number; word: string }[] = [];
426
+ const spans: number[][] = [];
427
+ const assign = (slot: string, value: string, at: number[], spelled: string, accumulates: boolean) => {
428
+ const prev = lastAt.get(slot);
429
+ if (prev && accumulates) {
430
+ slots[slot] = `${slots[slot]} ${value}`;
431
+ lastAt.set(slot, { at: [...prev.at, ...at], spelled: `${prev.spelled} ${spelled}` });
432
+ return;
433
+ }
434
+ if (prev) {
435
+ overriddenAt.push(...prev.at);
436
+ if (slots[slot] !== value) {
437
+ overrides.push({ at: at[0], line: `${prev.spelled} overridden by later ${spelled}` });
438
+ }
439
+ }
440
+ lastAt.set(slot, { at, spelled });
441
+ slots[slot] = value;
442
+ };
443
+ for (let i = 0; i < argv.length; i++) {
444
+ const word = argv[i];
445
+ if (family === 'mwcc' && word === '-pragma') {
446
+ const at = i + 1 < argv.length ? [i, i + 1] : [i];
447
+ spans.push(at);
448
+ const text = argv[i + 1] ?? '';
449
+ if (INERT_PRAGMA.test(text)) {
450
+ inertAt.push(...at);
451
+ } else {
452
+ const [name, ...rest] = text.split(/\s+/);
453
+ assign(`pragma:${name}`, rest.join(' '), at, `-pragma "${text}"`, false);
454
+ }
455
+ i++;
456
+ continue;
457
+ }
458
+ const hit = specFor(family, word);
459
+ if (!hit) {
460
+ refuseLevel(family, word);
461
+ unknown.push(i);
462
+ spans.push([i]);
463
+ continue;
464
+ }
465
+ if (family === 'mwcc' && hit.spec.slot === 'O') {
466
+ mwccLevelWords.push({ at: i, word });
467
+ spans.push([i]);
468
+ continue;
469
+ }
470
+ const at = [i];
471
+ let spelledValue = hit.m.length > 1 ? (hit.m[hit.m.length - 1] ?? '') : '';
472
+ if (hit.spec.takesArg) {
473
+ spelledValue = argv[i + 1] ?? '';
474
+ if (i + 1 < argv.length) {
475
+ at.push(++i);
476
+ }
477
+ while (hit.spec.commaList && spelledValue.endsWith(',') && i + 1 < argv.length) {
478
+ spelledValue += argv[++i];
479
+ at.push(i);
480
+ }
481
+ }
482
+ spans.push(at);
483
+ if (hit.spec.slot === undefined) {
484
+ inertAt.push(...at);
485
+ continue;
486
+ }
487
+ const slot = typeof hit.spec.slot === 'string' ? hit.spec.slot : hit.spec.slot(hit.m);
488
+ const value = hit.spec.value ? hit.spec.value(spelledValue, hit.m) : spelledValue;
489
+ assign(slot, value, at, at.map((k) => argv[k]).join(' '), hit.spec.accumulates === true);
490
+ }
491
+ if (mwccLevelWords.length > 0) {
492
+ const level = mwccLevel(mwccLevelWords);
493
+ slots.O = level.value;
494
+ lastAt.set('O', { at: level.at, spelled: level.spelled });
495
+ overriddenAt.push(...level.overriddenAt);
496
+ overrides.push(...level.overrides);
497
+ }
498
+
499
+ const rules = EFFECTIVE[family];
500
+ for (const [slot, v] of Object.entries(rules.defaults)) {
501
+ if (!(slot in slots)) {
502
+ slots[slot] = v;
503
+ }
504
+ }
505
+ const implied: string[] = [];
506
+ // a level word the compiler reads as another level (agbcc `-O9` is `-O3`), or level words it reads
507
+ // together (mwcc `-O4 -O2`), before what other slots imply
508
+ const spelledLevel = lastAt.get('O')?.spelled;
509
+ if (spelledLevel !== undefined) {
510
+ const reading = [
511
+ ...(spelledLevel === `-O${slots.O}` ? [] : [`-O${slots.O}`]),
512
+ ...(slots.O.includes(' ') ? ['neither level alone'] : []),
513
+ ];
514
+ if (reading.length > 0) {
515
+ implied.push(`${family} reads ${spelledLevel} as ${reading.join(', ')}`);
516
+ }
517
+ }
518
+ for (const rule of rules.implies) {
519
+ if (Object.entries(rule.when).every(([slot, vs]) => slot in slots && vs.includes(slots[slot]))) {
520
+ Object.assign(slots, rule.set);
521
+ implied.push(rule.note);
522
+ }
523
+ }
524
+
525
+ const lastUnknown = new Map<string, number>();
526
+ for (const k of unknown) {
527
+ lastUnknown.set(argv[k], k);
528
+ }
529
+ const unclassified: string[] = [];
530
+ for (const k of unknown) {
531
+ if (!isOperand(argv[k]) && lastUnknown.get(argv[k]) !== k) {
532
+ overriddenAt.push(k);
533
+ } else {
534
+ unclassified.push(argv[k]);
535
+ }
536
+ }
537
+ const unknownAt = new Set(unknown);
538
+ const grouped: number[][] = [];
539
+ for (const span of spans) {
540
+ const previous = grouped.at(-1);
541
+ const joinsPrevious =
542
+ previous?.length === 1 &&
543
+ span.length === 1 &&
544
+ unknownAt.has(previous[0]) &&
545
+ !isOperand(argv[previous[0]]) &&
546
+ isOperand(argv[span[0]]);
547
+ if (joinsPrevious) {
548
+ previous.push(span[0]);
549
+ } else {
550
+ grouped.push([...span]);
551
+ }
552
+ }
553
+ return {
554
+ family,
555
+ slots,
556
+ spelled: Object.fromEntries([...lastAt].map(([slot, { spelled }]) => [slot, spelled])),
557
+ unclassified,
558
+ overriddenAt: overriddenAt.sort((a, b) => a - b),
559
+ inertAt,
560
+ operandAt: unknown.filter((k) => isOperand(argv[k])),
561
+ spans: grouped,
562
+ overrides: overrides.sort((a, b) => a.at - b.at).map((o) => o.line),
563
+ implied,
564
+ };
565
+ }
566
+
567
+ /** A word the shell reads back as itself without quotes. */
568
+ const SHELL_BARE = /^[A-Za-z0-9_@%+=:,./-]+$/;
569
+
570
+ /** The flags as shell words, quoted only where the shell needs it (`-pragma 'cats off'`), so that
571
+ * `tokenizeFlags(shellJoinFlags(argv))` is `argv`. */
572
+ export function shellJoinFlags(argv: readonly string[]): string {
573
+ return argv.map((w) => (SHELL_BARE.test(w) ? w : `'${w.replaceAll("'", `'\\''`)}'`)).join(' ');
574
+ }
575
+
576
+ /** One string per distinct profile: the slots by name, whatever order the build spelled them in, then
577
+ * the unclassified words in build order, since their order may matter to the compiler. */
578
+ export function profileKey(profile: CodegenProfile): string {
579
+ const slots = Object.keys(profile.slots)
580
+ .sort()
581
+ .map((k) => `${k}=${profile.slots[k]}`);
582
+ return [...slots, ...profile.unclassified].join(' ');
583
+ }
584
+
585
+ /** The argv without the words a later word overrode: what the compiler acts on, in build order. */
586
+ export function effectiveFlags(family: FlagFamily, argv: readonly string[]): string[] {
587
+ const dropped = new Set(parseFlags(family, argv).overriddenAt);
588
+ return argv.filter((_, i) => !dropped.has(i));
589
+ }
590
+
591
+ /** A build's flags in their normal form: every inert word and its argument removed, the rest verbatim
592
+ * and in build order. */
593
+ export function storedFlags(family: FlagFamily, argv: readonly string[]): string[] {
594
+ const inert = new Set(parseFlags(family, argv).inertAt);
595
+ return argv.filter((_, i) => !inert.has(i));
596
+ }
597
+
598
+ /** THE DIALECT a translation unit is compiled in — the benchmark row's `language`, the `-lang` word
599
+ * its target and every candidate compile state, and (on a C++ row) the linkage a candidate needs
600
+ * to export the mangled symbol its target is keyed by.
601
+ *
602
+ * THE BUILD'S FLAGS ARE THE SIGNAL, never the file name: 62 of Animal Crossing's `.c` units are
603
+ * compiled `-lang=c++`. The option has two names and two spellings and one project uses two of
604
+ * them — Pikmin's 385 game units say `-lang=c++` and its 57 jaudio units say `-lang c++` — so the
605
+ * mwcc flag table owns the slot and the LAST occurrence wins, as it does on the command line.
606
+ *
607
+ * Its whole vocabulary is `c | c++ | ec++` (mwcceppc -help). `ec++` is Embedded C++, which the
608
+ * help says only adds warnings, and the container agrees: a unit with a virtual function, a loop
609
+ * and an `extern "C"` entry point compiles to a BYTE-IDENTICAL object at `-lang=ec++` and
610
+ * `-lang=c++`. So it is a C++ row, and any other word is refused rather than read as C — a unit
611
+ * built by the C++ front end and compiled here by the C one would publish a number about a
612
+ * language it never read.
613
+ *
614
+ * `unit` is the fallback and only that: where the flags name no dialect, the file's extension is
615
+ * what the front end itself would have used. Across the three GameCube projects the only units
616
+ * whose flags say nothing are six `.s` files no compile edge builds. Only mwcc has a dialect
617
+ * option, and only mwcc has a C++ front end, so every other family answers `c` unless its unit is
618
+ * named for C++ — which none is, and where one is, real.ts refuses the row by name. */
619
+ export function unitLanguage(unit: string, cflags: readonly string[]): 'c' | 'c++' {
620
+ const stated = parseFlags('mwcc', cflags).slots.lang;
621
+ if (stated === undefined) {
622
+ return /\.(cc|cp|cpp|cxx)$/i.test(unit) ? 'c++' : 'c';
623
+ }
624
+ if (stated !== 'c' && stated !== 'c++' && stated !== 'ec++') {
625
+ throw new Error(`${unit}: CodeWarrior dialect '${stated}' is not one of c, c++, ec++`);
626
+ }
627
+ return stated === 'c' ? 'c' : 'c++';
628
+ }
629
+
630
+ /** The optimisation level the compiler acts on (`-O2`, `-O4,p`), or null when the flags name none and
631
+ * the family has no measured default. */
632
+ export function optLevel(family: FlagFamily, argv: readonly string[]): string | null {
633
+ return levelOf(parseFlags(family, argv));
634
+ }
635
+
636
+ /** A profile's optimisation level (`-O2`, `-O4,p`), or null when it has none. */
637
+ export function levelOf(profile: Pick<CodegenProfile, 'slots'>): string | null {
638
+ const o = profile.slots.O;
639
+ return o === undefined ? null : `-O${o}`;
640
+ }