@asmlift/core 0.10.0 → 0.11.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/package.json +1 -1
- package/src/backend/c.ts +1 -0
- package/src/backend/cfamily.ts +33 -12
- package/src/backend/cpp.ts +1 -0
- package/src/backend/lines.ts +10 -0
- package/src/backend/pascal.ts +17 -5
- package/src/cdecl/parse.ts +21 -0
- package/src/contracts.ts +73 -20
- package/src/frontend/arm-body.ts +155 -0
- package/src/frontend/asmdata.ts +2 -1
- package/src/frontend/call-plan.ts +197 -45
- package/src/frontend/callee-body.ts +370 -0
- package/src/frontend/device-pins.ts +18 -8
- package/src/frontend/frame-objects.ts +504 -61
- package/src/frontend/frontend.ts +20 -0
- package/src/frontend/jump-table.ts +35 -20
- package/src/frontend/mips.ts +226 -8
- package/src/frontend/ppc.ts +26 -2
- package/src/frontend/register-struct-call.ts +1 -86
- package/src/frontend/ssa.ts +263 -1
- package/src/frontend/stackargs.ts +95 -17
- package/src/frontend/thumb-entry.ts +340 -0
- package/src/frontend/thumb.ts +410 -61
- package/src/ir/alias.ts +29 -8
- package/src/ir/bits.ts +1081 -66
- package/src/ir/core.ts +154 -4
- package/src/ir/entry.ts +3 -5
- package/src/ir/opcodes.ts +28 -0
- package/src/ir/parse.ts +1 -0
- package/src/ir/simplify.ts +61 -10
- package/src/ir/verify.ts +16 -0
- package/src/l3/argbase.ts +2 -0
- package/src/l3/ast.ts +294 -2
- package/src/l3/basecse.ts +35 -10
- package/src/l3/caseendgoto.ts +223 -0
- package/src/l3/coalesce.ts +42 -14
- package/src/l3/constinit.ts +34 -6
- package/src/l3/dce.ts +52 -8
- package/src/l3/ptrcell.ts +2 -0
- package/src/l3/regspell.ts +2 -0
- package/src/l3/reindex.ts +13 -4
- package/src/l3/scopebase.ts +2 -0
- package/src/l3/tailmerge.ts +2 -0
- package/src/pipeline.ts +507 -23
- package/src/proto-context.ts +135 -1
- package/src/raise/const.ts +25 -0
- package/src/raise/declared-volatile.ts +1 -1
- package/src/raise/divpow2.ts +192 -0
- package/src/raise/offsetnames.ts +412 -10
- package/src/raise/pre-recovery.ts +8 -5
- package/src/raise/pre-structure.ts +24 -5
- package/src/raise/reroll.ts +4 -24
- package/src/raise/shortcircuit.ts +192 -23
- package/src/raise/unpre.ts +594 -0
- package/src/rank-variations.ts +97 -4
- package/src/rank.ts +258 -43
- package/src/structure/analysis.ts +56 -10
- package/src/structure/cross-jumped-tail.ts +216 -0
- package/src/structure/globalaccess.ts +46 -1
- package/src/structure/structure.ts +4221 -368
- package/src/structure/switch-recover.ts +345 -26
- package/src/structure/unfolded-branch-constant.ts +90 -0
- package/src/symbols.ts +52 -15
- package/src/target.ts +269 -8
- package/src/trace.ts +28 -14
- package/src/variation-definitions.ts +207 -6
- package/src/variation-gates.ts +2 -0
- package/src/variation-tokens.ts +63 -2
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { constWord } from '../ir/bits';
|
|
8
8
|
import { Block, Fn, Op, Value, forwardingTarget, isBodyless, successorsOf } from '../ir/core';
|
|
9
9
|
import { orderSensitive } from '../ir/discipline';
|
|
10
|
+
import { SWAPPED_ICMP } from '../ir/opcodes';
|
|
10
11
|
import { Expr, Stmt, SwitchCase } from '../l3/ast';
|
|
11
12
|
|
|
12
13
|
/** Which side of a relational test a compiler's dispatch lands a pinned case body on. */
|
|
@@ -83,6 +84,10 @@ export interface SwitchRecoverDeps {
|
|
|
83
84
|
* l3/ast.ts non-neutrality note). With `breaks`, some paths out reach the
|
|
84
85
|
* switch's merge instead: `case 1: if (c) break; case 2: …`, the arm's own
|
|
85
86
|
* `break;` on those paths.
|
|
87
|
+
* - `goto` a `fallthrough` exit whose sibling another arm already falls into: the same
|
|
88
|
+
* paths, ending in `goto` to that sibling's case label (l3/ast.ts) instead of
|
|
89
|
+
* running on into it. Only `jumpSharedFallTargets` below makes one, and only
|
|
90
|
+
* Regime B asks it.
|
|
86
91
|
* - `unstructurable` anything else: two different siblings, or a sibling beside a block the
|
|
87
92
|
* switch does not dominate. C needs a `goto` for those. Regime A declines to
|
|
88
93
|
* if-recovery on this verdict; Regime B, having no fallback, fails LOUD.
|
|
@@ -92,18 +97,38 @@ export interface SwitchRecoverDeps {
|
|
|
92
97
|
* path to the merge in the middle of the arm as `break;` (`switchBreakAt`), in a closed arm as in a
|
|
93
98
|
* falling one: `case 1: if (a) { if (b) break; g(1); } h(2); break;`. A closed arm that leaves
|
|
94
99
|
* sideways, into a block the switch does not dominate, has none: its paths through such a block
|
|
95
|
-
* are not the arm's, and it structures to the CFG's joins.
|
|
100
|
+
* are not the arm's, and it structures to the CFG's joins. It names those blocks instead
|
|
101
|
+
* (`escapedTo`, the first block outside the region on each such path), which decide whether its
|
|
102
|
+
* body can still be copied into the arms that fall into it (`copySharedFallTargets`). An arm that
|
|
103
|
+
* took such a copy carries the SIBLING's `escapedTo`, not its own first outside block: the blocks
|
|
104
|
+
* where the copy steps sideways, which is all the copies read of it. */
|
|
96
105
|
export type ArmExit =
|
|
97
|
-
| { kind: 'break'; region?: ReadonlySet<Block> }
|
|
106
|
+
| { kind: 'break'; region?: ReadonlySet<Block>; escapedTo?: ReadonlySet<Block> }
|
|
98
107
|
| { kind: 'fallthrough'; to: Block; breaks: boolean; region: ReadonlySet<Block> }
|
|
108
|
+
| { kind: 'goto'; to: Block; breaks: boolean; region: ReadonlySet<Block> }
|
|
99
109
|
| { kind: 'unstructurable'; why: string };
|
|
100
110
|
|
|
101
111
|
export interface SwitchRecovery {
|
|
102
|
-
|
|
112
|
+
/** `mergeAt`, given the tree's case and default blocks, may name the switch's end ahead of the
|
|
113
|
+
* CFG's (structure.ts's join past early returns); `undefined` keeps `ipdom ?? stop`. */
|
|
114
|
+
recognizeSwitch: (
|
|
115
|
+
b: Block,
|
|
116
|
+
stop: Block | null,
|
|
117
|
+
mergeAt?: (arms: readonly Block[]) => Block | undefined,
|
|
118
|
+
) => Stmt[] | null;
|
|
103
119
|
/** shared with the Regime-B (`switch_br`) path in structure.ts. Both regimes recover the
|
|
104
120
|
* fall-through this returns; on an `unstructurable` verdict Regime A declines to if-recovery and
|
|
105
121
|
* Regime B, which has no fallback, fails loud. */
|
|
106
|
-
analyzeArmExit: (
|
|
122
|
+
analyzeArmExit: (
|
|
123
|
+
entry: Block,
|
|
124
|
+
b: Block,
|
|
125
|
+
merge: Block | null,
|
|
126
|
+
siblings: Set<Block>,
|
|
127
|
+
stopAtLoop?: boolean,
|
|
128
|
+
jumps?: ReadonlySet<Block>,
|
|
129
|
+
) => ArmExit;
|
|
130
|
+
/** may the copies write out a sibling that leaves sideways into `s`? (`copySharedFallTargets`) */
|
|
131
|
+
forwardToMerge: (s: Block, b: Block, merge: Block | null, siblings: ReadonlySet<Block>) => boolean;
|
|
107
132
|
/** a block's position in the ASSEMBLY — the arm-order evidence, shared with Regime B so the two
|
|
108
133
|
* regimes read it from one definition (and one statement of what it rests on). */
|
|
109
134
|
layoutIndex: (blk: Block) => number;
|
|
@@ -180,18 +205,6 @@ const INT_MIN = -0x80000000;
|
|
|
180
205
|
const INT_MAX = 0x7fffffff;
|
|
181
206
|
const ALL: Ranges = [[INT_MIN, INT_MAX]];
|
|
182
207
|
|
|
183
|
-
/** The same opcode with its operands swapped: `k < x` is `x > k`. */
|
|
184
|
-
const MIRRORED: Record<string, string> = {
|
|
185
|
-
icmp_slt: 'icmp_sgt',
|
|
186
|
-
icmp_sgt: 'icmp_slt',
|
|
187
|
-
icmp_sle: 'icmp_sge',
|
|
188
|
-
icmp_sge: 'icmp_sle',
|
|
189
|
-
icmp_ult: 'icmp_ugt',
|
|
190
|
-
icmp_ugt: 'icmp_ult',
|
|
191
|
-
icmp_ule: 'icmp_uge',
|
|
192
|
-
icmp_uge: 'icmp_ule',
|
|
193
|
-
};
|
|
194
|
-
|
|
195
208
|
/** `[lo, hi]` of the UNSIGNED order as signed intervals: one, or two where it straddles 2^31. */
|
|
196
209
|
function fromUnsigned(lo: number, hi: number): Ranges {
|
|
197
210
|
if (lo > hi) {
|
|
@@ -236,7 +249,7 @@ function intersect(a: Ranges, b: Ranges): Ranges {
|
|
|
236
249
|
|
|
237
250
|
/** The register values that take the test's BRANCH. */
|
|
238
251
|
function takenBy(ti: TestInfo): Ranges {
|
|
239
|
-
const op = ti.xOnLeft ? ti.opcode : (
|
|
252
|
+
const op = ti.xOnLeft ? ti.opcode : (SWAPPED_ICMP[ti.opcode] ?? ti.opcode);
|
|
240
253
|
if (op === 'icmp_eq' || op === 'icmp_ne') {
|
|
241
254
|
const k = ti.k | 0;
|
|
242
255
|
return op === 'icmp_eq' ? [[k, k]] : complement([[k, k]]);
|
|
@@ -276,6 +289,204 @@ function singleton(r: Ranges): number | null {
|
|
|
276
289
|
return r.length === 1 && r[0][0] === r[0][1] ? r[0][0] : null;
|
|
277
290
|
}
|
|
278
291
|
|
|
292
|
+
/** TWO OR MORE ARMS ENTERING ONE SIBLING'S BODY, put back as copies of it. A compiler that
|
|
293
|
+
* cross-jumps (agbcc's and gcc 2.7.2's `jump.c` cross-jumping, KMC's with it) finds two arms
|
|
294
|
+
* ending in the same code as a sibling arm and keeps one copy, so `case 1: g(v); gOut = v;
|
|
295
|
+
* break; case 2: gOut = v; break; case 3: h(v); gOut = v; break;` comes back with case 1 and
|
|
296
|
+
* case 3 both entering case 2's `gOut = v`. C drops into an arm from above along one edge only,
|
|
297
|
+
* so read as written that is two fall-throughs into one arm and `chainArms` refuses it — but the
|
|
298
|
+
* source had one fall-through at most.
|
|
299
|
+
*
|
|
300
|
+
* WHICH ENTRANT FALLS IS NOT IN THE ASM, so it is a ranked choice (`/tail-copies`,
|
|
301
|
+
* rank-variations.ts), never a fixed default. The cross-jump keeps ONE copy of the tail and
|
|
302
|
+
* says nothing about which arm the source wrote it under, and one compiler at one set of flags
|
|
303
|
+
* answers both ways: KMC `-O2` and gcc 2.7.2 `-O1` lay arm bodies out in SOURCE order, and a
|
|
304
|
+
* source whose fall-through arm is written directly above its target in case order recompiles
|
|
305
|
+
* only with that arm falling, while one whose arms are written out of case order (`case 3`,
|
|
306
|
+
* `case 2`, `case 1`) recompiles nearer with every entrant holding its own copy. Neither the
|
|
307
|
+
* layout nor the case order decides it: the copy the cross-jump kept is not the source's
|
|
308
|
+
* fall-through, so the arm that physically runs into the tail can be the wrong faller too.
|
|
309
|
+
*
|
|
310
|
+
* - `faller` (the default): at most one entrant keeps its fall-through, the one the caller's
|
|
311
|
+
* arm-order policy (`order`, before the chain moves anything) puts DIRECTLY ABOVE the sibling —
|
|
312
|
+
* on `switchArmsFollowLayout` targets (agbcc) its layout neighbour, elsewhere the arm before it in
|
|
313
|
+
* case order. Every other entrant copies; no entrant above it ⇒ every entrant copies.
|
|
314
|
+
* - not `faller`: every entrant copies, whichever arm sits above the sibling.
|
|
315
|
+
*
|
|
316
|
+
* Either way each copying entrant gets the sibling's region written out again under its own arm:
|
|
317
|
+
* its exit becomes a closed `break` over the two regions.
|
|
318
|
+
*
|
|
319
|
+
* A `default:` SIBLING takes the LAST arm of `order` as the arm above it: that is where `chainArms`
|
|
320
|
+
* puts a default that an arm falls into, at the end. It is not always where the label is printed —
|
|
321
|
+
* a target that prints `default:` at its layout position (`defaultLayoutPos`) can put it between two
|
|
322
|
+
* cases, and then the arm the source wrote above it is not `order`'s last, and its entrants all copy
|
|
323
|
+
* (a measured agbcc shape of that kind still recompiles to its target).
|
|
324
|
+
*
|
|
325
|
+
* A `default:` SIBLING MAY BE CODE THE SOURCE WROTE AFTER THE SWITCH. Where the other arms
|
|
326
|
+
* `return` from a void function they reach the same epilogue the default block does, so
|
|
327
|
+
* post-dominance ends the switch there, and arms that `break` to the code after the switch read as
|
|
328
|
+
* falling into the default block. The copies then write that code into each of them: the same
|
|
329
|
+
* program, not the source's spelling (which would end the switch at the default block and spell
|
|
330
|
+
* the other arms `return`, a reading nothing makes yet).
|
|
331
|
+
*
|
|
332
|
+
* ONLY OVER A CLOSED SIBLING. The copy is the sibling's region walked again inside the entrant's
|
|
333
|
+
* arm, so it is spelled only where that region ends the switch: the sibling's own exit a `break`
|
|
334
|
+
* WITH a region — it reaches no sibling of its own and leaves through no block the switch does
|
|
335
|
+
* not dominate. A sibling that falls on stays two-into-one for `chainArms` to refuse. PURE: the
|
|
336
|
+
* closed exit is exactly what `analyzeArmExit` reads for the entrant once the sibling is no
|
|
337
|
+
* longer a sibling — its walk reaches the sibling's blocks, and they reach the merge and nothing
|
|
338
|
+
* else — so it is assembled here rather than walked again.
|
|
339
|
+
*
|
|
340
|
+
* OR A SIBLING THAT LEAVES SIDEWAYS ONLY FORWARD. A closed sibling with no region stepped into
|
|
341
|
+
* blocks the switch does not dominate (`escapedTo`). Its body is still the switch's end on every
|
|
342
|
+
* path where each of those blocks runs only on to the merge or a `ret` — never back into the
|
|
343
|
+
* dispatch, a sibling (the entrants among them) or round a cycle; a path out through the
|
|
344
|
+
* enclosing loop's exit is one more path to a `ret`. `sidewaysEnds` answers that per block
|
|
345
|
+
* (`forwardToMerge`, the caller's to hand in; absent, no such sibling is copied). Nothing in it is
|
|
346
|
+
* particular to one compiler: it is asked of every jump table whose shared tail lost its region
|
|
347
|
+
* because a block on the tail's way to the merge is also reached from outside the switch. Where it
|
|
348
|
+
* shows:
|
|
349
|
+
* - agbcc's gcse PRE puts the next switch's index in the dispatch block, which then no longer
|
|
350
|
+
* dominates the default the bounds test reaches too, and the default's tail runs on through
|
|
351
|
+
* blocks other arms cross-jumped into (`synthetic:jt_xjdefpre`, kleod EntityCreate);
|
|
352
|
+
* - with no PRE at all (IDO 7.1 has no gcse), the tail runs on into a second jump table after the
|
|
353
|
+
* switch that the other branch of an `if` around it reaches too, and from there to the switch's
|
|
354
|
+
* end: in a `for` whose tail `continue`s (switch-arms.test.ts, the ido7.1 `jl_cont` fixture),
|
|
355
|
+
* or where a `return` in the tail makes the epilogue the merge, and the copies then write that
|
|
356
|
+
* whole second switch into each entrant.
|
|
357
|
+
* Each copying entrant then has no region either: like every arm that leaves sideways, it
|
|
358
|
+
* structures to the CFG's joins, which walks the sibling's blocks once inside it and stops at the
|
|
359
|
+
* merge.
|
|
360
|
+
*
|
|
361
|
+
* `onFallerKept` fires each time `faller` leaves an entrant its fall-through — exactly where the
|
|
362
|
+
* two settings of `faller` return different exits. Where it never fires over a whole structuring,
|
|
363
|
+
* the other setting builds the same tree, which is what rank.ts reads to skip `/tail-copies`.
|
|
364
|
+
* `onCopied` fires with the sibling each time an entrant takes a copy of a CASE arm (not the
|
|
365
|
+
* `default:` arm's), which is where `jumpSharedFallTargets` can read a `goto` instead: rank.ts's
|
|
366
|
+
* witness for `/case-goto`, and the siblings that variation hands it. */
|
|
367
|
+
export function copySharedFallTargets(
|
|
368
|
+
order: readonly Block[],
|
|
369
|
+
dflt: Block | null,
|
|
370
|
+
exitOf: ReadonlyMap<Block, ArmExit>,
|
|
371
|
+
faller: boolean,
|
|
372
|
+
onFallerKept?: () => void,
|
|
373
|
+
sidewaysEnds?: (s: Block) => boolean,
|
|
374
|
+
onCopied?: (to: Block) => void,
|
|
375
|
+
): Map<Block, ArmExit> {
|
|
376
|
+
const out = new Map(exitOf);
|
|
377
|
+
for (const { to, movers } of sharedFallTargets(order, dflt, exitOf, faller)) {
|
|
378
|
+
const own = exitOf.get(to);
|
|
379
|
+
if (own?.kind !== 'break') {
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
const { region, escapedTo } = own;
|
|
383
|
+
if (
|
|
384
|
+
region === undefined &&
|
|
385
|
+
(escapedTo === undefined || sidewaysEnds === undefined || ![...escapedTo].every(sidewaysEnds))
|
|
386
|
+
) {
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
for (const [e, x] of movers(onFallerKept)) {
|
|
390
|
+
if (to !== dflt) {
|
|
391
|
+
onCopied?.(to);
|
|
392
|
+
}
|
|
393
|
+
out.set(
|
|
394
|
+
e,
|
|
395
|
+
region === undefined
|
|
396
|
+
? { kind: 'break', escapedTo: escapedTo! }
|
|
397
|
+
: { kind: 'break', region: new Set([...x.region, ...region]) },
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return out;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** TWO OR MORE ARMS ENTERING ONE SIBLING'S BODY, put back as one fall-through and `goto`s to the
|
|
405
|
+
* sibling's case label — where the copies above do not take them (they are read first, and this
|
|
406
|
+
* is read over what they leave). It is the source's own spelling of the shape: `case 1: if (c)
|
|
407
|
+
* continue; goto L; case 2: if (d) continue; goto L; case 3: case 4: L: …` (kleod sub_0800D188's
|
|
408
|
+
* `goto block_1643;`), which every compiler that does not cross-jump a copy into it compiles with
|
|
409
|
+
* the sibling's code once. Unlike the copies it needs nothing of the sibling: the sibling's own exit
|
|
410
|
+
* is read for itself and may fall on, break, or leave sideways, since the jump does not write it
|
|
411
|
+
* out again.
|
|
412
|
+
*
|
|
413
|
+
* WHO FALLS is read from the one place the copies ask it too (`sharedFallTargets`), but with no
|
|
414
|
+
* choice in it: the entrant the caller's arm-order policy puts directly above the sibling always
|
|
415
|
+
* keeps its fall-through, and every other entrant jumps. A `goto` from that arm would land on the
|
|
416
|
+
* label directly below it, a path its plain fall-through already spells, so it is a jump goto-free
|
|
417
|
+
* C does not need (asmlift-101 §2.7). `/tail-copies`' setting of `faller` asks which entrant the
|
|
418
|
+
* SOURCE wrote the body under, a question a jump does not have: it writes no body.
|
|
419
|
+
*
|
|
420
|
+
* ONLY INTO A CASE ARM: the sibling is in `order`, never `dflt`, since only a case arm carries a
|
|
421
|
+
* label (l3/ast.ts `SwitchCase.label`) — and, given `only`, only into those siblings (`/case-goto`
|
|
422
|
+
* hands it the case arms the copies wrote back).
|
|
423
|
+
*
|
|
424
|
+
* WHAT MAKES THE JUMP THE SAME PROGRAM is the exit it replaces. A `goto` exit is a `fallthrough`
|
|
425
|
+
* exit with its last edge spelled as a jump: the arm is structured to stop at the sibling's entry
|
|
426
|
+
* exactly as a falling arm is (structure.ts `structureArm`), so every path that leaves the arm's
|
|
427
|
+
* body at its end is a path into the sibling, with the edge's own copies already run, and the
|
|
428
|
+
* `goto` is the arm's last top-level statement. The jump lands right after the sibling's case
|
|
429
|
+
* labels, which is where a fall-through lands; the emission refuses a sibling that opens with
|
|
430
|
+
* switch-edge copies, as it does for a fall-through: entering it by its own case value runs those,
|
|
431
|
+
* and the jump, landing above them, would run them again. No loop stands between the jump and the
|
|
432
|
+
* label: both are the switch's, in the loop the switch is in. */
|
|
433
|
+
export function jumpSharedFallTargets(
|
|
434
|
+
order: readonly Block[],
|
|
435
|
+
dflt: Block | null,
|
|
436
|
+
exitOf: ReadonlyMap<Block, ArmExit>,
|
|
437
|
+
only?: ReadonlySet<Block>,
|
|
438
|
+
): Map<Block, ArmExit> {
|
|
439
|
+
const out = new Map(exitOf);
|
|
440
|
+
for (const { to, movers } of sharedFallTargets(order, dflt, exitOf, true)) {
|
|
441
|
+
if (to === dflt || !order.includes(to) || (only !== undefined && !only.has(to))) {
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
for (const [e, x] of movers()) {
|
|
445
|
+
out.set(e, { kind: 'goto', to, breaks: x.breaks, region: x.region });
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
return out;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
type Entrant = [Block, Extract<ArmExit, { kind: 'fallthrough' }>];
|
|
452
|
+
|
|
453
|
+
/** WHO FALLS, the one answer both rewrites above read: each arm two or more arms fall into (`to`,
|
|
454
|
+
* in the order its first entrant comes in `order` and then `dflt`), and `movers` — its entrants
|
|
455
|
+
* other than the one that keeps its fall-through, with their exits, which each rewrite spells its
|
|
456
|
+
* own way (a copy, a jump). The keeper is the arm the caller's arm-order policy puts directly
|
|
457
|
+
* above `to`, where `faller` is set (`dflt` sits below the last of `order`); `movers` fires
|
|
458
|
+
* `onFallerKept` when that arm is an entrant, which is where the two settings of `faller` differ.
|
|
459
|
+
* It fires on the call, so a rewrite that refuses `to` before asking reports nothing. */
|
|
460
|
+
function sharedFallTargets(
|
|
461
|
+
order: readonly Block[],
|
|
462
|
+
dflt: Block | null,
|
|
463
|
+
exitOf: ReadonlyMap<Block, ArmExit>,
|
|
464
|
+
faller: boolean,
|
|
465
|
+
): { to: Block; movers: (onFallerKept?: () => void) => Entrant[] }[] {
|
|
466
|
+
const entrants = new Map<Block, Entrant[]>();
|
|
467
|
+
for (const e of [...order, ...(dflt ? [dflt] : [])]) {
|
|
468
|
+
const x = exitOf.get(e);
|
|
469
|
+
if (x?.kind === 'fallthrough') {
|
|
470
|
+
entrants.set(x.to, [...(entrants.get(x.to) ?? []), [e, x]]);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
return [...entrants]
|
|
474
|
+
.filter(([, from]) => from.length >= 2)
|
|
475
|
+
.map(([to, from]) => {
|
|
476
|
+
const at = to === dflt ? order.length : order.indexOf(to);
|
|
477
|
+
const above = faller && at > 0 ? order[at - 1] : undefined;
|
|
478
|
+
return {
|
|
479
|
+
to,
|
|
480
|
+
movers: (onFallerKept?: () => void) => {
|
|
481
|
+
if (above !== undefined && from.some(([e]) => e === above)) {
|
|
482
|
+
onFallerKept?.();
|
|
483
|
+
}
|
|
484
|
+
return from.filter(([e]) => e !== above);
|
|
485
|
+
},
|
|
486
|
+
};
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
|
|
279
490
|
/** Re-thread `order` so every FALLING arm sits directly above the arm it falls into. Each
|
|
280
491
|
* fall-through chain is emitted contiguously and takes the position of its HEAD in `order`,
|
|
281
492
|
* which is the caller's own arm-order policy — so with no fall-through every chain is a
|
|
@@ -357,8 +568,12 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
357
568
|
// the list for every conditional-return branch, wherever in the stream that branch sits. So
|
|
358
569
|
// its real blocks keep address order among themselves, and each synthetic one — a bare
|
|
359
570
|
// return, never a test — sorts after all of them;
|
|
360
|
-
// - raising
|
|
361
|
-
//
|
|
571
|
+
// - raising mostly REMOVES blocks from the list (raise/{divpow2,latch,retsink,shortcircuit}.ts
|
|
572
|
+
// all `filter`), but not only: raise/reroll.ts moves a rerolled loop's new head ahead of the
|
|
573
|
+
// old one and inserts a block of its own before it. So this order is the frontend's only
|
|
574
|
+
// where no reroll fired. A reader that needs the asm's own order wherever a pass rewrote the
|
|
575
|
+
// list reads the frontend's record instead (ir/core.ts `AsmLayout`, which the return-tail
|
|
576
|
+
// chooser reads); this one has not been moved onto it.
|
|
362
577
|
// TWO READERS, both target-gated, and they need DIFFERENT strengths of the compiler half:
|
|
363
578
|
// - `switchArmsFollowLayout` (arm ORDER, the sort below) needs the full claim — no block moved
|
|
364
579
|
// at all — because it PLACES the arms from the layout;
|
|
@@ -619,6 +834,74 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
619
834
|
return true;
|
|
620
835
|
};
|
|
621
836
|
|
|
837
|
+
// A SIDEWAYS PATH THAT ONLY RETURNS. An arm can step into a block the switch does not dominate
|
|
838
|
+
// and still go nowhere but the end of the function: every path from `s` runs forward to a `ret`,
|
|
839
|
+
// never back into the switch, its merge or a sibling, and never round a cycle. Such an arm spells
|
|
840
|
+
// the escaping path in place as its `return;` (`case 0: if (c) { k(a); return; } gOut = 1;`
|
|
841
|
+
// falling into `case 1:`), and a falling arm's join already reads that path as a jump
|
|
842
|
+
// (`regionJoins`, "a `return k` through a shared epilogue"). It is a property of the ARM, not of
|
|
843
|
+
// any loop, so it is asked of every jump-table switch (Regime B; `analyzeArmExit` says why not of
|
|
844
|
+
// Regime A's comparison trees). Where it shows:
|
|
845
|
+
// - in a loop body, where an arm `return`s into the one epilogue past the loop (agbcc and IDO
|
|
846
|
+
// when the code after the loop returns early too, mwcc always) and the switch ends at the
|
|
847
|
+
// body's own join (structure.ts, THE LOOP CLAMP);
|
|
848
|
+
// - outside any loop, where an arm `goto`s a returning tail it shares with code the switch does
|
|
849
|
+
// not dominate: a guard above the switch cross-jumped into the same `k(7); return;`, a shared
|
|
850
|
+
// `out:` label, the other branch of an `if` around the switch, or a switch with no merge.
|
|
851
|
+
// The cycle test is a walk of the paths themselves: a block met again while it is still on the
|
|
852
|
+
// path being walked is a cycle, reducible or not (an IDO `goto mid;` into a `while` body makes an
|
|
853
|
+
// irreducible one, which a dominance test does not see). An edge to a block that dominates its
|
|
854
|
+
// source is refused too, since that block may lie above `s` where the walk never put it.
|
|
855
|
+
//
|
|
856
|
+
// WITH `mergeEnds` it is the copies' question (`forwardToMerge`): a path may also END at the
|
|
857
|
+
// merge, and nothing more is admitted. A path out through the enclosing loop's exit is one more
|
|
858
|
+
// forward path to a `ret`, wherever it falls on the path: the copy writes the code past the loop
|
|
859
|
+
// and its `return;` inside the case arm. A step round the loop to its header needs no test of its
|
|
860
|
+
// own: the header dominates every block of the loop, so the dominance test refuses it from inside,
|
|
861
|
+
// and the cycle test from a path that entered at the header.
|
|
862
|
+
//
|
|
863
|
+
// There the SIBLING and CYCLE refusals are conservative, not needed for soundness: forced off, a
|
|
864
|
+
// tail that steps into a sibling (agbcc, the `jt_xjdefpre` shape with `if (v == 9) return;` in the
|
|
865
|
+
// tail, so the epilogue is the merge and the empty `case 4:` is a sibling) or round a cycle lifts
|
|
866
|
+
// correct, that sibling's body or the loop written out in each entrant. They spare that
|
|
867
|
+
// duplication; dropping one needs a differential over the shapes it then admits.
|
|
868
|
+
const onlyReturns = (
|
|
869
|
+
s: Block,
|
|
870
|
+
b: Block,
|
|
871
|
+
merge: Block | null,
|
|
872
|
+
siblings: ReadonlySet<Block>,
|
|
873
|
+
mergeEnds = false,
|
|
874
|
+
): boolean => {
|
|
875
|
+
const done = new Set<Block>();
|
|
876
|
+
const onPath = new Set<Block>([s]);
|
|
877
|
+
const stack: { x: Block; next: number }[] = [{ x: s, next: 0 }];
|
|
878
|
+
while (stack.length) {
|
|
879
|
+
const top = stack[stack.length - 1];
|
|
880
|
+
const succ = successorsOf(top.x);
|
|
881
|
+
if (top.next === 0 && succ.length === 0 && top.x.ops[top.x.ops.length - 1]?.opcode !== 'ret') {
|
|
882
|
+
return false;
|
|
883
|
+
}
|
|
884
|
+
if (top.next === succ.length) {
|
|
885
|
+
stack.pop();
|
|
886
|
+
onPath.delete(top.x);
|
|
887
|
+
done.add(top.x);
|
|
888
|
+
continue;
|
|
889
|
+
}
|
|
890
|
+
const y = succ[top.next++];
|
|
891
|
+
if (mergeEnds && y === merge && merge !== null) {
|
|
892
|
+
continue; // a path that reaches the switch's end
|
|
893
|
+
}
|
|
894
|
+
if (y === b || y === merge || siblings.has(y) || onPath.has(y) || dom.get(top.x)!.has(y)) {
|
|
895
|
+
return false; // back into the switch, or a cycle
|
|
896
|
+
}
|
|
897
|
+
if (!done.has(y)) {
|
|
898
|
+
onPath.add(y);
|
|
899
|
+
stack.push({ x: y, next: 0 });
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
return true;
|
|
903
|
+
};
|
|
904
|
+
|
|
622
905
|
// Where does one arm's region LEAVE? Walk it from `entry`, never stepping THROUGH the merge or a
|
|
623
906
|
// sibling arm's entry, and classify what it steps INTO. `siblings` is every OTHER arm entry the
|
|
624
907
|
// caller can emit a `case`/`default` label for — the merge is deliberately not among them, so a
|
|
@@ -649,19 +932,27 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
649
932
|
// under this arm. That is duplication, not a wrong dispatch — the same duplication the structurer
|
|
650
933
|
// already does for any tail two arms share, and how the case bodies agbcc tail-merged are put
|
|
651
934
|
// back. Costly for matching, correct to run.
|
|
935
|
+
//
|
|
936
|
+
// `jumps` are blocks an edge into which is a jump that ends its path where it sits, whatever the
|
|
937
|
+
// switch: the latch of a loop spelled `for`, which the structurer writes as `continue;`. Such an
|
|
938
|
+
// edge is neither a fall into a sibling nor an escape, and an arm that IS one is a bare jump.
|
|
652
939
|
const analyzeArmExit = (
|
|
653
940
|
entry: Block,
|
|
654
941
|
b: Block,
|
|
655
942
|
merge: Block | null,
|
|
656
943
|
siblings: Set<Block>,
|
|
657
944
|
stopAtLoop = false,
|
|
945
|
+
jumps: ReadonlySet<Block> = new Set(),
|
|
658
946
|
): ArmExit => {
|
|
659
947
|
if (entry === merge) {
|
|
660
948
|
return { kind: 'break' }; // an empty arm (a table slot pointing straight at the switch's end)
|
|
661
949
|
}
|
|
950
|
+
if (jumps.has(entry)) {
|
|
951
|
+
return { kind: 'break', region: new Set([entry]) };
|
|
952
|
+
}
|
|
662
953
|
const into = new Set<Block>(); // sibling entries this arm flows into
|
|
954
|
+
const escapedTo = new Set<Block>(); // the blocks outside the switch's region it steps into
|
|
663
955
|
let toMerge = false,
|
|
664
|
-
escapes = false,
|
|
665
956
|
leavesLoop = false;
|
|
666
957
|
const seen = new Set<Block>([entry]);
|
|
667
958
|
const q = [entry];
|
|
@@ -671,11 +962,13 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
671
962
|
for (const s of successorsOf(cur)) {
|
|
672
963
|
if (s === merge) {
|
|
673
964
|
toMerge = true;
|
|
965
|
+
} else if (jumps.has(s)) {
|
|
966
|
+
continue;
|
|
674
967
|
} else if (s !== entry && siblings.has(s)) {
|
|
675
968
|
into.add(s);
|
|
676
969
|
} else if (s !== entry && !seen.has(s)) {
|
|
677
970
|
if (!dom.get(s)!.has(b)) {
|
|
678
|
-
|
|
971
|
+
escapedTo.add(s);
|
|
679
972
|
continue;
|
|
680
973
|
}
|
|
681
974
|
seen.add(s);
|
|
@@ -684,7 +977,7 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
684
977
|
}
|
|
685
978
|
}
|
|
686
979
|
if (into.size === 0) {
|
|
687
|
-
return
|
|
980
|
+
return escapedTo.size > 0 ? { kind: 'break', escapedTo } : { kind: 'break', region: seen };
|
|
688
981
|
}
|
|
689
982
|
// A path to the merge beside the fall-through is the arm's own `break;` — switch-scoped, so it
|
|
690
983
|
// spells only where nothing breakable sits between that edge and the arm, which the structurer
|
|
@@ -695,7 +988,17 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
695
988
|
why: `a case body falls into sibling case ${[...into].map((x) => `#${layoutIndex(x)}`).join(', ')}, reaches the end of the switch, and leaves the loop the switch sits in`,
|
|
696
989
|
};
|
|
697
990
|
}
|
|
698
|
-
|
|
991
|
+
// A sideways path that only RETURNS does not stop the fall-through (`onlyReturns`): it is a
|
|
992
|
+
// jump that ends where it sits, as a `ret` inside the arm is. Regime B only. Regime A
|
|
993
|
+
// (`stopAtLoop`) commits to its `switch` on gates that read an arm's `breaks` alone, and has
|
|
994
|
+
// if-recovery to fall back on, so a falling arm beside an escape stays `unstructurable` there:
|
|
995
|
+
// admitted, it commits Regime A to switches whose emission then refuses the whole function (an
|
|
996
|
+
// escape to a `goto out` past a loop exit, or out of a nested switch) where if-recovery spells
|
|
997
|
+
// them.
|
|
998
|
+
if (
|
|
999
|
+
into.size === 1 &&
|
|
1000
|
+
(escapedTo.size === 0 || (!stopAtLoop && [...escapedTo].every((s) => onlyReturns(s, b, merge, siblings))))
|
|
1001
|
+
) {
|
|
699
1002
|
return { kind: 'fallthrough', to: [...into][0], breaks: toMerge, region: seen };
|
|
700
1003
|
}
|
|
701
1004
|
// Name what is actually missing. These are two different facts: the first is a shape C's
|
|
@@ -713,7 +1016,11 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
713
1016
|
};
|
|
714
1017
|
};
|
|
715
1018
|
|
|
716
|
-
const recognizeSwitch = (
|
|
1019
|
+
const recognizeSwitch = (
|
|
1020
|
+
b: Block,
|
|
1021
|
+
stop: Block | null,
|
|
1022
|
+
mergeAt?: (arms: readonly Block[]) => Block | undefined,
|
|
1023
|
+
): Stmt[] | null => {
|
|
717
1024
|
const root = testInfo(b);
|
|
718
1025
|
if (!root) {
|
|
719
1026
|
return null;
|
|
@@ -937,8 +1244,8 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
937
1244
|
// a sibling beside a block the switch does not dominate — and Regime A declines to if-recovery,
|
|
938
1245
|
// which spells every one of those edges. (Regime B reads the same verdicts and fails LOUD on
|
|
939
1246
|
// either.)
|
|
940
|
-
const merge = ipdom.get(b) ?? stop;
|
|
941
1247
|
const targets = new Set<Block>([...caseBlocks, ...(defaultBlk ? [defaultBlk] : [])]);
|
|
1248
|
+
const merge = mergeAt?.([...targets]) ?? ipdom.get(b) ?? stop;
|
|
942
1249
|
const siblings = new Set([...targets].filter((t) => t !== merge));
|
|
943
1250
|
const exitOf = new Map<Block, ArmExit>();
|
|
944
1251
|
for (const t of siblings) {
|
|
@@ -1150,6 +1457,13 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
1150
1457
|
// The `default:` arm is a chain member too — an arm may run on into it, and C prints it last.
|
|
1151
1458
|
const dfltArm = defaultBlk !== null && defaultBlk !== merge ? defaultBlk : null;
|
|
1152
1459
|
const preChain = [...armsByBlock.keys()];
|
|
1460
|
+
// NO COPIES HERE. Arms a cross-jump sent into one sibling's body do not linearize, and Regime
|
|
1461
|
+
// B reads them back as copies (structure.ts, THE COPIES) because its only alternative is a loud
|
|
1462
|
+
// failure. Regime A's alternative is if-recovery, which LIFTS: a copy reading here would replace
|
|
1463
|
+
// one lift with another, the choice between two legal spellings of one CFG that only a ranked
|
|
1464
|
+
// variation may make (docs/level-tower.md). On comparison trees agbcc and KMC cross-jumped,
|
|
1465
|
+
// the switch spelling measured nearer the target on some and further on others, and no corpus
|
|
1466
|
+
// row reaches this point, so the refusal stands and if-recovery answers.
|
|
1153
1467
|
const entries = chainArms(preChain, dfltArm, exitOf);
|
|
1154
1468
|
if (entries === null) {
|
|
1155
1469
|
return null;
|
|
@@ -1279,7 +1593,12 @@ export function makeSwitchRecovery(deps: SwitchRecoverDeps): SwitchRecovery {
|
|
|
1279
1593
|
}
|
|
1280
1594
|
return out;
|
|
1281
1595
|
};
|
|
1282
|
-
|
|
1596
|
+
/** `copySharedFallTargets`' `sidewaysEnds`: does every path from `s`, a block outside the
|
|
1597
|
+
* switch's region, run only forward to `merge` or a `ret` (`onlyReturns` with the merge as an
|
|
1598
|
+
* end)? */
|
|
1599
|
+
const forwardToMerge = (s: Block, b: Block, merge: Block | null, siblings: ReadonlySet<Block>): boolean =>
|
|
1600
|
+
merge !== null && onlyReturns(s, b, merge, siblings, true);
|
|
1601
|
+
return { recognizeSwitch, analyzeArmExit, forwardToMerge, layoutIndex, defaultLayoutPos, chainArms };
|
|
1283
1602
|
}
|
|
1284
1603
|
|
|
1285
1604
|
/** Internal surface for this module's own tests, and for nothing else: PRE3 reads each test
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// AN EARLY RETURN THAT THE JOIN PAST EARLY RETURNS CANNOT SPELL: a return of the value its branch
|
|
2
|
+
// tested equal to a constant, where the object still returns the value and not the constant.
|
|
3
|
+
//
|
|
4
|
+
// On a compiler whose cse folds a branch-proven constant only into the branch's FALL-THROUGH arm
|
|
5
|
+
// (target.ts `foldsBranchConstantOnTakenEdge` false: gcc 2.x at -O1), `if (x == 0) return x;`
|
|
6
|
+
// compiles to `return 0`, and so does the same test spelled `if (x != 0) ; else return x;`. The join
|
|
7
|
+
// writes the early return one of those two ways. A return of `x` the object left unfolded was
|
|
8
|
+
// therefore the jump-target arm of its branch in the source: the `else` of `if (x != 0) { ...tail }
|
|
9
|
+
// else { return x; }`, which is the copies' spelling. So this mark publishes the copies.
|
|
10
|
+
//
|
|
11
|
+
// THE CONSTANT IS NOT REMATERIALISED ON THE EDGE. Read off the lifted IR: the return block's value
|
|
12
|
+
// is the very SSA value the comparison reads, not a constant. A delay slot that set the return
|
|
13
|
+
// register to the constant (`beqz v0,L; move v0,zero`) lifts as a `const` returned on that edge, and
|
|
14
|
+
// is no mark.
|
|
15
|
+
//
|
|
16
|
+
// MEASURED on the probe pair `corpus/probe-rettail-pg-{copies,join}.c` (unfolded-branch-constant.test.ts)
|
|
17
|
+
// and on Mario Party 3's `func_8010BD28_472F98_mgmode`, which it is the shape of: gcc2.7.2 -O1, the
|
|
18
|
+
// copies `nop` in the `beqz`'s slot and the join `move v0,zero`. There the join loses the copies'
|
|
19
|
+
// match (0 -> 1 line off, really compiled, 2026-11-06).
|
|
20
|
+
//
|
|
21
|
+
// AN `if` SITE ONLY. The premise is an `if`'s two arms, one of them the fall-through. A switch's case
|
|
22
|
+
// arms, read as a switch (`tree`) or a jump table (`switch`), are each a jump target, a case label,
|
|
23
|
+
// so no return in one folds the switch value at -O1 whatever the source wrote, and an unfolded one
|
|
24
|
+
// says nothing. Compiled at gcc2.7.2 -O1: `switch (x) { case 0: return x; case 5: p[1] = 2; break; }`
|
|
25
|
+
// followed by its tail, written with the join, keeps `move v0,a0` in the case-0 jump's slot, and
|
|
26
|
+
// the join and the copies both compile byte-exact. A switch the structurer reads as `if`s (three
|
|
27
|
+
// cases of that function) is still read, the residue of the rule: no reading of it is exact.
|
|
28
|
+
//
|
|
29
|
+
// A MARK IS EVIDENCE, NOT PROOF, as structure/cross-jumped-tail.ts says of its own: it moves which
|
|
30
|
+
// verified reading is the default, never what is lifted. Read only where the target says its cse
|
|
31
|
+
// leaves the taken edge unfolded; elsewhere (`true`, or absent) an unfolded return says nothing.
|
|
32
|
+
import type { Block, Fn, Op, Value } from '../ir/core';
|
|
33
|
+
import type { JoinPastReturnsSite } from './structure';
|
|
34
|
+
|
|
35
|
+
const succsOf = (b: Block): Block[] => [...new Set(b.ops[b.ops.length - 1]?.successors.map((s) => s.block) ?? [])];
|
|
36
|
+
|
|
37
|
+
/** Does an early return of the `if` site `site` give back, unfolded, the value its own branch tested
|
|
38
|
+
* equal to a constant? The return block holds only its `ret`, has one predecessor, and that
|
|
39
|
+
* predecessor's `cond_br` takes it on the equal side of an `icmp_eq`/`icmp_ne` of that value against
|
|
40
|
+
* a `const`. */
|
|
41
|
+
export function unfoldedBranchConstant(fn: Fn, site: JoinPastReturnsSite): boolean {
|
|
42
|
+
if (site.kind !== 'if') {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
const def = new Map<Value, Op>();
|
|
46
|
+
const preds = new Map<Block, Block[]>(fn.blocks.map((b) => [b, []]));
|
|
47
|
+
for (const b of fn.blocks) {
|
|
48
|
+
for (const op of b.ops) {
|
|
49
|
+
for (const r of op.results) {
|
|
50
|
+
def.set(r, op);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
for (const s of succsOf(b)) {
|
|
54
|
+
preds.get(s)?.push(b);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const seen = new Set<Block>();
|
|
58
|
+
const stack = [site.branch];
|
|
59
|
+
while (stack.length) {
|
|
60
|
+
const x = stack.pop()!;
|
|
61
|
+
if (x === site.join || seen.has(x)) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
seen.add(x);
|
|
65
|
+
stack.push(...succsOf(x));
|
|
66
|
+
}
|
|
67
|
+
for (const r of seen) {
|
|
68
|
+
const ret = r.ops.length === 1 ? r.ops[0] : undefined;
|
|
69
|
+
const value = ret?.opcode === 'ret' && ret.operands.length === 1 ? ret.operands[0] : undefined;
|
|
70
|
+
const from = preds.get(r) ?? [];
|
|
71
|
+
if (value === undefined || from.length !== 1 || fn.asmLayout?.get(r) === undefined) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const br = from[0].ops[from[0].ops.length - 1];
|
|
75
|
+
const cmp = br?.opcode === 'cond_br' ? def.get(br.operands[0]) : undefined;
|
|
76
|
+
if (cmp === undefined || br.successors.length !== 2 || br.successors[0].block === br.successors[1].block) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const equalSide = cmp.opcode === 'icmp_eq' ? 0 : cmp.opcode === 'icmp_ne' ? 1 : -1;
|
|
80
|
+
if (equalSide < 0 || br.successors[equalSide].block !== r || cmp.operands.length !== 2) {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const [a, b] = cmp.operands;
|
|
84
|
+
const isConst = (v: Value) => def.get(v)?.opcode === 'const';
|
|
85
|
+
if ((a === value && isConst(b)) || (b === value && isConst(a))) {
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return false;
|
|
90
|
+
}
|