@asmlift/core 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -24
- package/package.json +1 -1
- package/src/aggregate.ts +149 -0
- package/src/backend/c.ts +1 -0
- package/src/backend/cfamily.ts +165 -50
- package/src/backend/cpp.ts +2 -1
- package/src/backend/pascal.ts +31 -12
- package/src/contracts.ts +173 -16
- package/src/declare.ts +69 -20
- package/src/frontend/asmdata.ts +7 -1
- package/src/frontend/call-plan.ts +964 -60
- package/src/frontend/dead-reads.ts +628 -0
- package/src/frontend/device-pins.ts +23 -4
- package/src/frontend/emit.ts +4 -3
- package/src/frontend/fpu.ts +224 -17
- package/src/frontend/frame-objects.ts +1232 -192
- package/src/frontend/frontend.ts +64 -3
- package/src/frontend/high-half.ts +53 -4
- package/src/frontend/jump-table.ts +530 -0
- package/src/frontend/local-object.ts +158 -32
- package/src/frontend/mips.ts +2076 -215
- package/src/frontend/own-struct-return.ts +500 -0
- package/src/frontend/ppc.ts +2711 -232
- package/src/frontend/register-struct-call.ts +484 -0
- package/src/frontend/reloc-symbol.ts +22 -7
- package/src/frontend/ssa.ts +1460 -44
- package/src/frontend/stackargs.ts +106 -57
- package/src/frontend/thumb.ts +2044 -318
- package/src/ir/alias.ts +42 -0
- package/src/ir/bits.ts +681 -1
- package/src/ir/core.ts +100 -5
- package/src/ir/discipline.ts +86 -21
- package/src/ir/entry.ts +159 -0
- package/src/{structure → ir}/loops.ts +6 -2
- package/src/ir/opcodes.ts +109 -9
- package/src/ir/types.ts +86 -2
- package/src/ir/verify.ts +80 -11
- package/src/l3/ast.ts +290 -18
- package/src/l3/basecse.ts +33 -5
- package/src/l3/constinit.ts +382 -0
- package/src/l3/dce.ts +2 -2
- package/src/l3/initfirst.ts +42 -11
- package/src/l3/intcell.ts +2 -1
- package/src/l3/narrowdecl.ts +77 -36
- package/src/l3/pollguard.ts +8 -2
- package/src/l3/ptrcell.ts +31 -34
- package/src/l3/ptrfield.ts +123 -8
- package/src/l3/regspell.ts +9 -4
- package/src/l3/reindex.ts +283 -96
- package/src/l3/storage.ts +98 -1
- package/src/l3/symbol-refs.ts +84 -6
- package/src/l3/tailmerge.ts +4 -2
- package/src/l3/typing.ts +108 -15
- package/src/l3/unreduce.ts +6 -5
- package/src/l3/volatileptr.ts +17 -54
- package/src/mangle.ts +30 -0
- package/src/pattern/engine.ts +361 -73
- package/src/pipeline.ts +541 -24
- package/src/proto-context.ts +905 -45
- package/src/proto.ts +555 -40
- package/src/raise/aggmembers.ts +290 -0
- package/src/raise/arrays.ts +5 -1
- package/src/raise/declared-volatile.ts +207 -31
- package/src/raise/floathelpers.ts +254 -33
- package/src/raise/gvn.ts +3 -3
- package/src/raise/latch.ts +162 -6
- package/src/raise/memberarrays.ts +10 -4
- package/src/raise/offsetnames.ts +10 -26
- package/src/raise/pairparams.ts +33 -0
- package/src/raise/paramwidth.ts +196 -6
- package/src/raise/pre-recovery.ts +102 -19
- package/src/raise/pre-structure.ts +54 -0
- package/src/raise/recover.ts +12 -5
- package/src/raise/reroll.ts +694 -0
- package/src/raise/shortcircuit.ts +26 -12
- package/src/raise/softdiv.ts +1 -1
- package/src/raise/storesign.ts +153 -0
- package/src/raise/struct-arrays.ts +30 -11
- package/src/raise/structs.ts +396 -33
- package/src/raise/truncload.ts +4 -3
- package/src/raise/unthread.ts +400 -0
- package/src/rank-declare.ts +118 -12
- package/src/rank-variations.ts +89 -31
- package/src/rank.ts +307 -54
- package/src/runtime-helpers.ts +140 -22
- package/src/structure/analysis.ts +165 -2
- package/src/structure/arith-ops.ts +23 -0
- package/src/structure/globalaccess.ts +111 -9
- package/src/structure/hazards.ts +16 -64
- package/src/structure/local-statics.ts +145 -9
- package/src/structure/namecoalesce.ts +1 -1
- package/src/structure/pointer-spelling.ts +1106 -0
- package/src/structure/redundant-test.ts +1 -1
- package/src/structure/structure.ts +2180 -1054
- package/src/structure/switch-recover.ts +244 -100
- package/src/symbols.ts +6 -0
- package/src/target.ts +765 -52
- package/src/trace.ts +26 -8
- package/src/variation-definitions.ts +126 -14
- package/src/variation-gates.ts +2 -0
- package/src/variation-tokens.ts +5 -1
package/src/ir/bits.ts
CHANGED
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
// It is a function over `Fn` rather than a closure inside a rendering pass so that a test can ask
|
|
11
11
|
// it directly: reachable only through emitted C, a wrong answer for one opcode reads as a silent
|
|
12
12
|
// wrong address in a row nobody is looking at.
|
|
13
|
-
import type { Op, Value } from './core';
|
|
13
|
+
import type { Block, Op, Value } from './core';
|
|
14
|
+
import { OPCODES, type OpSig } from './opcodes';
|
|
14
15
|
|
|
15
16
|
/** A materialized def emits its own named temp, so its value is a VARIABLE at every use — never a
|
|
16
17
|
* literal this analysis may fold through. Callers that have no materialization model pass none. */
|
|
@@ -42,6 +43,518 @@ export function constMask(ctx: BitsCtx, v: Value): number | null {
|
|
|
42
43
|
return null;
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
/** What the word evaluator below may read. */
|
|
47
|
+
export interface WordCtx {
|
|
48
|
+
defs: ReadonlyMap<Value, Op>;
|
|
49
|
+
/** Block param -> the value each in-edge passes for it (undefined: an edge that passes none). A
|
|
50
|
+
* param absent from it (an entry param, or every param when the caller gives none) holds no
|
|
51
|
+
* known word. */
|
|
52
|
+
incoming?: ReadonlyMap<Value, readonly (Value | undefined)[]>;
|
|
53
|
+
/** A value the caller says is no constant, whatever computes it: switch recovery's named
|
|
54
|
+
* variables, whose every use reads the variable rather than the expression that set it. */
|
|
55
|
+
opaque?: (v: Value) => boolean;
|
|
56
|
+
/** A whole-word load -> every value a store this function made may have left there, one per
|
|
57
|
+
* path in (`storeForwards`), met like a block param's in-edges. The machine
|
|
58
|
+
* reads that word back unless the address is I/O, so only a reader that fails closed passes it:
|
|
59
|
+
* the structurer's `neg_s`, to whom a store and a reload of INT_MIN is INT_MIN as surely as a
|
|
60
|
+
* compiler that forwards the store makes it. Without it every load varies. */
|
|
61
|
+
forwarded?: ReadonlyMap<Op, readonly Value[]>;
|
|
62
|
+
/** A read every path into which passes a write this function made that may land on its cell
|
|
63
|
+
* (`overwrittenReads`). Unless `forwarded` names what that write left, it reads a constant this
|
|
64
|
+
* evaluator cannot compute: the memory the function wrote, through an address it cannot match
|
|
65
|
+
* to the read's, may be every word that read sees. Passed only beside `forwarded`. */
|
|
66
|
+
overwritten?: ReadonlySet<Op>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** A value's word, over a lattice: not yet seen (`top`, a loop-carried param before its back
|
|
70
|
+
* edge), one known word, a CONSTANT this evaluator cannot compute (`konst`: a pure op of words
|
|
71
|
+
* it has no rule for, or a register shift by 32 or more, which each ISA reads differently), or a
|
|
72
|
+
* value that varies (`var`). */
|
|
73
|
+
type Word = { k: 'top' } | { k: 'word'; w: number } | { k: 'konst' } | { k: 'var' };
|
|
74
|
+
const TOP: Word = { k: 'top' };
|
|
75
|
+
const KONST: Word = { k: 'konst' };
|
|
76
|
+
const VAR: Word = { k: 'var' };
|
|
77
|
+
const word = (w: number): Word => ({ k: 'word', w: w | 0 });
|
|
78
|
+
const sameWord = (a: Word, b: Word): boolean => a.k === b.k && (a.k !== 'word' || a.w === (b as { w: number }).w);
|
|
79
|
+
const meet = (a: Word, b: Word): Word =>
|
|
80
|
+
a.k === 'top'
|
|
81
|
+
? b
|
|
82
|
+
: b.k === 'top'
|
|
83
|
+
? a
|
|
84
|
+
: a.k === 'var' || b.k === 'var'
|
|
85
|
+
? VAR
|
|
86
|
+
: a.k === 'word' && b.k === 'word'
|
|
87
|
+
? a.w === b.w
|
|
88
|
+
? a
|
|
89
|
+
: VAR
|
|
90
|
+
: KONST;
|
|
91
|
+
|
|
92
|
+
const BINARY: Readonly<Record<string, (a: number, c: number) => number>> = {
|
|
93
|
+
add: (a, c) => a + c,
|
|
94
|
+
sub: (a, c) => a - c,
|
|
95
|
+
mul: (a, c) => Math.imul(a, c),
|
|
96
|
+
and: (a, c) => a & c,
|
|
97
|
+
or: (a, c) => a | c,
|
|
98
|
+
xor: (a, c) => a ^ c,
|
|
99
|
+
};
|
|
100
|
+
const SHIFT: Readonly<Record<string, (a: number, c: number) => number>> = {
|
|
101
|
+
shl: (a, c) => a << c,
|
|
102
|
+
shr_u: (a, c) => a >>> c,
|
|
103
|
+
shr_s: (a, c) => a >> c,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/** One op's word, given its operands'. The annihilators come first because they hold whatever
|
|
107
|
+
* the other operand is: `x & 0`, `x | -1`, `x * 0`, `x - x`, `x ^ x`. */
|
|
108
|
+
function opWord(d: Op, of: (v: Value) => Word): Word {
|
|
109
|
+
if (d.opcode === 'const') {
|
|
110
|
+
const value = d.attrs.value;
|
|
111
|
+
return typeof value === 'number'
|
|
112
|
+
? word(value)
|
|
113
|
+
: typeof value === 'bigint'
|
|
114
|
+
? word(Number(BigInt.asIntN(32, value)))
|
|
115
|
+
: KONST;
|
|
116
|
+
}
|
|
117
|
+
const sig = (OPCODES as Record<string, OpSig | undefined>)[d.opcode];
|
|
118
|
+
if (!sig || sig.effects || sig.reads || sig.terminator || d.results.length !== 1 || d.operands.length === 0) {
|
|
119
|
+
return VAR; // a load, a call, an address (`gaddr`: the linker's, not a word a compiler folds)
|
|
120
|
+
}
|
|
121
|
+
const imm = typeof d.attrs.imm === 'number' ? word(d.attrs.imm as number) : null;
|
|
122
|
+
const ws = d.operands.map(of);
|
|
123
|
+
const [a, c] = d.operands.length === 1 && imm !== null ? [ws[0], imm] : ws;
|
|
124
|
+
const is = (x: Word | undefined, n: number) => x?.k === 'word' && x.w === n;
|
|
125
|
+
if (
|
|
126
|
+
(d.opcode === 'and' && (is(a, 0) || is(c, 0))) ||
|
|
127
|
+
(d.opcode === 'mul' && (is(a, 0) || is(c, 0))) ||
|
|
128
|
+
((d.opcode === 'sub' || d.opcode === 'xor') && d.operands.length === 2 && d.operands[0] === d.operands[1])
|
|
129
|
+
) {
|
|
130
|
+
return word(0);
|
|
131
|
+
}
|
|
132
|
+
if (d.opcode === 'or' && (is(a, -1) || is(c, -1))) {
|
|
133
|
+
return word(-1);
|
|
134
|
+
}
|
|
135
|
+
const all = d.operands.length === 1 && imm !== null ? [a, c] : ws;
|
|
136
|
+
// An operand not yet seen comes BEFORE one that varies. Once it settles it may be the word an
|
|
137
|
+
// annihilator above needs (`q & x` with q a param that settles to 0), and an answer of VAR now
|
|
138
|
+
// would drop a param that reads it for good: the solver below would then reach whichever
|
|
139
|
+
// fixpoint the param order made, and the imprecise one answers `mayAlwaysHold` false.
|
|
140
|
+
if (all.some((x) => x.k === 'top')) {
|
|
141
|
+
return TOP;
|
|
142
|
+
}
|
|
143
|
+
if (all.some((x) => x.k === 'var')) {
|
|
144
|
+
return VAR;
|
|
145
|
+
}
|
|
146
|
+
if (all.some((x) => x.k !== 'word')) {
|
|
147
|
+
return KONST;
|
|
148
|
+
}
|
|
149
|
+
const n = all.map((x) => (x as { w: number }).w);
|
|
150
|
+
if ((d.opcode === 'neg' || d.opcode === 'not') && n.length === 1) {
|
|
151
|
+
return word(d.opcode === 'neg' ? -n[0] : ~n[0]);
|
|
152
|
+
}
|
|
153
|
+
// The mathematical negation is the wrapping one at every word but INT_MIN, whose negation
|
|
154
|
+
// (+2^31) is a constant and no word.
|
|
155
|
+
if (d.opcode === 'neg_s' && n.length === 1) {
|
|
156
|
+
return n[0] === -0x80000000 ? KONST : word(-n[0]);
|
|
157
|
+
}
|
|
158
|
+
if (n.length === 2 && d.opcode in BINARY) {
|
|
159
|
+
return word(BINARY[d.opcode](n[0], n[1]));
|
|
160
|
+
}
|
|
161
|
+
if (n.length === 2 && d.opcode in SHIFT) {
|
|
162
|
+
return n[1] >= 0 && n[1] < 32 ? word(SHIFT[d.opcode](n[0], n[1])) : KONST;
|
|
163
|
+
}
|
|
164
|
+
return KONST;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Solve every block param `ctx.incoming` names, optimistically: each starts unseen and drops to
|
|
168
|
+
* the meet of what its in-edges pass, until nothing moves — so a loop-carried param that only
|
|
169
|
+
* ever passes itself back keeps the word it entered with, and a counter (`0`, then `i + 1`)
|
|
170
|
+
* varies. A param's new state is met with its old one, so it only ever drops, and drops at most
|
|
171
|
+
* three times; with `opWord` monotone (an unseen operand is TOP before a varying one is VAR)
|
|
172
|
+
* that meet changes nothing, and the fixpoint is the same in every param order.
|
|
173
|
+
*
|
|
174
|
+
* A forwarded load is solved the same way, its forwarded stores as its in-edges: memory carried
|
|
175
|
+
* round a loop is a param the frontend did not name, and a loop that stores a reload back
|
|
176
|
+
* (`ldr r2, [r3]; …; str r2, [r3]`) passes it itself, which must keep the word it entered with. */
|
|
177
|
+
function solveParams(ctx: WordCtx): Map<Value, Word> {
|
|
178
|
+
const state = new Map<Value, Word>();
|
|
179
|
+
const incoming = new Map<Value, readonly (Value | undefined)[]>(ctx.incoming ?? []);
|
|
180
|
+
for (const [load, vals] of ctx.forwarded ?? []) {
|
|
181
|
+
if (load.results.length === 1) {
|
|
182
|
+
incoming.set(load.results[0], vals);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
for (const p of incoming.keys()) {
|
|
186
|
+
state.set(p, TOP);
|
|
187
|
+
}
|
|
188
|
+
for (let round = 0; ; round++) {
|
|
189
|
+
if (round > 3 * state.size + 1) {
|
|
190
|
+
throw new Error('constWord: block params did not settle');
|
|
191
|
+
}
|
|
192
|
+
const memo = new Map<Value, Word>();
|
|
193
|
+
let moved = false;
|
|
194
|
+
for (const [p, args] of incoming) {
|
|
195
|
+
let w: Word = TOP;
|
|
196
|
+
for (const x of args) {
|
|
197
|
+
w = meet(w, x === undefined ? VAR : x === p ? TOP : evalWord(ctx, state, memo, x));
|
|
198
|
+
}
|
|
199
|
+
w = meet(state.get(p)!, w);
|
|
200
|
+
if (!sameWord(w, state.get(p)!)) {
|
|
201
|
+
state.set(p, w);
|
|
202
|
+
moved = true;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (!moved) {
|
|
206
|
+
return state;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function evalWord(ctx: WordCtx, params: ReadonlyMap<Value, Word>, memo: Map<Value, Word>, v: Value): Word {
|
|
212
|
+
if (ctx.opaque?.(v)) {
|
|
213
|
+
return VAR;
|
|
214
|
+
}
|
|
215
|
+
const p = params.get(v);
|
|
216
|
+
if (p !== undefined) {
|
|
217
|
+
return p;
|
|
218
|
+
}
|
|
219
|
+
const seen = memo.get(v);
|
|
220
|
+
if (seen !== undefined) {
|
|
221
|
+
return seen;
|
|
222
|
+
}
|
|
223
|
+
const d = ctx.defs.get(v);
|
|
224
|
+
// In progress reads VAR: SSA has no cycle but through a param, and a forwarded load is one
|
|
225
|
+
// (`solveParams`), so nothing reaches this mark but malformed IR.
|
|
226
|
+
memo.set(v, VAR);
|
|
227
|
+
const w = d === undefined ? VAR : ctx.overwritten?.has(d) ? KONST : opWord(d, (x) => evalWord(ctx, params, memo, x));
|
|
228
|
+
memo.set(v, w);
|
|
229
|
+
return w;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const solved = new WeakMap<WordCtx, Map<Value, Word>>();
|
|
233
|
+
function wordOf(ctx: WordCtx, v: Value): Word {
|
|
234
|
+
let params = solved.get(ctx);
|
|
235
|
+
if (params === undefined) {
|
|
236
|
+
params = solveParams(ctx);
|
|
237
|
+
solved.set(ctx, params);
|
|
238
|
+
}
|
|
239
|
+
return evalWord(ctx, params, new Map(), v);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** THE WORD A VALUE ALWAYS HOLDS — what the machine computes, when every input to it is a constant
|
|
243
|
+
* (a pool word, a `mov` and a shift of it, any integer arithmetic over those, an operand an
|
|
244
|
+
* annihilator makes irrelevant, a block param every in-edge passes the same word), or null.
|
|
245
|
+
* Unlike `constMask` it reads THROUGH a materialized def: it answers what the machine holds, not
|
|
246
|
+
* whether a spelling may fold it. Its readers: the structurer's `neg_s`, switch recovery's case
|
|
247
|
+
* constants, the float helpers' literal words. An opcode it has no rule for is no word, so a new
|
|
248
|
+
* op whose value is not the plain machine word fails closed here; `neg_s` has its own rule. */
|
|
249
|
+
export function constWord(ctx: WordCtx, v: Value): number | null {
|
|
250
|
+
const w = wordOf(ctx, v);
|
|
251
|
+
return w.k === 'word' ? w.w : null;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** Whether a value MAY hold `w` on every execution — it is that constant, a constant this
|
|
255
|
+
* evaluator cannot compute, or a value the solver never settled (a param no executed edge
|
|
256
|
+
* feeds). The fail-closed reading of `constWord`, for a caller to whom a constant it cannot name
|
|
257
|
+
* is as bad as the one it is looking for. A value that varies, or that is `w` on some paths only,
|
|
258
|
+
* answers false. */
|
|
259
|
+
export function mayAlwaysHold(ctx: WordCtx, v: Value, w: number): boolean {
|
|
260
|
+
const x = wordOf(ctx, v);
|
|
261
|
+
return x.k === 'konst' || x.k === 'top' || (x.k === 'word' && x.w === (w | 0));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** Where an access lands, normalised: a ROOT (one SSA value, one symbol, or the absolute address
|
|
265
|
+
* space) and a byte offset from it. `add`/`sub` of a constant move along the root, so a base held
|
|
266
|
+
* in another register (`add r2, r3, #4`), an addend in the pool word (`.word gX+4`) and the same
|
|
267
|
+
* offset carried by the access all name one cell. Two keys on one root overlap exactly when their
|
|
268
|
+
* byte ranges do; keys on two roots are two pointers nothing here can tell apart. */
|
|
269
|
+
interface Cell {
|
|
270
|
+
root: Value | string;
|
|
271
|
+
byte: number;
|
|
272
|
+
width: number;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** The blocks a path from the entry (`blocks[0]`) reaches. A block nothing reaches, and every
|
|
276
|
+
* edge out of one, is no path in: a dead block falling into a read must not pass for a way in
|
|
277
|
+
* that wrote nothing. */
|
|
278
|
+
function reachable(blocks: readonly Block[]): Set<Block> {
|
|
279
|
+
const seen = new Set<Block>(blocks.slice(0, 1));
|
|
280
|
+
const work = blocks.slice(0, 1);
|
|
281
|
+
while (work.length > 0) {
|
|
282
|
+
for (const o of work.pop()!.ops) {
|
|
283
|
+
for (const s of o.successors) {
|
|
284
|
+
if (!seen.has(s.block)) {
|
|
285
|
+
seen.add(s.block);
|
|
286
|
+
work.push(s.block);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return seen;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Two cells may be the same memory: on one root when their bytes overlap, and always on two. */
|
|
295
|
+
const overlaps = (x: Cell, y: Cell): boolean =>
|
|
296
|
+
x.root !== y.root || (x.byte < y.byte + y.width && y.byte < x.byte + x.width);
|
|
297
|
+
|
|
298
|
+
function cellOf(defs: ReadonlyMap<Value, Op>, addr: Value, off: number, width: number): Cell {
|
|
299
|
+
let root: Value | string = addr;
|
|
300
|
+
let byte = off;
|
|
301
|
+
for (let hops = 0; hops < 64 && typeof root !== 'string'; hops++) {
|
|
302
|
+
const d = defs.get(root);
|
|
303
|
+
if (d === undefined) {
|
|
304
|
+
break;
|
|
305
|
+
}
|
|
306
|
+
if (d.opcode === 'gaddr') {
|
|
307
|
+
root = `sym:${d.attrs.sym as string}`;
|
|
308
|
+
break;
|
|
309
|
+
}
|
|
310
|
+
if (d.opcode === 'const' && typeof d.attrs.value === 'number') {
|
|
311
|
+
root = 'abs';
|
|
312
|
+
byte += d.attrs.value;
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
const imm = typeof d.attrs.imm === 'number' ? d.attrs.imm : null;
|
|
316
|
+
if ((d.opcode === 'add' || d.opcode === 'sub') && d.operands.length === 1 && imm !== null) {
|
|
317
|
+
root = d.operands[0];
|
|
318
|
+
byte += d.opcode === 'add' ? imm : -imm;
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
if ((d.opcode === 'add' || d.opcode === 'sub') && d.operands.length === 2) {
|
|
322
|
+
const k = (v: Value): number | null => {
|
|
323
|
+
const c = defs.get(v);
|
|
324
|
+
return c?.opcode === 'const' && typeof c.attrs.value === 'number' ? c.attrs.value : null;
|
|
325
|
+
};
|
|
326
|
+
const [a, b] = d.operands;
|
|
327
|
+
if (k(b) !== null) {
|
|
328
|
+
root = a;
|
|
329
|
+
byte += d.opcode === 'add' ? k(b)! : -k(b)!;
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
if (d.opcode === 'add' && k(a) !== null) {
|
|
333
|
+
root = b;
|
|
334
|
+
byte += k(a)!;
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
break;
|
|
339
|
+
}
|
|
340
|
+
return { root, byte: byte | 0, width };
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** Whole-word load -> every value it may read, for `WordCtx.forwarded`: a forward MUST-analysis of
|
|
344
|
+
* the words this function's own stores leave in memory. Each block starts from the meet of what
|
|
345
|
+
* its predecessors leave — a cell survives a join only if every in-edge knows it, and then carries
|
|
346
|
+
* each in-edge's stored value — and a predecessor not yet reached (a loop's back edge, on the
|
|
347
|
+
* first pass) is optimistic and adds nothing, so a store before a loop that nothing in the loop
|
|
348
|
+
* overwrites reaches a reload in its header. A store sets its cell and forgets every cell it may
|
|
349
|
+
* overlap (any cell on another root, which may be the same memory); any other op with effects (a
|
|
350
|
+
* call, an indexed store) forgets everything. A load is forwarded when every path into it knows
|
|
351
|
+
* its cell, as a whole word; the entry block knows nothing, and a block the entry does not reach
|
|
352
|
+
* forwards nothing and is no path into the blocks it jumps to. */
|
|
353
|
+
export function storeForwards(blocks: readonly Block[]): Map<Op, Value[]> {
|
|
354
|
+
const defs = new Map<Value, Op>();
|
|
355
|
+
const preds = new Map<Block, Block[]>();
|
|
356
|
+
const live = reachable(blocks);
|
|
357
|
+
for (const b of blocks) {
|
|
358
|
+
for (const op of b.ops) {
|
|
359
|
+
for (const r of op.results) {
|
|
360
|
+
defs.set(r, op);
|
|
361
|
+
}
|
|
362
|
+
for (const s of op.successors) {
|
|
363
|
+
if (live.has(b)) {
|
|
364
|
+
preds.set(s.block, [...(preds.get(s.block) ?? []), b]);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
type Mem = Map<string, { cell: Cell; vals: Value[] }>;
|
|
370
|
+
const ids = new Map<Value, number>();
|
|
371
|
+
const keyOf = (c: Cell): string =>
|
|
372
|
+
`${typeof c.root === 'string' ? c.root : `v${ids.get(c.root) ?? ids.set(c.root, ids.size).get(c.root)!}`}@${c.byte}:${c.width}`;
|
|
373
|
+
const cells = new Map<Op, Cell>();
|
|
374
|
+
for (const b of blocks) {
|
|
375
|
+
for (const op of b.ops) {
|
|
376
|
+
if ((op.opcode === 'load' || op.opcode === 'store') && typeof op.attrs.width === 'number') {
|
|
377
|
+
cells.set(op, cellOf(defs, op.operands[0], (op.attrs.off as number | undefined) ?? 0, op.attrs.width));
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
const out = new Map<Op, Value[]>();
|
|
382
|
+
// One pass over a block: from its entry state, record each whole-word load's forward (when
|
|
383
|
+
// `record`) and return its exit state.
|
|
384
|
+
const run = (b: Block, mem: Mem, record: boolean): Mem => {
|
|
385
|
+
const m: Mem = new Map(mem);
|
|
386
|
+
for (const op of b.ops) {
|
|
387
|
+
const cell = cells.get(op);
|
|
388
|
+
if (op.opcode === 'load') {
|
|
389
|
+
const known = cell === undefined || cell.width !== 4 ? undefined : m.get(keyOf(cell));
|
|
390
|
+
if (record && known !== undefined) {
|
|
391
|
+
out.set(op, known.vals);
|
|
392
|
+
}
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
if (op.opcode === 'store' && cell !== undefined) {
|
|
396
|
+
for (const [k, e] of m) {
|
|
397
|
+
if (overlaps(e.cell, cell)) {
|
|
398
|
+
m.delete(k);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
m.set(keyOf(cell), { cell, vals: [op.operands[1]] });
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
const sig = (OPCODES as Record<string, OpSig | undefined>)[op.opcode];
|
|
405
|
+
if ((sig === undefined || sig.effects) && !sig?.terminator) {
|
|
406
|
+
m.clear();
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
return m;
|
|
410
|
+
};
|
|
411
|
+
const meetMem = (a: Mem, b: Mem): Mem => {
|
|
412
|
+
const m: Mem = new Map();
|
|
413
|
+
for (const [k, x] of a) {
|
|
414
|
+
const y = b.get(k);
|
|
415
|
+
if (y !== undefined) {
|
|
416
|
+
m.set(k, { cell: x.cell, vals: [...new Set([...x.vals, ...y.vals])] });
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return m;
|
|
420
|
+
};
|
|
421
|
+
const sameMem = (a: Mem, b: Mem): boolean =>
|
|
422
|
+
a.size === b.size &&
|
|
423
|
+
[...a].every(([k, x]) => {
|
|
424
|
+
const y = b.get(k);
|
|
425
|
+
return y !== undefined && y.vals.length === x.vals.length && x.vals.every((v) => y.vals.includes(v));
|
|
426
|
+
});
|
|
427
|
+
// Exit states; a block absent from it is not reached yet (TOP). Cells only ever leave a state
|
|
428
|
+
// and values only ever join one, both finite, so this settles.
|
|
429
|
+
const exit = new Map<Block, Mem>();
|
|
430
|
+
const entryOf = (b: Block): Mem | undefined => {
|
|
431
|
+
if (!live.has(b)) {
|
|
432
|
+
return undefined; // dead: it records nothing and adds nothing to a block it falls into
|
|
433
|
+
}
|
|
434
|
+
const from = preds.get(b) ?? [];
|
|
435
|
+
if (b === blocks[0]) {
|
|
436
|
+
return new Map();
|
|
437
|
+
}
|
|
438
|
+
let m: Mem | undefined;
|
|
439
|
+
for (const p of from) {
|
|
440
|
+
const e = exit.get(p);
|
|
441
|
+
if (e !== undefined) {
|
|
442
|
+
m = m === undefined ? e : meetMem(m, e);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
return m;
|
|
446
|
+
};
|
|
447
|
+
for (let moved = true, round = 0; moved; round++) {
|
|
448
|
+
if (round > 4 * blocks.length + 64) {
|
|
449
|
+
throw new Error('storeForwards: memory states did not settle');
|
|
450
|
+
}
|
|
451
|
+
moved = false;
|
|
452
|
+
for (const b of blocks) {
|
|
453
|
+
const at = entryOf(b);
|
|
454
|
+
if (at === undefined) {
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
const e = run(b, at, false);
|
|
458
|
+
const old = exit.get(b);
|
|
459
|
+
if (old === undefined || !sameMem(old, e)) {
|
|
460
|
+
exit.set(b, e);
|
|
461
|
+
moved = true;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
for (const b of blocks) {
|
|
466
|
+
const at = entryOf(b);
|
|
467
|
+
if (at !== undefined) {
|
|
468
|
+
run(b, at, true);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
return out;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/** Every read (`load`, `aload`) that each path from the function's entry reaches only past a write
|
|
475
|
+
* that may land on its cell, for `WordCtx.overwritten`: a store to an overlapping cell or to
|
|
476
|
+
* another root (which may be the same memory), and any other op with effects. What such a read
|
|
477
|
+
* sees is what this function wrote, on every path, and when `storeForwards` cannot say which word
|
|
478
|
+
* that was — a register-indexed address, two spellings of one sum, narrow stores that compose a
|
|
479
|
+
* word, a write through another pointer — the read must not pass for one that varies. A read with
|
|
480
|
+
* one write-free path in from the entry sees the caller's memory on it, which varies; a block the
|
|
481
|
+
* entry does not reach is no such path. An `aload`'s cell is never known, so every write may land
|
|
482
|
+
* on it. */
|
|
483
|
+
export function overwrittenReads(blocks: readonly Block[]): Set<Op> {
|
|
484
|
+
const defs = new Map<Value, Op>();
|
|
485
|
+
for (const b of blocks) {
|
|
486
|
+
for (const op of b.ops) {
|
|
487
|
+
for (const r of op.results) {
|
|
488
|
+
defs.set(r, op);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
const cellAt = (op: Op): Cell | undefined =>
|
|
493
|
+
(op.opcode === 'load' || op.opcode === 'store') && typeof op.attrs.width === 'number'
|
|
494
|
+
? cellOf(defs, op.operands[0], (op.attrs.off as number | undefined) ?? 0, op.attrs.width)
|
|
495
|
+
: undefined;
|
|
496
|
+
const writesOver = (op: Op, cell: Cell | undefined): boolean => {
|
|
497
|
+
if (op.opcode === 'load' || op.opcode === 'aload') {
|
|
498
|
+
return false;
|
|
499
|
+
}
|
|
500
|
+
if (op.opcode === 'store') {
|
|
501
|
+
const at = cellAt(op);
|
|
502
|
+
return at === undefined || cell === undefined || overlaps(at, cell);
|
|
503
|
+
}
|
|
504
|
+
const sig = (OPCODES as Record<string, OpSig | undefined>)[op.opcode];
|
|
505
|
+
return (sig === undefined || sig.effects === true) && !sig?.terminator;
|
|
506
|
+
};
|
|
507
|
+
const roots = blocks.slice(0, 1);
|
|
508
|
+
const out = new Set<Op>();
|
|
509
|
+
for (const b of blocks) {
|
|
510
|
+
b.ops.forEach((op, i) => {
|
|
511
|
+
if (op.opcode !== 'load' && op.opcode !== 'aload') {
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
const cell = cellAt(op);
|
|
515
|
+
// The blocks whose entry a write-free path reaches.
|
|
516
|
+
const clean = new Set<Block>(roots);
|
|
517
|
+
const work = [...roots];
|
|
518
|
+
while (work.length > 0) {
|
|
519
|
+
const x = work.pop()!;
|
|
520
|
+
if (x.ops.some((o) => writesOver(o, cell))) {
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
for (const o of x.ops) {
|
|
524
|
+
for (const s of o.successors) {
|
|
525
|
+
if (!clean.has(s.block)) {
|
|
526
|
+
clean.add(s.block);
|
|
527
|
+
work.push(s.block);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
if (!clean.has(b) || b.ops.slice(0, i).some((o) => writesOver(o, cell))) {
|
|
533
|
+
out.add(op);
|
|
534
|
+
}
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
return out;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/** Block param -> the values its in-edges pass, for `WordCtx.incoming`. */
|
|
541
|
+
export function incomingArgs(blocks: readonly Block[]): Map<Value, (Value | undefined)[]> {
|
|
542
|
+
const out = new Map<Value, (Value | undefined)[]>();
|
|
543
|
+
for (const b of blocks) {
|
|
544
|
+
for (const op of b.ops) {
|
|
545
|
+
for (const s of op.successors) {
|
|
546
|
+
s.block.params.forEach((p, i) => {
|
|
547
|
+
if (!out.has(p)) {
|
|
548
|
+
out.set(p, []);
|
|
549
|
+
}
|
|
550
|
+
out.get(p)!.push(s.args[i]);
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
return out;
|
|
556
|
+
}
|
|
557
|
+
|
|
45
558
|
/** An UPPER BOUND on the significant bits of a value, or 32 when nothing bounds it. Every answer
|
|
46
559
|
* below 32 comes from an instruction or from a caller's declaration-backed `bound`:
|
|
47
560
|
* · a zero-fill shift right by n leaves 32 - n;
|
|
@@ -73,3 +586,170 @@ export function provableBits(ctx: BitsCtx, v: Value): number {
|
|
|
73
586
|
: null;
|
|
74
587
|
return m !== null && m >= 0 ? 32 - Math.clz32(m) : 32;
|
|
75
588
|
}
|
|
589
|
+
|
|
590
|
+
// LINEAR FORMS AND STRIDED RANGES — "which words may this value be", for a caller that asks
|
|
591
|
+
// whether a computed address can land on one register (frontend/frame-objects.ts: a store after a
|
|
592
|
+
// DMA arming that may hit the channel's control halfword, a word store that hands an address to a
|
|
593
|
+
// channel's source register). One decomposition answers both, so they cannot disagree on a store.
|
|
594
|
+
|
|
595
|
+
/** What a linear form may read: each value's def and the constant a value is. */
|
|
596
|
+
export interface LinearCtx {
|
|
597
|
+
readonly defs: ReadonlyMap<Value, Op>;
|
|
598
|
+
constOf(v: Value): number | undefined;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/** `v` as a sum of runtime values times constants, plus a constant (the `1` key): agbcc's `ch *
|
|
602
|
+
* 12` is `((ch << 1) + ch) << 2`, which is 12 times `ch` only once the two `ch` terms are added.
|
|
603
|
+
* Every rule is a ring operation, so the form equals the machine's word modulo 2^32 whatever
|
|
604
|
+
* wraps on the way. A value this cannot read further is its own term. */
|
|
605
|
+
export function linearTerms(ctx: LinearCtx, v: Value, depth = 0): Map<Value | 1, number> | undefined {
|
|
606
|
+
const c = ctx.constOf(v);
|
|
607
|
+
if (c !== undefined) {
|
|
608
|
+
return new Map([[1, c]]);
|
|
609
|
+
}
|
|
610
|
+
const d = ctx.defs.get(v);
|
|
611
|
+
if (d === undefined || depth > 8) {
|
|
612
|
+
return new Map([[v, 1]]);
|
|
613
|
+
}
|
|
614
|
+
const scaled = (x: Value, by: number) => {
|
|
615
|
+
const l = linearTerms(ctx, x, depth + 1);
|
|
616
|
+
return l && new Map([...l].map(([t, k]) => [t, k * by] as const));
|
|
617
|
+
};
|
|
618
|
+
const k = d.operands.length === 1 ? (d.attrs.imm as number | undefined) : ctx.constOf(d.operands[1]);
|
|
619
|
+
if (d.opcode === 'shl' && k !== undefined && k >= 0 && k < 31) {
|
|
620
|
+
return scaled(d.operands[0], 2 ** k);
|
|
621
|
+
}
|
|
622
|
+
if (d.opcode === 'mul' && d.operands.length === 2 && k !== undefined) {
|
|
623
|
+
return scaled(d.operands[0], k);
|
|
624
|
+
}
|
|
625
|
+
if ((d.opcode === 'add' || d.opcode === 'sub') && d.operands.length === 2) {
|
|
626
|
+
const x = linearTerms(ctx, d.operands[0], depth + 1);
|
|
627
|
+
const y = scaled(d.operands[1], d.opcode === 'add' ? 1 : -1);
|
|
628
|
+
if (x === undefined || y === undefined) {
|
|
629
|
+
return undefined;
|
|
630
|
+
}
|
|
631
|
+
y.forEach((n, t) => x.set(t, (x.get(t) ?? 0) + n));
|
|
632
|
+
return x;
|
|
633
|
+
}
|
|
634
|
+
return new Map([[v, 1]]);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/** A number every value of `v` is a multiple of, or 0 when this cannot name one. */
|
|
638
|
+
export function linearFactor(ctx: LinearCtx, v: Value): number {
|
|
639
|
+
const l = linearTerms(ctx, v);
|
|
640
|
+
return l === undefined ? 0 : [...l.values()].reduce(gcdOf, 0);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/** Every word a value may hold is `lo + stride * n` for some `n >= 0`, and lies in `[lo, hi]`, an
|
|
644
|
+
* unsigned closed interval. `stride` is 0 exactly when `lo === hi`. */
|
|
645
|
+
export interface StridedRange {
|
|
646
|
+
readonly lo: number;
|
|
647
|
+
readonly hi: number;
|
|
648
|
+
readonly stride: number;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/** What a strided range may read beyond a linear form. */
|
|
652
|
+
export interface RangeCtx extends LinearCtx {
|
|
653
|
+
/** block param -> the value each in-edge passes (`incomingArgs`) */
|
|
654
|
+
readonly incoming: ReadonlyMap<Value, readonly (Value | undefined)[]>;
|
|
655
|
+
/** the function's own parameters, which arrive from a caller and hold anything */
|
|
656
|
+
readonly entry: ReadonlySet<Value>;
|
|
657
|
+
/** the one word a term the form cannot read is known to be — a name a symbol map places */
|
|
658
|
+
pointOf?(v: Value): number | undefined;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
const WORD = 2 ** 32;
|
|
662
|
+
/** The most terms one `stridedRange` question bounds before it answers nothing. */
|
|
663
|
+
const RANGE_BUDGET = 256;
|
|
664
|
+
|
|
665
|
+
/** Every word `v` may hold, or undefined when this cannot bound it. The form is `linearTerms`; a
|
|
666
|
+
* term is bounded by the word a caller places it at (`pointOf`), by `provableBits` (a zero-fill
|
|
667
|
+
* shift right, an unsigned narrow load, a non-negative mask) or a `zext`'s width, or, a block
|
|
668
|
+
* param, by the union of what its in-edges pass. Anything else — a function parameter, a whole
|
|
669
|
+
* word loaded from memory, a name nothing places, a param whose range depends on itself through a
|
|
670
|
+
* loop — bounds nothing, and nothing is the answer. The form equals the word modulo 2^32, so the
|
|
671
|
+
* words are the sum's integer range moved into one word, when it fits one; a sum whose range
|
|
672
|
+
* crosses a multiple of 2^32 holds words on both sides of zero, which no interval names. Exact is
|
|
673
|
+
* not the point: an answer must hold every word the machine can compute there, and a wider one
|
|
674
|
+
* only proves less.
|
|
675
|
+
*
|
|
676
|
+
* KNOWN GAP: a signed narrow index bounds nothing. Every leaf is unsigned (`provableBits` reads a
|
|
677
|
+
* signed value as 32 bits), and each in-edge of a param is moved into one word before the union,
|
|
678
|
+
* so `8 * (s8)i` — [-1024, 1016], across zero — names no interval even with a signed leaf. Both
|
|
679
|
+
* would have to change; no real GBA DMA function measured yet needs either. */
|
|
680
|
+
export function stridedRange(ctx: RangeCtx, v: Value): StridedRange | undefined {
|
|
681
|
+
const open = new Set<Value>();
|
|
682
|
+
let left = RANGE_BUDGET;
|
|
683
|
+
const bits: BitsCtx = {
|
|
684
|
+
defs: ctx.defs as Map<Value, Op>,
|
|
685
|
+
bound: (d) => (d.opcode === 'zext' ? (d.attrs.width as number) : null),
|
|
686
|
+
};
|
|
687
|
+
const termRange = (t: Value): StridedRange | undefined => {
|
|
688
|
+
const at = ctx.pointOf?.(t);
|
|
689
|
+
if (at !== undefined) {
|
|
690
|
+
return { lo: mod(at), hi: mod(at), stride: 0 };
|
|
691
|
+
}
|
|
692
|
+
if (ctx.defs.has(t)) {
|
|
693
|
+
const n = provableBits(bits, t);
|
|
694
|
+
return n < 32 ? { lo: 0, hi: 2 ** n - 1, stride: n === 0 ? 0 : 1 } : undefined;
|
|
695
|
+
}
|
|
696
|
+
const ins = ctx.entry.has(t) ? undefined : ctx.incoming.get(t);
|
|
697
|
+
if (ins === undefined || ins.length === 0 || open.has(t) || --left < 0) {
|
|
698
|
+
return undefined;
|
|
699
|
+
}
|
|
700
|
+
open.add(t);
|
|
701
|
+
try {
|
|
702
|
+
let out: StridedRange | undefined;
|
|
703
|
+
for (const x of ins) {
|
|
704
|
+
const r = x === undefined ? undefined : rangeOf(x);
|
|
705
|
+
if (r === undefined) {
|
|
706
|
+
return undefined;
|
|
707
|
+
}
|
|
708
|
+
out =
|
|
709
|
+
out === undefined
|
|
710
|
+
? r
|
|
711
|
+
: {
|
|
712
|
+
lo: Math.min(out.lo, r.lo),
|
|
713
|
+
hi: Math.max(out.hi, r.hi),
|
|
714
|
+
stride: gcdOf(gcdOf(out.stride, r.stride), r.lo - out.lo),
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
return out;
|
|
718
|
+
} finally {
|
|
719
|
+
open.delete(t);
|
|
720
|
+
}
|
|
721
|
+
};
|
|
722
|
+
const rangeOf = (x: Value): StridedRange | undefined => {
|
|
723
|
+
const form = linearTerms(ctx, x);
|
|
724
|
+
if (form === undefined) {
|
|
725
|
+
return undefined;
|
|
726
|
+
}
|
|
727
|
+
let lo = 0;
|
|
728
|
+
let hi = 0;
|
|
729
|
+
let stride = 0;
|
|
730
|
+
for (const [t, c] of form) {
|
|
731
|
+
if (c === 0) {
|
|
732
|
+
continue;
|
|
733
|
+
}
|
|
734
|
+
const r = t === 1 ? { lo: 1, hi: 1, stride: 0 } : termRange(t);
|
|
735
|
+
if (r === undefined) {
|
|
736
|
+
return undefined;
|
|
737
|
+
}
|
|
738
|
+
lo += c > 0 ? c * r.lo : c * r.hi;
|
|
739
|
+
hi += c > 0 ? c * r.hi : c * r.lo;
|
|
740
|
+
stride = gcdOf(stride, c * r.stride);
|
|
741
|
+
}
|
|
742
|
+
if (!Number.isSafeInteger(lo) || !Number.isSafeInteger(hi)) {
|
|
743
|
+
return undefined;
|
|
744
|
+
}
|
|
745
|
+
const base = Math.floor(lo / WORD) * WORD;
|
|
746
|
+
return hi - base >= WORD ? undefined : { lo: lo - base, hi: hi - base, stride };
|
|
747
|
+
};
|
|
748
|
+
return rangeOf(v);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
const mod = (w: number): number => ((w % WORD) + WORD) % WORD;
|
|
752
|
+
|
|
753
|
+
function gcdOf(a: number, b: number): number {
|
|
754
|
+
return b === 0 ? Math.abs(a) : gcdOf(b, a % b);
|
|
755
|
+
}
|