@chenglou/freerange 0.0.1

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.
@@ -0,0 +1,1723 @@
1
+ import {
2
+ remainderNumbers,
3
+ roundedNumber,
4
+ squareRootNumber,
5
+ nextDown,
6
+ nextUp,
7
+ absoluteNumber,
8
+ addNumbers,
9
+ constantNumber,
10
+ divideNumbers,
11
+ divideNumbersNonzeroDivisor,
12
+ floorNumber,
13
+ finiteNumberPart,
14
+ includesZero,
15
+ isDefinitelyZero,
16
+ isFiniteNumber,
17
+ maximumNumbers,
18
+ minimumNumbers,
19
+ multiplyNumbers,
20
+ pointExcluded,
21
+ subtractNumbers,
22
+ type AbstractNumber,
23
+ } from '../domain/number.ts'
24
+ import {
25
+ joinValues,
26
+ recordProperty,
27
+ recordPropertiesByName,
28
+ recordValue,
29
+ tryJoinValues,
30
+ unknownBoolean,
31
+ type AbstractBoolean,
32
+ type AbstractRecord,
33
+ type AbstractTaggedUnion,
34
+ type AbstractValue,
35
+ type TaggedVariant,
36
+ } from '../domain/value.ts'
37
+ import type {FunctionID, SiteID, ValueID} from '../ir/ids.ts'
38
+ import type {ComparisonOperator, InstructionIR} from '../ir/instructions.ts'
39
+ import {coveringKindValue, declaredKindOf, type DeclaredKind, type ProgramIR} from '../ir/program.ts'
40
+ import {
41
+ addPrecondition,
42
+ constantRequirementStatus,
43
+ peelNonzero,
44
+ canonicalValueKey,
45
+ numericExpression,
46
+ resolveStoredValue,
47
+ sameRuntimeValue,
48
+ staticRequirement,
49
+ type ExpressionContext,
50
+ } from '../requirements/infer.ts'
51
+ import type {BoundsAssumption, InferredPrecondition, NumericExpression} from '../requirements/model.ts'
52
+ import {completedEvaluation, type FunctionEvaluation, type RequirementFailure, type Stop} from './outcome.ts'
53
+ import {
54
+ addValueFact,
55
+ cloneState,
56
+ hasIndexFact,
57
+ hasNonzeroFact,
58
+ type ExecutionState,
59
+ type SharedState,
60
+ type ValueFact,
61
+ } from './state.ts'
62
+
63
+ type EvaluateFunction = (
64
+ functionID: FunctionID,
65
+ arguments_: AbstractValue[],
66
+ argumentExpressions: Array<NumericExpression | null>,
67
+ sharedState: SharedState,
68
+ callStack: FunctionID[],
69
+ valueFacts: ValueFact[],
70
+ parameterIdentityKeys: string[],
71
+ identityNamespace: string,
72
+ ) => FunctionEvaluation
73
+
74
+ export type TransferContext = {
75
+ program: ProgramIR
76
+ callStack: FunctionID[]
77
+ expressionContext: ExpressionContext
78
+ preconditions: InferredPrecondition[]
79
+ // Element reads the engine could not prove in bounds — the peer of preconditions,
80
+ // accumulated per evaluation and adopted from completed callees the same way.
81
+ boundsAssumptions: BoundsAssumption[]
82
+ evaluateFunction: EvaluateFunction
83
+ }
84
+
85
+ // TypeScript's narrowing is an open-ended set of rules; the analyzer models the common
86
+ // shapes (null checks, ?? , nested guards) and consults the checker's types at every
87
+ // gate, but a value can still reach an operation whose kind the local narrowing did not
88
+ // establish. That is a mismatch between two narrowing systems, not an accepted-subset
89
+ // violation, so it degrades to a per-path stop (owner decision) instead of crashing the
90
+ // run — thrown here, converted to a stop at the single catch in evaluateInstruction.
91
+ class KindMismatch extends Error {
92
+ constructor(message: string, readonly value: ValueID) {
93
+ super(message)
94
+ }
95
+ }
96
+
97
+ type ValueStep = {kind: 'value'; value: AbstractValue}
98
+
99
+ // One instruction either produces a value, records an assertion observation, or stops the
100
+ // current path.
101
+ export type StepResult =
102
+ | ValueStep
103
+ | {
104
+ kind: 'assertion'
105
+ assertion: number
106
+ observation: AbstractBoolean
107
+ value: Extract<AbstractValue, {kind: 'void'}>
108
+ }
109
+ | {kind: 'stop'; stop: Stop}
110
+ // The path ends contributing nothing — a call to a function that throws on every path,
111
+ // behaving exactly like an inline throw: no return value, no stop record.
112
+ | {kind: 'ends'}
113
+
114
+ function failedRequirement(failure: RequirementFailure): StepResult {
115
+ return {kind: 'stop', stop: {
116
+ site: failure.site,
117
+ reason: {kind: 'requirementFailure', failure, callee: null},
118
+ }}
119
+ }
120
+
121
+ // The three ways an instruction arm produces its value, typed so a freshly computed number
122
+ // cannot leave evaluateInstruction without the blame stamp: value() rejects numbers at the
123
+ // type level, computedNumber() stamps, and passthroughValue() is the one named escape hatch
124
+ // for values whose numbers were already stamped where they were produced (reads, call
125
+ // results, constants — stamping constants would newly blame overflowing literals).
126
+ function value(result: Exclude<AbstractValue, AbstractNumber>): ValueStep {
127
+ return {kind: 'value', value: result}
128
+ }
129
+
130
+ function passthroughValue(result: AbstractValue): ValueStep {
131
+ return {kind: 'value', value: result.kind === 'number' ? normalizeRefinedNumber(result) : result}
132
+ }
133
+
134
+ // Report annotations only: a degraded result inherits the relevant operand site, or, when
135
+ // every operand still had the property, records this operation. Finiteness and NaN use
136
+ // separate sites because an overflow can enable a later operation to produce NaN without
137
+ // being the operation that produced it.
138
+ function computedNumber(raw: AbstractNumber, operands: AbstractNumber[], site: SiteID): ValueStep {
139
+ return {kind: 'value', value: withLossBlame(normalizeRefinedNumber(raw), operands, site)}
140
+ }
141
+
142
+ function withLossBlame(result: AbstractNumber, operands: AbstractNumber[], site: SiteID): AbstractNumber {
143
+ if (isFiniteNumber(result) && !result.mayBeNaN) return result
144
+ let annotated = result
145
+ if (!isFiniteNumber(result) && result.nonFiniteSite == null) {
146
+ const carrier = operands.find(operand => !isFiniteNumber(operand) && operand.nonFiniteSite != null)
147
+ const nonFiniteSite = carrier?.nonFiniteSite
148
+ ?? (operands.every(operand => isFiniteNumber(operand)) ? site : undefined)
149
+ if (nonFiniteSite != null) annotated = {...annotated, nonFiniteSite}
150
+ }
151
+ if (result.mayBeNaN && result.nanSite == null) {
152
+ const carrier = operands.find(operand => operand.mayBeNaN && operand.nanSite != null)
153
+ const nanSite = carrier?.nanSite
154
+ ?? (operands.every(operand => !operand.mayBeNaN) ? site : undefined)
155
+ if (nanSite != null) annotated = {...annotated, nanSite}
156
+ }
157
+ return annotated
158
+ }
159
+
160
+ export function evaluateInstruction(
161
+ instruction: InstructionIR,
162
+ state: ExecutionState,
163
+ context: TransferContext,
164
+ ): StepResult {
165
+ try {
166
+ return evaluateInstructionKinded(instruction, state, context)
167
+ } catch (error) {
168
+ if (error instanceof KindMismatch) {
169
+ const missingElementSite = possiblyMissingElementReadSite(
170
+ state,
171
+ error.value,
172
+ context.expressionContext.instructionByValue,
173
+ )
174
+ if (missingElementSite != null) {
175
+ return {kind: 'stop', stop: {site: missingElementSite, reason: {kind: 'possiblyMissingElement'}}}
176
+ }
177
+ return {kind: 'stop', stop: {site: instruction.site, reason: {kind: 'kindMismatch'}}}
178
+ }
179
+ throw error
180
+ }
181
+ }
182
+
183
+ function evaluateInstructionKinded(
184
+ instruction: InstructionIR,
185
+ state: ExecutionState,
186
+ context: TransferContext,
187
+ ): StepResult {
188
+ switch (instruction.kind) {
189
+ case 'constant': return passthroughValue(constantNumber(instruction.value))
190
+ case 'nullishConstant': return value({kind: 'nullish', sentinels: instruction.sentinel})
191
+ case 'opaqueConstant': return value(
192
+ instruction.content == null ? {kind: 'opaque'} : {kind: 'opaque', content: instruction.content})
193
+ case 'unknownBoolean': return value(unknownBoolean())
194
+ case 'arrayLiteral': {
195
+ const elements = instruction.elements.map(id => requiredValue(state, id))
196
+ if (instruction.form === 'tuple') return value({kind: 'tuple', elements})
197
+ const element = elements.length === 0 ? null : elements.reduce((joined, next) => joinValues(joined, next))
198
+ return value({kind: 'array', element, length: constantNumber(instruction.elements.length)})
199
+ }
200
+ case 'arrayLength': {
201
+ const sequence = requiredSequence(state, instruction.array)
202
+ return passthroughValue(sequence.kind === 'tuple'
203
+ ? constantNumber(sequence.elements.length)
204
+ : sequence.length)
205
+ }
206
+ case 'arrayIndex': {
207
+ const sequence = requiredSequence(state, instruction.array)
208
+ const index = requiredNumberWithFacts(state, instruction.index, context.expressionContext)
209
+ const element = sequence.kind === 'tuple'
210
+ ? tupleElement(sequence, index)
211
+ : sequence.element
212
+ const length = sequence.kind === 'tuple' ? constantNumber(sequence.elements.length) : sequence.length
213
+ const indexKey = canonicalValueKey(instruction.index, context.expressionContext)
214
+ const arrayKey = canonicalValueKey(instruction.array, context.expressionContext)
215
+ const assumedValid = hasIndexFact(state.valueFacts, 'validIndex', indexKey, arrayKey)
216
+ // Three proofs of in-bounds: a complete prior requirement, intervals, or the strict
217
+ // below-length half from a guard combined with the index's own integer/nonnegative
218
+ // facts. A for-of loop reaches the same third proof through its generated guard.
219
+ const inBounds = assumedValid
220
+ || (index.integer && !index.mayBeNaN && index.lower >= 0 && index.upper < length.lower)
221
+ || (index.integer && !index.mayBeNaN && index.lower >= 0
222
+ && hasIndexFact(state.valueFacts, 'belowLength', indexKey, arrayKey))
223
+ // A provably out-of-bounds read: for the asserted form the assertion lied; for the
224
+ // bare form the value is exactly undefined. An empty sequence is the special case
225
+ // where every read is out of bounds.
226
+ const firstPossibleIndex = Math.ceil(Math.max(index.lower, 0))
227
+ const lastPossibleIndex = Math.floor(Math.min(index.upper, length.upper - 1))
228
+ const provablyOut = element == null
229
+ // Array indexes are nonnegative integers below the array's length. If the index
230
+ // and length ranges admit no such integer, every concrete read misses.
231
+ || firstPossibleIndex > lastPossibleIndex
232
+ if (provablyOut) {
233
+ if (instruction.mode === 'asserted') {
234
+ return failedRequirement({kind: 'elementInBounds', site: instruction.site})
235
+ }
236
+ return value({kind: 'nullish', sentinels: 'undefined'})
237
+ }
238
+ if (instruction.mode === 'bare' || instruction.mode === 'bareUnchecked') {
239
+ // Bare arr[i] types T | undefined; a proven read cannot miss, an unproven one
240
+ // honestly carries the possibility.
241
+ return inBounds
242
+ ? passthroughValue(element)
243
+ : passthroughValue(joinValues({kind: 'nullish', sentinels: 'undefined'}, element))
244
+ }
245
+ if (!inBounds) {
246
+ // The caller-actionable form wins when both sides are nameable: a requires line
247
+ // the caller can satisfy, instead of an assumes line the entry merely rests on.
248
+ const indexExpression = numericExpression(instruction.index, context.expressionContext)
249
+ const sequenceExpression = numericExpression(instruction.array, context.expressionContext)
250
+ if (indexExpression != null && sequenceExpression != null) {
251
+ addPrecondition(context.preconditions, {
252
+ kind: 'inBounds',
253
+ index: indexExpression,
254
+ sequence: sequenceExpression,
255
+ site: instruction.site,
256
+ })
257
+ } else {
258
+ addBoundsAssumption(context.boundsAssumptions, {site: instruction.site, kind: 'elementInBounds'})
259
+ }
260
+ // The function's contract now assumes this read is valid. Keep the index facts
261
+ // and the exact array/index relationship for later reads on this path.
262
+ writeThroughProducers(
263
+ state, instruction.index, validIndexNumber(index),
264
+ context.expressionContext.instructionByValue,
265
+ )
266
+ addValueFact(state.valueFacts, {kind: 'validIndex', index: indexKey, array: arrayKey})
267
+ }
268
+ return passthroughValue(element)
269
+ }
270
+ case 'numberCheck': {
271
+ const operand = requiredNumberWithFacts(state, instruction.value, context.expressionContext)
272
+ return value(evaluateNumberCheck(instruction.predicate, operand))
273
+ }
274
+ case 'tagCheck': {
275
+ const operand = requiredValue(state, instruction.union)
276
+ // A plain record can reach a tag check: a builder whose declared return is a single
277
+ // variant produces a record, and the caller's union-typed binding checks its tag.
278
+ // The record's tag value is an opaque string the analysis never learned, so the
279
+ // check is honestly unknown and both branches analyze — the same dispatch the
280
+ // shared-shape classification used to give this code.
281
+ if (operand.kind === 'record') return value(unknownBoolean())
282
+ const union = requiredTaggedUnion(state, instruction.union)
283
+ const matches = union.variants.some(variant => variant.tagValue === instruction.tagValue)
284
+ const misses = union.variants.some(variant => variant.tagValue !== instruction.tagValue)
285
+ const equals: AbstractBoolean = {kind: 'boolean', canBeTrue: matches, canBeFalse: misses}
286
+ return value(instruction.negated
287
+ ? {kind: 'boolean', canBeTrue: equals.canBeFalse, canBeFalse: equals.canBeTrue}
288
+ : equals)
289
+ }
290
+ case 'nullishCheck': {
291
+ const operand = requiredValue(state, instruction.value)
292
+ // Opaque carries no claims, so the runtime value may itself be null or undefined —
293
+ // an unknown-typed parameter often is. A bare opaque, or a maybeNullish whose inner
294
+ // is opaque, therefore answers unknown: the sentinels set only describes what the
295
+ // nullish side of a join contributed, not what the opaque side may hold. (A review
296
+ // round caught the old definitely-false answer publishing a dead branch for
297
+ // `if (value === undefined)` on an unknown-typed value.)
298
+ const opaqueInside = operand.kind === 'opaque'
299
+ || (operand.kind === 'maybeNullish' && operand.inner.kind === 'opaque')
300
+ const canBeSentinel = opaqueInside
301
+ || (operand.kind === 'nullish' || operand.kind === 'maybeNullish'
302
+ ? instruction.sentinel === 'nullish' || sentinelsAdmit(operand.sentinels, instruction.sentinel)
303
+ : false)
304
+ const canMiss = operand.kind === 'nullish'
305
+ // A pure missing value fails a strict check only when it can be the OTHER sentinel.
306
+ ? instruction.sentinel !== 'nullish' && operand.sentinels !== instruction.sentinel
307
+ : true
308
+ const equals: AbstractBoolean = {kind: 'boolean', canBeTrue: canBeSentinel, canBeFalse: canMiss}
309
+ return value(instruction.negated
310
+ ? {kind: 'boolean', canBeTrue: equals.canBeFalse, canBeFalse: equals.canBeTrue}
311
+ : equals)
312
+ }
313
+ case 'booleanConstant': return value({
314
+ kind: 'boolean',
315
+ canBeTrue: instruction.value,
316
+ canBeFalse: !instruction.value,
317
+ })
318
+ case 'moduleRead': {
319
+ const slot = state.shared[instruction.binding]
320
+ if (slot === undefined) throw new Error(`Unknown module binding ${instruction.binding}`)
321
+ if (slot === null) {
322
+ return {kind: 'stop', stop: {site: instruction.site, reason: {kind: 'moduleRead', binding: instruction.binding}}}
323
+ }
324
+ return passthroughValue(slot)
325
+ }
326
+ case 'moduleWrite': {
327
+ const assigned = requiredValue(state, instruction.value)
328
+ const binding = context.program.moduleBindings[instruction.binding]
329
+ if (binding == null) throw new Error(`Unknown module binding ${instruction.binding}`)
330
+ // An opaque binding's declared type spans value kinds (e.g. `unknown`), so two paths
331
+ // could put a number and a boolean in one slot and meet at a join, which only handles
332
+ // matching kinds. Reads of opaque bindings stop regardless, so the slot stays
333
+ // uninitialized instead of holding a value nothing may consume. Every other writable
334
+ // category is single-kind: value/kind writes are type-checked against the declared
335
+ // number, boolean, or record shape.
336
+ if (binding.category.kind !== 'opaque') {
337
+ state.shared[instruction.binding] = assigned
338
+ }
339
+ return passthroughValue(assigned)
340
+ }
341
+ case 'moduleHavoc': {
342
+ const binding = context.program.moduleBindings[instruction.binding]
343
+ if (binding == null) throw new Error(`Unknown module binding ${instruction.binding}`)
344
+ const declaredKind = declaredKindOf(binding.category)
345
+ // Covering, not assumed-finite: values computed from this slot can publish without
346
+ // any assumes line, so the reset must include NaN and infinities.
347
+ state.shared[instruction.binding] = declaredKind == null
348
+ ? null
349
+ : coveringKindValue(declaredKind)
350
+ return value({kind: 'void'})
351
+ }
352
+ case 'object': {
353
+ const record = recordValue(instruction.properties.map(property => ({
354
+ name: property.name,
355
+ value: requiredValue(state, property.value),
356
+ })))
357
+ if (instruction.tag == null) return value(record)
358
+ // The variant pin comes from the tag property's VALUE — known string content (a
359
+ // written literal, or a tag seeded from its declared variant and carried through
360
+ // spreads and bindings) or an exact boolean. The checker's TYPE for the tag is
361
+ // deliberately not consulted: a review round chained three assertion launders
362
+ // (cast tag, quoted-key cast tag, spread of a cast-tagged template) through the
363
+ // type channel, while erased casts by construction carry no content. A tag value
364
+ // the engine cannot pin leaves the literal a plain record, whose tag checks
365
+ // dispatch as unknown booleans and whose reads fall to the record-hull backstop.
366
+ const tagPropertyValue = recordProperty(record, instruction.tag.property)
367
+ const pinned = tagPropertyValue?.kind === 'opaque' && tagPropertyValue.content != null
368
+ ? tagPropertyValue.content
369
+ : tagPropertyValue?.kind === 'boolean' && tagPropertyValue.canBeTrue !== tagPropertyValue.canBeFalse
370
+ ? tagPropertyValue.canBeTrue
371
+ : null
372
+ if (pinned == null) return value(record)
373
+ return value({
374
+ kind: 'taggedUnion',
375
+ tagProperty: instruction.tag.property,
376
+ variants: [{tagValue: pinned, record}],
377
+ })
378
+ }
379
+ case 'property': {
380
+ const object = requiredValue(state, instruction.object)
381
+ // A read through a tagged union: a single remaining variant (after a tag check)
382
+ // reads like the plain record it is; with several variants left, a property every
383
+ // variant carries reads as the join of the per-variant values — a fact true no
384
+ // matter which shape the value is. A property only SOME variants carry needs a tag
385
+ // check first, and reaching here without one is an unmodeled-narrowing stop, not a
386
+ // crash (requiredRecord throws KindMismatch below for non-record kinds already).
387
+ if (object.kind === 'taggedUnion') {
388
+ const variantProperty = (variant: TaggedVariant): AbstractValue => {
389
+ const inVariant = recordProperty(variant.record, instruction.property)
390
+ if (inVariant == null) {
391
+ throw new KindMismatch(`Variant ${variant.tagValue} has no property ${instruction.property}`, instruction.object)
392
+ }
393
+ return inVariant
394
+ }
395
+ const [firstVariant, ...restVariants] = object.variants
396
+ let joined: AbstractValue = variantProperty(firstVariant)
397
+ for (const variant of restVariants) {
398
+ const next = tryJoinValues(joined, variantProperty(variant))
399
+ if (next == null) {
400
+ throw new KindMismatch(`Property ${instruction.property} mixes kinds across variants`, instruction.object)
401
+ }
402
+ joined = next
403
+ }
404
+ return passthroughValue(joined)
405
+ }
406
+ const record = requiredRecord(state, instruction.object)
407
+ const propertyValue = recordProperty(record, instruction.property)
408
+ // A missing property is an honest per-path stop, not a crash: a tagged union that
409
+ // met a plain record degraded to their shared hull, and a read past the hull is
410
+ // exactly a narrowing the analysis did not model. (Before hulls existed this was a
411
+ // gate-bug tripwire; the backstop bucket is what review rounds audit now.)
412
+ if (propertyValue == null) {
413
+ throw new KindMismatch(`Record has no property ${instruction.property}`, instruction.object)
414
+ }
415
+ return passthroughValue(propertyValue)
416
+ }
417
+ case 'compare': {
418
+ // Equality dispatches on the operand kind: booleans answer from their exact
419
+ // two-point lattice, numbers from their intervals.
420
+ const left = requiredValue(state, instruction.left)
421
+ const right = requiredValue(state, instruction.right)
422
+ const same = sameRuntimeValue(instruction.left, instruction.right, context.expressionContext)
423
+ if (left.kind === 'boolean' && right.kind === 'boolean'
424
+ && (instruction.operator === 'equal' || instruction.operator === 'notEqual')) {
425
+ if (same) return value(exactBoolean(instruction.operator === 'equal'))
426
+ return value(compareBooleans(left, right, instruction.operator === 'notEqual'))
427
+ }
428
+ const leftNumber = requiredNumberWithFacts(state, instruction.left, context.expressionContext)
429
+ const rightNumber = requiredNumberWithFacts(state, instruction.right, context.expressionContext)
430
+ if (same) {
431
+ return value(compareSameNumber(intersectSameNumbers(leftNumber, rightNumber), instruction.operator))
432
+ }
433
+ const intervalResult = compareNumbers(leftNumber, rightNumber, instruction.operator)
434
+ return value(intervalResult)
435
+ }
436
+ case 'parsedNumber': return computedNumber({
437
+ kind: 'number',
438
+ lower: Number.NEGATIVE_INFINITY,
439
+ upper: Number.POSITIVE_INFINITY,
440
+ integer: instruction.integer,
441
+ mayBeNaN: true,
442
+ }, [], instruction.site)
443
+ case 'stringLength': return computedNumber({
444
+ kind: 'number',
445
+ lower: 0,
446
+ upper: Number.MAX_SAFE_INTEGER,
447
+ integer: true,
448
+ mayBeNaN: false,
449
+ }, [], instruction.site)
450
+ case 'mathUnary': {
451
+ const operand = requiredNumberWithFacts(state, instruction.value, context.expressionContext)
452
+ return computedNumber(
453
+ instruction.operator === 'sqrt' ? squareRootNumber(operand) : roundedNumber(instruction.operator, operand),
454
+ [operand],
455
+ instruction.site,
456
+ )
457
+ }
458
+ case 'floor': {
459
+ const operand = requiredNumberWithFacts(state, instruction.value, context.expressionContext)
460
+ return computedNumber(floorNumber(operand), [operand], instruction.site)
461
+ }
462
+ case 'platformValue': return passthroughValue({
463
+ kind: 'number',
464
+ lower: instruction.lower,
465
+ upper: instruction.upper,
466
+ integer: instruction.integer,
467
+ mayBeNaN: false,
468
+ })
469
+ case 'absolute': {
470
+ const operand = requiredNumberWithFacts(state, instruction.value, context.expressionContext)
471
+ return computedNumber(absoluteNumber(operand), [operand], instruction.site)
472
+ }
473
+ case 'not': {
474
+ const operand = requiredBoolean(state, instruction.value)
475
+ return value({kind: 'boolean', canBeTrue: operand.canBeFalse, canBeFalse: operand.canBeTrue})
476
+ }
477
+ case 'staticAssert': {
478
+ const observation = staticAssertionObservation(instruction.value, state, context)
479
+ return {kind: 'assertion', assertion: instruction.assertion, observation, value: {kind: 'void'}}
480
+ }
481
+ case 'staticRequire': {
482
+ const failureKind = instruction.purpose === 'finiteInput' ? 'finiteInput' : 'declared'
483
+ const check = context.expressionContext.instructionByValue[instruction.value]
484
+ if (check?.kind !== 'compare' && check?.kind !== 'numberCheck') {
485
+ return failedRequirement({kind: failureKind, site: instruction.site, status: 'unproven'})
486
+ }
487
+ const condition = requiredValue(state, instruction.value)
488
+ if (condition.kind !== 'boolean') {
489
+ return failedRequirement({kind: failureKind, site: instruction.site, status: 'unproven'})
490
+ }
491
+ if (!condition.canBeTrue) {
492
+ return failedRequirement({kind: failureKind, site: instruction.site, status: 'refuted'})
493
+ }
494
+ if (!condition.canBeFalse) return value({kind: 'void'})
495
+ const requirement = staticRequirement(
496
+ check,
497
+ instruction.site,
498
+ context.expressionContext,
499
+ instruction.purpose,
500
+ )
501
+ if (requirement == null) {
502
+ return failedRequirement({kind: failureKind, site: instruction.site, status: 'unproven'})
503
+ }
504
+ const constantStatus = constantRequirementStatus(requirement)
505
+ if (constantStatus === false) {
506
+ return failedRequirement({kind: failureKind, site: instruction.site, status: 'refuted'})
507
+ }
508
+ if (constantStatus === true) return value({kind: 'void'})
509
+ const refined = refineCheck(state, check, true, context.expressionContext)
510
+ if (refined == null) {
511
+ return failedRequirement({kind: failureKind, site: instruction.site, status: 'refuted'})
512
+ }
513
+ state.values = refined.values
514
+ state.shared = refined.shared
515
+ state.valueFacts = refined.valueFacts
516
+ addPrecondition(context.preconditions, requirement)
517
+ return value({kind: 'void'})
518
+ }
519
+ case 'minimum': {
520
+ const operands = instruction.values.map(id => requiredNumberWithFacts(state, id, context.expressionContext))
521
+ return computedNumber(minimumNumbers(operands), operands, instruction.site)
522
+ }
523
+ case 'maximum': {
524
+ const operands = instruction.values.map(id => requiredNumberWithFacts(state, id, context.expressionContext))
525
+ return computedNumber(maximumNumbers(operands), operands, instruction.site)
526
+ }
527
+ case 'call': {
528
+ const callee = context.program.functions[instruction.function]
529
+ if (callee == null) throw new Error(`Unknown function ${instruction.function}`)
530
+ if (callee.kind === 'unsupported') {
531
+ return {kind: 'stop', stop: {site: instruction.site, reason: {kind: 'calleeStopped', callee: instruction.function}}}
532
+ }
533
+ if (context.callStack.includes(instruction.function)) {
534
+ return {kind: 'stop', stop: {site: instruction.site, reason: {kind: 'recursion', callee: instruction.function}}}
535
+ }
536
+ const arguments_ = instruction.arguments.map(id => requiredValue(state, id))
537
+ const argumentExpressions = instruction.arguments.map(id => numericExpression(id, context.expressionContext))
538
+ // Parameters use their caller arguments' identity keys. The same small fact list
539
+ // therefore works inside the callee and comes back after every normal return, so a
540
+ // requirement established by a completed helper call applies below that call.
541
+ const argumentKeys = instruction.arguments.map(id => canonicalValueKey(id, context.expressionContext))
542
+ const calleeNamespace = `${context.expressionContext.identityNamespace}call:${instruction.function}:${instruction.site}/`
543
+ const evaluation = context.evaluateFunction(
544
+ instruction.function,
545
+ arguments_,
546
+ argumentExpressions,
547
+ state.shared,
548
+ context.callStack,
549
+ state.valueFacts,
550
+ argumentKeys,
551
+ calleeNamespace,
552
+ )
553
+ // A partial callee's result is discarded wholesale: the callee ran on a clone, and
554
+ // state.shared is assigned only on the complete path below, so a partial callee's
555
+ // module writes cannot become this caller's state.
556
+ const completed = completedEvaluation(evaluation)
557
+ if (completed == null) {
558
+ // A callee that throws on every path is fully analyzed; the call just never
559
+ // returns, so this path ends exactly like an inline throw — silently. A guarded
560
+ // `if (bad) return fail(x)` then reports the same full contract the throw
561
+ // spelling gets.
562
+ if (evaluation.stops.length === 0 && evaluation.normal == null) {
563
+ for (const precondition of evaluation.preconditions) addPrecondition(context.preconditions, precondition)
564
+ for (const assumption of evaluation.boundsAssumptions) addBoundsAssumption(context.boundsAssumptions, assumption)
565
+ return {kind: 'ends'}
566
+ }
567
+ const requirementFailure = evaluation.stops.find(stop => stop.reason.kind === 'requirementFailure')
568
+ if (requirementFailure?.reason.kind === 'requirementFailure') {
569
+ const reason = requirementFailure.reason
570
+ return {kind: 'stop', stop: {
571
+ site: instruction.site,
572
+ reason: {...reason, callee: instruction.function},
573
+ }}
574
+ }
575
+ return {kind: 'stop', stop: {site: instruction.site, reason: {kind: 'calleeStopped', callee: instruction.function}}}
576
+ }
577
+ state.shared = completed.sharedState
578
+ state.valueFacts = completed.valueFacts.filter(fact =>
579
+ !valueFactUsesNamespace(fact, calleeNamespace))
580
+ for (let index = 0; index < callee.parameters.length; index++) {
581
+ refineFiniteCallArgument(
582
+ state,
583
+ instruction.arguments[index]!,
584
+ callee.parameters[index]!.type,
585
+ context.expressionContext,
586
+ )
587
+ }
588
+ for (const precondition of completed.preconditions) addPrecondition(context.preconditions, precondition)
589
+ for (const assumption of completed.boundsAssumptions) addBoundsAssumption(context.boundsAssumptions, assumption)
590
+ return passthroughValue(completed.returnValue)
591
+ }
592
+ case 'binary': {
593
+ const left = requiredNumberWithFacts(state, instruction.left, context.expressionContext)
594
+ const right = requiredNumberWithFacts(state, instruction.right, context.expressionContext)
595
+ const sameOperand = sameRuntimeValue(
596
+ instruction.left,
597
+ instruction.right,
598
+ context.expressionContext,
599
+ ) ? intersectSameNumbers(left, right) : null
600
+ if (
601
+ (instruction.operator === 'divide' || instruction.operator === 'remainder')
602
+ && isDefinitelyZero(right)
603
+ ) {
604
+ return failedRequirement({
605
+ kind: 'nonzeroDivisor',
606
+ site: instruction.site,
607
+ operation: instruction.operator === 'divide' ? 'division' : 'remainder',
608
+ })
609
+ }
610
+ if (
611
+ (instruction.operator === 'divide' || instruction.operator === 'remainder')
612
+ && includesZero(right)
613
+ ) {
614
+ const operation = instruction.operator === 'divide' ? 'division' : 'remainder'
615
+ const expression = numericExpression(instruction.right, context.expressionContext)
616
+ if (expression == null) {
617
+ // The divisor is not expressible over the caller's arguments (a join, a module
618
+ // read, an element read, a call result, or an exhausted expression walk), so no
619
+ // requirement can be minted. Record the nonzero assumption instead of stopping —
620
+ // the same channel asserted element reads use — and compute below as if it
621
+ // holds: a zero divisor at runtime violates the printed assumes line, making
622
+ // the downstream claims vacuous, exactly like every other assumes line.
623
+ addBoundsAssumption(context.boundsAssumptions, {site: instruction.site, kind: 'nonzeroDivisor'})
624
+ } else {
625
+ addPrecondition(context.preconditions, peelNonzero(expression, instruction.site, operation))
626
+ }
627
+ // Preconditions and assumptions are promises made by the caller. Later uses of
628
+ // this same stored divisor on this path may rely on the promise just recorded.
629
+ recordNonzeroValueFact(state, instruction.right, context.expressionContext)
630
+ writeThroughProducers(
631
+ state,
632
+ instruction.right,
633
+ excludePointFrom(right, constantNumber(0)),
634
+ context.expressionContext.instructionByValue,
635
+ )
636
+ // Ensures assume the requires: with the nonzero requirement recorded, the quotient
637
+ // is computed over the divisor's range with zero cut out. An integer divisor gives
638
+ // a genuinely finite result; a non-integer one can still sit arbitrarily close to
639
+ // zero and stays possibly non-finite. The remainder is bounded by both operands.
640
+ return computedNumber(
641
+ sameOperand == null
642
+ ? instruction.operator === 'divide'
643
+ ? divideNumbersNonzeroDivisor(left, right)
644
+ : remainderNumbers(left, right, true)
645
+ : evaluateSameOperandBinary(instruction.operator, sameOperand),
646
+ [left, right],
647
+ instruction.site,
648
+ )
649
+ }
650
+ return computedNumber(
651
+ sameOperand == null
652
+ ? evaluateBinary(instruction.operator, left, right)
653
+ : evaluateSameOperandBinary(instruction.operator, sameOperand),
654
+ [left, right],
655
+ instruction.site,
656
+ )
657
+ }
658
+ }
659
+ }
660
+
661
+ export function addBoundsAssumption(assumptions: BoundsAssumption[], candidate: BoundsAssumption): void {
662
+ if (!assumptions.some(assumption => assumption.site === candidate.site && assumption.kind === candidate.kind)) {
663
+ assumptions.push(candidate)
664
+ }
665
+ }
666
+
667
+ function valueFactUsesNamespace(fact: ValueFact, namespace: string): boolean {
668
+ const marker = `v:${namespace}`
669
+ if (fact.kind === 'nonzero') return fact.value.includes(marker)
670
+ return fact.index.includes(marker) || fact.array.includes(marker)
671
+ }
672
+
673
+ function sentinelsAdmit(sentinels: 'null' | 'undefined' | 'both', sentinel: 'null' | 'undefined'): boolean {
674
+ return sentinels === 'both' || sentinels === sentinel
675
+ }
676
+
677
+ function withoutSentinel(sentinels: 'null' | 'undefined' | 'both', sentinel: 'null' | 'undefined'): 'null' | 'undefined' | null {
678
+ if (sentinels === 'both') return sentinel === 'null' ? 'undefined' : 'null'
679
+ return sentinels === sentinel ? null : sentinels
680
+ }
681
+
682
+ // Narrows the checked value along one branch of `x === null` and friends, and writes the
683
+ // narrowed value back through the producer chain: when the checked value is a property
684
+ // read, the parent record's property is replaced too (sound because values are immutable
685
+ // — the property cannot differ between this read and the next), so `if (point.x !== null)
686
+ // return point.x + 1` narrows both reads. Returns null when the branch is impossible
687
+ // (e.g. the value cannot be the checked sentinel).
688
+ function requiredTaggedUnion(state: ExecutionState, id: ValueID): AbstractTaggedUnion {
689
+ const operand = requiredValue(state, id)
690
+ if (operand.kind !== 'taggedUnion') throw new KindMismatch(`IR value ${id} is not a tagged union`, id)
691
+ return operand
692
+ }
693
+
694
+ // The branch where route.type === 'lightbox' held keeps only the matching variants; the
695
+ // other branch keeps the rest. A side with no variants left is impossible and prunes.
696
+ // Written through the producer chain like every refinement, so the union binding itself
697
+ // narrows, not just the read.
698
+ function refineTagCheck(
699
+ state: ExecutionState,
700
+ check: Extract<InstructionIR, {kind: 'tagCheck'}>,
701
+ truth: boolean,
702
+ producers: Array<InstructionIR | undefined>,
703
+ ): ExecutionState | null {
704
+ const result = cloneState(state)
705
+ // A record operand has nothing to refine (its tag value was never learned); both
706
+ // branches keep the state, mirroring the unknown-boolean evaluation above.
707
+ if (requiredValue(result, check.union).kind === 'record') return result
708
+ const union = requiredTaggedUnion(result, check.union)
709
+ const wantMatch = truth !== check.negated
710
+ const [firstKept, ...restKept] = union.variants.filter(variant => (variant.tagValue === check.tagValue) === wantMatch)
711
+ if (firstKept == null) return null
712
+ writeThroughProducers(result, check.union, {kind: 'taggedUnion', tagProperty: union.tagProperty, variants: [firstKept, ...restKept]}, producers)
713
+ return result
714
+ }
715
+
716
+ function refineNullishCheck(
717
+ state: ExecutionState,
718
+ check: Extract<InstructionIR, {kind: 'nullishCheck'}>,
719
+ truth: boolean,
720
+ producers: Array<InstructionIR | undefined>,
721
+ ): ExecutionState | null {
722
+ const result = cloneState(state)
723
+ const operand = requiredValue(result, check.value)
724
+ const isSentinel = truth !== check.negated
725
+ const refined = refineForSentinel(operand, check.sentinel, isSentinel)
726
+ if (refined == null) return null
727
+ writeThroughProducers(result, check.value, refined, producers)
728
+ return result
729
+ }
730
+
731
+ function refineForSentinel(
732
+ operand: AbstractValue,
733
+ sentinel: 'null' | 'undefined' | 'nullish',
734
+ isSentinel: boolean,
735
+ ): AbstractValue | null {
736
+ if (isSentinel) {
737
+ // This branch requires the value to BE the sentinel.
738
+ if (operand.kind === 'nullish') {
739
+ if (sentinel === 'nullish') return operand
740
+ return sentinelsAdmit(operand.sentinels, sentinel) ? {kind: 'nullish', sentinels: sentinel} : null
741
+ }
742
+ if (operand.kind === 'maybeNullish') {
743
+ // An opaque inner can be either sentinel at runtime, beyond what the wrapper's
744
+ // sentinels set lists — the branch stays live and widens to both.
745
+ const opaqueInner = operand.inner.kind === 'opaque'
746
+ if (sentinel === 'nullish') {
747
+ return {kind: 'nullish', sentinels: opaqueInner ? 'both' : operand.sentinels}
748
+ }
749
+ return sentinelsAdmit(operand.sentinels, sentinel) || opaqueInner
750
+ ? {kind: 'nullish', sentinels: sentinel}
751
+ : null
752
+ }
753
+ // A bare opaque may be the sentinel too; this branch pins it down to exactly that.
754
+ if (operand.kind === 'opaque') {
755
+ return {kind: 'nullish', sentinels: sentinel === 'nullish' ? 'both' : sentinel}
756
+ }
757
+ // A value that is never missing cannot take this branch.
758
+ return null
759
+ }
760
+ // This branch requires the value NOT to be the sentinel.
761
+ if (operand.kind === 'nullish') {
762
+ if (sentinel === 'nullish') return null
763
+ const remaining = withoutSentinel(operand.sentinels, sentinel)
764
+ return remaining == null ? null : {kind: 'nullish', sentinels: remaining}
765
+ }
766
+ if (operand.kind === 'maybeNullish') {
767
+ if (sentinel === 'nullish') return operand.inner
768
+ const remaining = withoutSentinel(operand.sentinels, sentinel)
769
+ return remaining == null ? operand.inner : {kind: 'maybeNullish', inner: operand.inner, sentinels: remaining}
770
+ }
771
+ return operand
772
+ }
773
+
774
+ // frame[id] := refined, then rebuild the enclosing value when id was produced by a
775
+ // structural read, recursively — later reads see the narrowed value. Property reads
776
+ // rebuild the record (and chase through a freshly built record into the value that went
777
+ // in: with `const copy = {columns: grid.columns}`, narrowing copy.columns narrows
778
+ // grid.columns, since the copy's property IS the stored grid.columns read); length reads
779
+ // rebuild the array's length interval. Every write MEETS the destination's current value
780
+ // — a refinement of a stale read must not widen a fresher narrowing already sitting in
781
+ // the record.
782
+ function writeThroughProducers(
783
+ state: ExecutionState,
784
+ id: ValueID,
785
+ refined: AbstractValue,
786
+ producers: Array<InstructionIR | undefined>,
787
+ ): void {
788
+ const current = state.values[id]
789
+ const met = current == null ? refined : meetValues(current, refined)
790
+ state.values[id] = met
791
+ const producer = producers[id]
792
+ if (producer?.kind === 'property') {
793
+ const parent = state.values[producer.object]
794
+ if (parent?.kind === 'record') {
795
+ const rebuilt: AbstractValue = {
796
+ kind: 'record',
797
+ properties: parent.properties.map(property =>
798
+ property.name === producer.property
799
+ ? {name: property.name, value: meetValues(property.value, met)}
800
+ : property),
801
+ }
802
+ writeThroughProducers(state, producer.object, rebuilt, producers)
803
+ }
804
+ // A property read through a tagged union (box.owner after a tag check on box, or a
805
+ // shared property before one): the refinement meets into every variant that carries
806
+ // the property, so the narrowing sticks on the union binding, not just this read.
807
+ if (parent?.kind === 'taggedUnion') {
808
+ const rebuildVariant = (variant: TaggedVariant): TaggedVariant => {
809
+ const existing = recordProperty(variant.record, producer.property)
810
+ if (existing == null) return variant
811
+ return {
812
+ tagValue: variant.tagValue,
813
+ record: {
814
+ kind: 'record',
815
+ properties: variant.record.properties.map(property =>
816
+ property.name === producer.property
817
+ ? {name: property.name, value: meetValues(property.value, met)}
818
+ : property),
819
+ },
820
+ }
821
+ }
822
+ const [firstVariant, ...restVariants] = parent.variants
823
+ const rebuilt: AbstractValue = {
824
+ kind: 'taggedUnion',
825
+ tagProperty: parent.tagProperty,
826
+ variants: [rebuildVariant(firstVariant), ...restVariants.map(rebuildVariant)],
827
+ }
828
+ writeThroughProducers(state, producer.object, rebuilt, producers)
829
+ }
830
+ // A read through a freshly built record narrows the value that went in.
831
+ const parentProducer = producers[producer.object]
832
+ if (parentProducer?.kind === 'object') {
833
+ const source = parentProducer.properties.find(property => property.name === producer.property)
834
+ if (source != null) writeThroughProducers(state, source.value, met, producers)
835
+ }
836
+ return
837
+ }
838
+ if (producer?.kind === 'arrayLength' && met.kind === 'number') {
839
+ const parent = state.values[producer.array]
840
+ if (parent?.kind !== 'array') return
841
+ const length = meetValues(parent.length, met)
842
+ if (length.kind !== 'number') return
843
+ writeThroughProducers(state, producer.array, {kind: 'array', element: parent.element, length}, producers)
844
+ }
845
+ }
846
+
847
+ function refineFiniteCallArgument(
848
+ state: ExecutionState,
849
+ value: ValueID,
850
+ declared: DeclaredKind,
851
+ expressionContext: ExpressionContext,
852
+ ): void {
853
+ const current = requiredValue(state, value)
854
+ const refined = refineFiniteValue(current, declared)
855
+ if (refined == null) return
856
+ if (refined !== current) {
857
+ writeThroughProducers(state, value, refined, expressionContext.instructionByValue)
858
+ }
859
+ if (declared.kind !== 'record') return
860
+ const producer = expressionContext.instructionByValue[resolveStoredValue(value, expressionContext)]
861
+ if (producer?.kind !== 'object') return
862
+ const declaredProperties = new Map(declared.properties.map(property => [property.name, property.declared]))
863
+ for (const field of producer.properties) {
864
+ const fieldKind = declaredProperties.get(field.name)
865
+ if (fieldKind != null) refineFiniteCallArgument(state, field.value, fieldKind, expressionContext)
866
+ }
867
+ }
868
+
869
+ function refineFiniteValue(value: AbstractValue, declared: DeclaredKind): AbstractValue | null {
870
+ if (declared.kind === 'number') {
871
+ if (declared.interval != null) return value
872
+ if (value.kind !== 'number') return null
873
+ return !value.mayBeNaN && isFiniteNumber(value) ? value : finiteNumberPart(value)
874
+ }
875
+ if (declared.kind !== 'record') return value
876
+ if (value.kind === 'record') return refineFiniteRecord(value, declared)
877
+ if (value.kind !== 'taggedUnion') return null
878
+ let changed = false
879
+ const variants: TaggedVariant[] = []
880
+ for (const variant of value.variants) {
881
+ const record = refineFiniteRecord(variant.record, declared)
882
+ if (record == null) return null
883
+ changed ||= record !== variant.record
884
+ variants.push(record === variant.record ? variant : {...variant, record})
885
+ }
886
+ if (!changed) return value
887
+ const [first, ...rest] = variants
888
+ return {...value, variants: [first!, ...rest]}
889
+ }
890
+
891
+ function refineFiniteRecord(value: AbstractRecord, declared: Extract<DeclaredKind, {kind: 'record'}>): AbstractRecord | null {
892
+ const declaredProperties = new Map(declared.properties.map(property => [property.name, property.declared]))
893
+ let changed = false
894
+ const properties: AbstractRecord['properties'] = []
895
+ for (const property of value.properties) {
896
+ const fieldKind = declaredProperties.get(property.name)
897
+ if (fieldKind == null) {
898
+ properties.push(property)
899
+ continue
900
+ }
901
+ const refined = refineFiniteValue(property.value, fieldKind)
902
+ if (refined == null) return null
903
+ changed ||= refined !== property.value
904
+ properties.push(refined === property.value ? property : {...property, value: refined})
905
+ }
906
+ return changed ? {kind: 'record', properties} : value
907
+ }
908
+
909
+ // The intersection of two covers of the same runtime value — both are supersets of the
910
+ // truth, so keeping the tighter fact per dimension is sound. Numbers intersect bounds;
911
+ // records, arrays, and tuples meet their structure pointwise (a rebuilt array must not
912
+ // clobber a fresher length narrowing already on the destination); anything else keeps the
913
+ // refined side.
914
+ function meetValues(current: AbstractValue, refined: AbstractValue): AbstractValue {
915
+ if (current === refined) return current
916
+ // An exhaustive switch on the refined side, like widenValue: every kind states its
917
+ // meet behavior, so a future kind cannot silently discard the current side's facts.
918
+ switch (refined.kind) {
919
+ case 'number': {
920
+ if (current.kind !== 'number') return refined
921
+ let met = normalizeRefinedNumber({
922
+ kind: 'number',
923
+ lower: Math.max(current.lower, refined.lower),
924
+ upper: Math.min(current.upper, refined.upper),
925
+ integer: current.integer || refined.integer,
926
+ mayBeNaN: current.mayBeNaN && refined.mayBeNaN,
927
+ })
928
+ // An intersection keeps every fact either cover proved; only one point fits the
929
+ // field, and the refined side's is the fresher fact.
930
+ const excludedPoint = refined.excludesPoint ?? current.excludesPoint
931
+ if (excludedPoint != null) met = normalizeRefinedNumber({...met, excludesPoint: excludedPoint})
932
+ if (!isFiniteNumber(met)) {
933
+ const nonFiniteSite = refined.nonFiniteSite ?? current.nonFiniteSite
934
+ if (nonFiniteSite != null) met.nonFiniteSite = nonFiniteSite
935
+ }
936
+ if (met.mayBeNaN) {
937
+ const nanSite = refined.nanSite ?? current.nanSite
938
+ if (nanSite != null) met.nanSite = nanSite
939
+ }
940
+ return met
941
+ }
942
+ case 'record': {
943
+ if (current.kind !== 'record') return refined
944
+ const currentProperties = recordPropertiesByName(current)
945
+ return {
946
+ kind: 'record',
947
+ properties: refined.properties.map(property => {
948
+ const existing = currentProperties.get(property.name)
949
+ return existing == null ? property : {name: property.name, value: meetValues(existing, property.value)}
950
+ }),
951
+ }
952
+ }
953
+ case 'array': {
954
+ if (current.kind !== 'array') return refined
955
+ const length = meetValues(current.length, refined.length)
956
+ const element = current.element == null ? refined.element
957
+ : refined.element == null ? current.element
958
+ : meetValues(current.element, refined.element)
959
+ return {kind: 'array', element, length: length.kind === 'number' ? length : refined.length}
960
+ }
961
+ case 'tuple': {
962
+ if (current.kind !== 'tuple' || current.elements.length !== refined.elements.length) return refined
963
+ return {kind: 'tuple', elements: refined.elements.map((element, index) => meetValues(current.elements[index]!, element))}
964
+ }
965
+ // No pointwise structure to intersect (or, for taggedUnion, none the refinements
966
+ // produce today): the refined side is the fresher cover of the same runtime value.
967
+ case 'boolean':
968
+ case 'void':
969
+ case 'nullish':
970
+ case 'maybeNullish':
971
+ case 'opaque':
972
+ case 'taggedUnion':
973
+ return refined
974
+ }
975
+ }
976
+
977
+ function evaluateNumberCheck(predicate: 'integer' | 'finite' | 'nan', operand: AbstractNumber): AbstractBoolean {
978
+ const finite = finiteNumberPart(operand)
979
+ if (predicate === 'nan') {
980
+ // The domain cannot express "always NaN", so the false side stays possible.
981
+ return {kind: 'boolean', canBeTrue: operand.mayBeNaN, canBeFalse: true}
982
+ }
983
+ if (predicate === 'finite') {
984
+ return {
985
+ kind: 'boolean',
986
+ // True is possible when a finite inhabitant exists; false when NaN or an infinity can.
987
+ canBeTrue: finite != null,
988
+ canBeFalse: operand.mayBeNaN || !isFiniteNumber(operand),
989
+ }
990
+ }
991
+ return {
992
+ kind: 'boolean',
993
+ // Number.isInteger is false for NaN and the infinities, so the true side needs a
994
+ // finite integer inhabitant and the false side anything else.
995
+ canBeTrue: finite != null && Math.ceil(finite.lower) <= Math.floor(finite.upper),
996
+ canBeFalse: !operand.integer || operand.mayBeNaN || !isFiniteNumber(operand),
997
+ }
998
+ }
999
+
1000
+ function refineNumberCheck(
1001
+ state: ExecutionState,
1002
+ check: Extract<InstructionIR, {kind: 'numberCheck'}>,
1003
+ truth: boolean,
1004
+ producers: Array<InstructionIR | undefined>,
1005
+ ): ExecutionState | null {
1006
+ const result = cloneState(state)
1007
+ const operand = requiredNumber(result, check.value)
1008
+ if (check.predicate === 'nan') {
1009
+ // The passing branch holds exactly NaN — not representable as a refinement, so the
1010
+ // unrefined operand stays as its sound cover, and the branch prunes outright when the
1011
+ // value provably cannot be NaN. The failing branch launders: mayBeNaN clears.
1012
+ if (truth) return operand.mayBeNaN ? result : null
1013
+ const laundered: AbstractNumber = {...operand, mayBeNaN: false}
1014
+ writeThroughProducers(result, check.value, laundered, producers)
1015
+ return result
1016
+ }
1017
+ if (truth) {
1018
+ // The passing branch proves finiteness for both predicates (isInteger rejects the
1019
+ // infinities too), and integrality snaps the bounds inward.
1020
+ let refined = finiteNumberPart(operand)
1021
+ if (refined == null) return null
1022
+ if (check.predicate === 'integer') {
1023
+ refined = {...refined, integer: true, lower: Math.ceil(refined.lower), upper: Math.floor(refined.upper)}
1024
+ }
1025
+ if (refined.lower > refined.upper) return null
1026
+ writeThroughProducers(result, check.value, refined, producers)
1027
+ return result
1028
+ }
1029
+ // The failing branch holds NaN, the infinities, and (for isInteger) every non-integer —
1030
+ // none of which an interval can carve out, except the one provable contradiction: a
1031
+ // value already finite and NaN-free cannot fail isFinite at all.
1032
+ if (check.predicate === 'finite' && !operand.mayBeNaN && isFiniteNumber(operand)) return null
1033
+ return result
1034
+ }
1035
+
1036
+ function refineComparison(
1037
+ state: ExecutionState,
1038
+ comparison: Extract<InstructionIR, {kind: 'compare'}>,
1039
+ truth: boolean,
1040
+ expressionContext: ExpressionContext,
1041
+ ): ExecutionState | null {
1042
+ const producers = expressionContext.instructionByValue
1043
+ const result = cloneState(state)
1044
+ // Boolean equality refines exactly over the two-point lattice: in the branch where
1045
+ // flag === true held, flag IS true. A contradiction (both sides known, and the branch
1046
+ // demands they differ from what they are) prunes.
1047
+ const leftOperand = requiredValue(result, comparison.left)
1048
+ const rightOperand = requiredValue(result, comparison.right)
1049
+ if (leftOperand.kind === 'boolean' && rightOperand.kind === 'boolean') {
1050
+ const equalHolds = truth === (comparison.operator === 'equal')
1051
+ const known = (side: AbstractBoolean): boolean | null =>
1052
+ side.canBeTrue === side.canBeFalse ? null : side.canBeTrue
1053
+ const refineTo = (id: ValueID, mustBe: boolean): boolean => {
1054
+ const current = requiredBoolean(result, id)
1055
+ if (mustBe ? !current.canBeTrue : !current.canBeFalse) return false
1056
+ writeThroughProducers(result, id, {kind: 'boolean', canBeTrue: mustBe, canBeFalse: !mustBe}, producers)
1057
+ return true
1058
+ }
1059
+ const leftKnown = known(leftOperand)
1060
+ const rightKnown = known(rightOperand)
1061
+ if (rightKnown != null && !refineTo(comparison.left, equalHolds ? rightKnown : !rightKnown)) return null
1062
+ if (leftKnown != null && !refineTo(comparison.right, equalHolds ? leftKnown : !leftKnown)) return null
1063
+ return result
1064
+ }
1065
+ const left = requiredNumberWithFacts(result, comparison.left, expressionContext)
1066
+ const right = requiredNumberWithFacts(result, comparison.right, expressionContext)
1067
+ const operator = truth ? comparison.operator : invertedComparison(comparison.operator)
1068
+ // The branch where the written condition did not hold is also where a NaN operand lands,
1069
+ // with the OTHER operand unconstrained. Inverting an ordered comparison and refining
1070
+ // bounds is only sound when neither operand can be NaN; e.g. with a possibly-NaN clamp
1071
+ // result as the right operand, `if (x < clamped) ... else return x` reaches the else
1072
+ // with any x at all whenever clamped is NaN. The not-equal refinement is exempt: it only
1073
+ // cuts interval points, which never rules out a NaN inhabitant, and NaN lands on the
1074
+ // not-equal side anyway (NaN !== c is true).
1075
+ if (!truth && operator !== 'equal' && (left.mayBeNaN || right.mayBeNaN)) return result
1076
+ let refinedLeft = left
1077
+ let refinedRight = right
1078
+ switch (operator) {
1079
+ case 'lessThan':
1080
+ refinedLeft = withBounds(left, left.lower, strictUpper(right.upper, left.integer))
1081
+ refinedRight = withBounds(right, strictLower(left.lower, right.integer), right.upper)
1082
+ break
1083
+ case 'lessThanOrEqual':
1084
+ refinedLeft = withBounds(left, left.lower, Math.min(left.upper, right.upper))
1085
+ refinedRight = withBounds(right, Math.max(right.lower, left.lower), right.upper)
1086
+ break
1087
+ case 'greaterThan':
1088
+ refinedLeft = withBounds(left, strictLower(right.lower, left.integer), left.upper)
1089
+ refinedRight = withBounds(right, right.lower, strictUpper(left.upper, right.integer))
1090
+ break
1091
+ case 'greaterThanOrEqual':
1092
+ refinedLeft = withBounds(left, Math.max(left.lower, right.lower), left.upper)
1093
+ refinedRight = withBounds(right, right.lower, Math.min(right.upper, left.upper))
1094
+ break
1095
+ case 'equal': {
1096
+ const intersection = intersectSameNumbers(left, right)
1097
+ refinedLeft = intersection
1098
+ refinedRight = intersection
1099
+ break
1100
+ }
1101
+ case 'notEqual': {
1102
+ refinedLeft = excludePointFrom(left, right)
1103
+ refinedRight = excludePointFrom(right, left)
1104
+ break
1105
+ }
1106
+ }
1107
+ // The bounds-check idiom: in the branch where `i < arr.length` held, record the pair —
1108
+ // the below-length half of in-bounds that no interval can carry (a relation between two
1109
+ // unknowns). The `i > arr.length`-failed spelling arrives here as the inverted
1110
+ // lessThanOrEqual and is deliberately NOT recorded: <= length is one past the last
1111
+ // element. Only the strict form proves a valid index.
1112
+ if (operator === 'lessThan') {
1113
+ const rightProducer = producers[comparison.right]
1114
+ if (rightProducer?.kind === 'arrayLength') {
1115
+ addValueFact(result.valueFacts, {
1116
+ kind: 'belowLength',
1117
+ index: canonicalValueKey(comparison.left, expressionContext),
1118
+ array: canonicalValueKey(rightProducer.array, expressionContext),
1119
+ })
1120
+ }
1121
+ }
1122
+ if (operator === 'greaterThan') {
1123
+ const leftProducer = producers[comparison.left]
1124
+ if (leftProducer?.kind === 'arrayLength') {
1125
+ addValueFact(result.valueFacts, {
1126
+ kind: 'belowLength',
1127
+ index: canonicalValueKey(comparison.right, expressionContext),
1128
+ array: canonicalValueKey(leftProducer.array, expressionContext),
1129
+ })
1130
+ }
1131
+ }
1132
+ const emptied = refinedLeft.lower > refinedLeft.upper || refinedRight.lower > refinedRight.upper
1133
+ // NaN fails every ordered comparison and ===, so it never reaches the branch where that
1134
+ // condition held, and it always reaches the branch where it failed. Not-equal is the
1135
+ // mirror image: NaN !== c is true, so NaN lands on the not-equal side and mayBeNaN must
1136
+ // survive there.
1137
+ const holdsForNaN = operator === 'notEqual'
1138
+ if (emptied) {
1139
+ // The interval refinement only rules out the non-NaN inhabitants; a NaN operand still
1140
+ // lands on the side its comparison semantics allow (e.g. `x > -1 ? 1 : 0` with x
1141
+ // possibly NaN takes the 0 arm at runtime, and `x !== x` style emptiness keeps the
1142
+ // NaN inhabitant on the not-equal side). Keep the unrefined values — a superset —
1143
+ // rather than pruning the branch.
1144
+ if ((!truth || holdsForNaN) && (left.mayBeNaN || right.mayBeNaN)) return cloneState(state)
1145
+ return null
1146
+ }
1147
+ if (truth && !holdsForNaN) {
1148
+ refinedLeft = {...refinedLeft, mayBeNaN: false}
1149
+ refinedRight = {...refinedRight, mayBeNaN: false}
1150
+ }
1151
+ // Through the producer chain, like the null-check refinement: narrowing an
1152
+ // arrayLength's result rebuilds the array value with the narrowed length (so
1153
+ // `if (values.length > 0) values[0]!` proves the read), and narrowing a property read
1154
+ // rebuilds the record.
1155
+ writeThroughProducers(result, comparison.left, refinedLeft, producers)
1156
+ writeThroughProducers(result, comparison.right, refinedRight, producers)
1157
+ recordNonzeroComparisonFacts(result, comparison, expressionContext)
1158
+ return result
1159
+ }
1160
+
1161
+ // The check instructions whose branch outcomes refine the state. The list is the single
1162
+ // source of truth: the branch terminator asks membership here, and refineCheck's switch
1163
+ // is exhaustive over exactly these kinds — a new check kind added to the list gets a
1164
+ // compile error until refineCheck says how it refines.
1165
+ const refinableCheckKinds = ['compare', 'nullishCheck', 'numberCheck', 'tagCheck'] as const
1166
+ export type RefinableCheck = Extract<InstructionIR, {kind: (typeof refinableCheckKinds)[number]}>
1167
+
1168
+ export function asRefinableCheck(instruction: InstructionIR | undefined): RefinableCheck | undefined {
1169
+ if (instruction == null) return undefined
1170
+ return (refinableCheckKinds as readonly string[]).includes(instruction.kind)
1171
+ ? instruction as RefinableCheck
1172
+ : undefined
1173
+ }
1174
+
1175
+ // One dispatch for both branch arms; each refine function clones internally. Returns null
1176
+ // when the refinement proves the branch cannot be taken.
1177
+ export function refineCheck(
1178
+ state: ExecutionState,
1179
+ check: RefinableCheck,
1180
+ truth: boolean,
1181
+ expressionContext: ExpressionContext,
1182
+ ): ExecutionState | null {
1183
+ switch (check.kind) {
1184
+ case 'compare': return refineComparison(state, check, truth, expressionContext)
1185
+ case 'nullishCheck': return refineNullishCheck(state, check, truth, expressionContext.instructionByValue)
1186
+ case 'numberCheck': return refineNumberCheck(state, check, truth, expressionContext.instructionByValue)
1187
+ case 'tagCheck': return refineTagCheck(state, check, truth, expressionContext.instructionByValue)
1188
+ }
1189
+ }
1190
+
1191
+ function requiredNumber(state: ExecutionState, id: ValueID): AbstractNumber {
1192
+ const value = requiredValue(state, id)
1193
+ if (value.kind !== 'number') throw new KindMismatch(`IR value ${id} is not a number`, id)
1194
+ return value
1195
+ }
1196
+
1197
+ function requiredNumberWithFacts(
1198
+ state: ExecutionState,
1199
+ id: ValueID,
1200
+ expressionContext: ExpressionContext,
1201
+ ): AbstractNumber {
1202
+ const result = numberWithFacts(state, id, expressionContext)
1203
+ if (result == null) throw new KindMismatch(`IR value ${id} is not a number`, id)
1204
+ return result
1205
+ }
1206
+
1207
+ function numberWithFacts(
1208
+ state: ExecutionState,
1209
+ id: ValueID,
1210
+ expressionContext: ExpressionContext,
1211
+ ): AbstractNumber | null {
1212
+ const held = state.values[id]
1213
+ if (held?.kind !== 'number') return null
1214
+ let result = held
1215
+ const key = canonicalValueKey(id, expressionContext)
1216
+ if (state.valueFacts.some(fact => fact.kind === 'validIndex' && fact.index === key)) {
1217
+ result = validIndexNumber(result)
1218
+ }
1219
+ if (hasNonzeroFact(state.valueFacts, key)) result = excludePointFrom(result, constantNumber(0))
1220
+ return result
1221
+ }
1222
+
1223
+ function validIndexNumber(value: AbstractNumber): AbstractNumber {
1224
+ return {
1225
+ ...value, integer: true, mayBeNaN: false,
1226
+ lower: Math.ceil(Math.max(value.lower, 0)),
1227
+ upper: Math.floor(value.upper),
1228
+ }
1229
+ }
1230
+
1231
+ function recordNonzeroComparisonFacts(
1232
+ state: ExecutionState,
1233
+ check: Extract<InstructionIR, {kind: 'compare'}>,
1234
+ expressionContext: ExpressionContext,
1235
+ ): void {
1236
+ for (const id of [check.left, check.right]) {
1237
+ if (expressionContext.instructionByValue[id]?.kind === 'constant') continue
1238
+ const held = requiredValue(state, id)
1239
+ if (held.kind === 'number' && !includesZero(held)) recordNonzeroValueFact(state, id, expressionContext)
1240
+ }
1241
+ }
1242
+
1243
+ function recordNonzeroValueFact(
1244
+ state: ExecutionState,
1245
+ value: ValueID,
1246
+ expressionContext: ExpressionContext,
1247
+ ): void {
1248
+ addValueFact(state.valueFacts, {kind: 'nonzero', value: canonicalValueKey(value, expressionContext)})
1249
+ }
1250
+
1251
+ export function requiredBoolean(state: ExecutionState, id: ValueID): AbstractBoolean {
1252
+ const value = requiredValue(state, id)
1253
+ if (value.kind !== 'boolean') throw new KindMismatch(`IR value ${id} is not a boolean`, id)
1254
+ return value
1255
+ }
1256
+
1257
+ // The branch terminator's condition read, with the same KindMismatch-to-stop conversion
1258
+ // evaluateInstruction gives instruction operands. The static type of a condition can lie
1259
+ // about the value's kind — `if (setting as boolean)` passes the boolean-condition gate on
1260
+ // the checker's word while the erased cast's value is opaque — and the terminator sits
1261
+ // outside evaluateInstruction's catch, so without this wrapper the mismatch that every
1262
+ // other opaque use converts to an honest stop escaped as a crash (a review round ran it).
1263
+ export function branchConditionOutcome(
1264
+ state: ExecutionState,
1265
+ id: ValueID,
1266
+ site: SiteID,
1267
+ expressionContext: ExpressionContext,
1268
+ ): {kind: 'value'; value: AbstractBoolean} | {kind: 'stop'; stop: Stop} {
1269
+ try {
1270
+ return {kind: 'value', value: requiredBoolean(state, id)}
1271
+ } catch (error) {
1272
+ if (error instanceof KindMismatch) {
1273
+ const missingElementSite = possiblyMissingElementReadSite(
1274
+ state,
1275
+ id,
1276
+ expressionContext.instructionByValue,
1277
+ )
1278
+ if (missingElementSite != null) {
1279
+ return {kind: 'stop', stop: {site: missingElementSite, reason: {kind: 'possiblyMissingElement'}}}
1280
+ }
1281
+ return {kind: 'stop', stop: {site, reason: {kind: 'kindMismatch'}}}
1282
+ }
1283
+ throw error
1284
+ }
1285
+ }
1286
+
1287
+ function possiblyMissingElementReadSite(
1288
+ state: ExecutionState,
1289
+ valueID: ValueID,
1290
+ producers: Array<InstructionIR | undefined>,
1291
+ ): SiteID | null {
1292
+ const producer = producers[valueID]
1293
+ if (producer?.kind !== 'arrayIndex' || producer.mode !== 'bareUnchecked') return null
1294
+ const value = state.values[valueID]
1295
+ if (value == null) return null
1296
+ const canBeUndefined = (value.kind === 'nullish' || value.kind === 'maybeNullish')
1297
+ && value.sentinels !== 'null'
1298
+ return canBeUndefined ? producer.site : null
1299
+ }
1300
+
1301
+ // A constant in-bounds index picks the exact tuple element; anything else takes the hull.
1302
+ // Returns null only for the empty tuple.
1303
+ function tupleElement(tuple: Extract<AbstractValue, {kind: 'tuple'}>, index: AbstractNumber): AbstractValue | null {
1304
+ if (tuple.elements.length === 0) return null
1305
+ if (index.integer && !index.mayBeNaN && index.lower === index.upper) {
1306
+ const exact = tuple.elements[index.lower]
1307
+ if (exact != null) return exact
1308
+ }
1309
+ return tuple.elements.reduce((joined, next) => joinValues(joined, next))
1310
+ }
1311
+
1312
+ function requiredSequence(state: ExecutionState, id: ValueID): Extract<AbstractValue, {kind: 'tuple' | 'array'}> {
1313
+ const value = requiredValue(state, id)
1314
+ if (value.kind !== 'tuple' && value.kind !== 'array') throw new KindMismatch(`IR value ${id} is not an array`, id)
1315
+ return value
1316
+ }
1317
+
1318
+ function requiredRecord(state: ExecutionState, id: ValueID): AbstractRecord {
1319
+ const value = requiredValue(state, id)
1320
+ if (value.kind !== 'record') throw new KindMismatch(`IR value ${id} is not a record`, id)
1321
+ return value
1322
+ }
1323
+
1324
+ export function requiredValue(state: ExecutionState, id: ValueID): AbstractValue {
1325
+ const value = state.values[id]
1326
+ if (value == null) throw new Error(`Missing IR value ${id}`)
1327
+ return value
1328
+ }
1329
+
1330
+ function evaluateBinary(
1331
+ operator: Extract<InstructionIR, {kind: 'binary'}>['operator'],
1332
+ left: AbstractNumber,
1333
+ right: AbstractNumber,
1334
+ ): AbstractNumber {
1335
+ switch (operator) {
1336
+ case 'add': return addNumbers(left, right)
1337
+ case 'subtract': return subtractNumbers(left, right)
1338
+ case 'multiply': return multiplyNumbers(left, right)
1339
+ case 'divide': return divideNumbers(left, right)
1340
+ case 'remainder': return remainderNumbers(left, right, false)
1341
+ }
1342
+ }
1343
+
1344
+ function intersectSameNumbers(left: AbstractNumber, right: AbstractNumber): AbstractNumber {
1345
+ const met = meetValues(left, right)
1346
+ if (met.kind !== 'number') throw new Error('Meeting two numbers produced a non-number')
1347
+ return met
1348
+ }
1349
+
1350
+ // These are JavaScript operations on one already-evaluated value, not algebraic rewrites
1351
+ // of two expressions that happen to look alike. In particular, x + x cannot combine
1352
+ // opposite infinities, while x - x can still be NaN when x is infinite.
1353
+ function evaluateSameOperandBinary(
1354
+ operator: Extract<InstructionIR, {kind: 'binary'}>['operator'],
1355
+ operand: AbstractNumber,
1356
+ ): AbstractNumber {
1357
+ switch (operator) {
1358
+ case 'add': {
1359
+ const doubled = addNumbers(operand, operand)
1360
+ return {...doubled, mayBeNaN: operand.mayBeNaN}
1361
+ }
1362
+ case 'subtract': return {
1363
+ kind: 'number',
1364
+ lower: 0,
1365
+ upper: 0,
1366
+ integer: true,
1367
+ mayBeNaN: operand.mayBeNaN || !isFiniteNumber(operand),
1368
+ }
1369
+ case 'multiply': {
1370
+ const lowerSquare = operand.lower * operand.lower
1371
+ const upperSquare = operand.upper * operand.upper
1372
+ const crossesZero = operand.lower <= 0 && operand.upper >= 0
1373
+ return {
1374
+ kind: 'number',
1375
+ lower: crossesZero
1376
+ ? operand.integer && operand.excludesPoint === 0 ? 1 : 0
1377
+ : Math.min(lowerSquare, upperSquare),
1378
+ upper: Math.max(lowerSquare, upperSquare),
1379
+ integer: operand.integer,
1380
+ mayBeNaN: operand.mayBeNaN,
1381
+ }
1382
+ }
1383
+ case 'divide': return {
1384
+ kind: 'number',
1385
+ lower: 1,
1386
+ upper: 1,
1387
+ integer: true,
1388
+ mayBeNaN: operand.mayBeNaN || !isFiniteNumber(operand),
1389
+ }
1390
+ case 'remainder': return {
1391
+ kind: 'number',
1392
+ lower: 0,
1393
+ upper: 0,
1394
+ integer: true,
1395
+ mayBeNaN: operand.mayBeNaN || !isFiniteNumber(operand),
1396
+ }
1397
+ }
1398
+ }
1399
+
1400
+ // Assertion-only comparison proofs walk the immutable producer graph. Producer edges
1401
+ // point to earlier values, so literal reads terminate without cycle tracking. Ordering
1402
+ // rules may revisit the same pair through several min/max operands, so those pairs remain
1403
+ // memoized before their producers are expanded.
1404
+
1405
+ function staticAssertionObservation(
1406
+ valueID: ValueID,
1407
+ state: ExecutionState,
1408
+ context: TransferContext,
1409
+ ): AbstractBoolean {
1410
+ const held = requiredValue(state, valueID)
1411
+ // Lowering accepts boolean conditions. If an erased type assertion hid the runtime
1412
+ // kind, the assertion remains unproven instead of stopping the function analysis.
1413
+ if (held.kind !== 'boolean') return unknownBoolean()
1414
+ if (!held.canBeTrue || !held.canBeFalse) return held
1415
+ const producer = context.expressionContext.instructionByValue[valueID]
1416
+ if (producer?.kind === 'not') {
1417
+ const operand = staticAssertionObservation(producer.value, state, context)
1418
+ return {kind: 'boolean', canBeTrue: operand.canBeFalse, canBeFalse: operand.canBeTrue}
1419
+ }
1420
+ if (producer?.kind !== 'compare') return held
1421
+ const left = numberWithFacts(state, producer.left, context.expressionContext)
1422
+ const right = numberWithFacts(state, producer.right, context.expressionContext)
1423
+ if (left == null || right == null) return held
1424
+ return comparisonLocalProof(left, right, producer, state, context) ?? held
1425
+ }
1426
+
1427
+ function comparisonLocalProof(
1428
+ left: AbstractNumber,
1429
+ right: AbstractNumber,
1430
+ instruction: Extract<InstructionIR, {kind: 'compare'}>,
1431
+ state: ExecutionState,
1432
+ context: TransferContext,
1433
+ ): AbstractBoolean | null {
1434
+ if (left.mayBeNaN || right.mayBeNaN) return null
1435
+ const proof = createComparisonProof(state, context.expressionContext)
1436
+
1437
+ switch (instruction.operator) {
1438
+ case 'lessThan': {
1439
+ if (proof.strictlyBelow(instruction.left, instruction.right)) return exactBoolean(true)
1440
+ return proof.atMost(instruction.right, instruction.left) ? exactBoolean(false) : null
1441
+ }
1442
+ case 'lessThanOrEqual': {
1443
+ if (proof.atMost(instruction.left, instruction.right)) return exactBoolean(true)
1444
+ return proof.strictlyBelow(instruction.right, instruction.left) ? exactBoolean(false) : null
1445
+ }
1446
+ case 'greaterThan': {
1447
+ if (proof.strictlyBelow(instruction.right, instruction.left)) return exactBoolean(true)
1448
+ return proof.atMost(instruction.left, instruction.right) ? exactBoolean(false) : null
1449
+ }
1450
+ case 'greaterThanOrEqual': {
1451
+ if (proof.atMost(instruction.right, instruction.left)) return exactBoolean(true)
1452
+ return proof.strictlyBelow(instruction.left, instruction.right) ? exactBoolean(false) : null
1453
+ }
1454
+ case 'equal': {
1455
+ return proof.strictlyBelow(instruction.left, instruction.right)
1456
+ || proof.strictlyBelow(instruction.right, instruction.left) ? exactBoolean(false) : null
1457
+ }
1458
+ case 'notEqual': {
1459
+ return proof.strictlyBelow(instruction.left, instruction.right)
1460
+ || proof.strictlyBelow(instruction.right, instruction.left) ? exactBoolean(true) : null
1461
+ }
1462
+ }
1463
+ }
1464
+
1465
+ function createComparisonProof(
1466
+ state: ExecutionState,
1467
+ context: ExpressionContext,
1468
+ ): {
1469
+ same: (left: ValueID, right: ValueID) => boolean
1470
+ atMost: (left: ValueID, right: ValueID) => boolean
1471
+ strictlyBelow: (left: ValueID, right: ValueID) => boolean
1472
+ } {
1473
+ const atMostMemo = new Map<string, boolean>()
1474
+
1475
+ const heldNumber = (value: ValueID): AbstractNumber | null => {
1476
+ return numberWithFacts(state, resolveStoredValue(value, context), context)
1477
+ }
1478
+
1479
+ const same = (left: ValueID, right: ValueID): boolean => sameRuntimeValue(left, right, context)
1480
+
1481
+ const nonnegative = (value: ValueID): boolean => {
1482
+ const held = heldNumber(value)
1483
+ return held != null && held.lower >= 0 && !held.mayBeNaN
1484
+ }
1485
+
1486
+ const atMost = (rawLeft: ValueID, rawRight: ValueID): boolean => {
1487
+ const left = resolveStoredValue(rawLeft, context)
1488
+ const right = resolveStoredValue(rawRight, context)
1489
+ if (same(left, right)) return true
1490
+
1491
+ const leftNumber = heldNumber(left)
1492
+ const rightNumber = heldNumber(right)
1493
+ if (leftNumber != null && rightNumber != null
1494
+ && !leftNumber.mayBeNaN && !rightNumber.mayBeNaN
1495
+ && leftNumber.upper <= rightNumber.lower) return true
1496
+
1497
+ const key = `${left}:${right}`
1498
+ const cached = atMostMemo.get(key)
1499
+ if (cached != null) return cached
1500
+ atMostMemo.set(key, false)
1501
+
1502
+ const leftProducer = context.instructionByValue[left]
1503
+ const rightProducer = context.instructionByValue[right]
1504
+ let answer = false
1505
+
1506
+ // The defining operand is the cheapest and most common selection proof:
1507
+ // min(x, y) <= x and x <= max(x, y). Try exact identity before recursive rules.
1508
+ if (leftProducer?.kind === 'minimum') {
1509
+ answer = leftProducer.values.some(operand => same(operand, right))
1510
+ }
1511
+ if (!answer && rightProducer?.kind === 'maximum') {
1512
+ answer = rightProducer.values.some(operand => same(left, operand))
1513
+ }
1514
+
1515
+ // Math.min and Math.max are monotone in corresponding operands. Keeping the written
1516
+ // operand order makes the rule linear and predictable; agents can align equivalent
1517
+ // clamps instead of asking the checker to search permutations.
1518
+ if (leftProducer?.kind === 'minimum' && rightProducer?.kind === 'minimum'
1519
+ && leftProducer.values.length === rightProducer.values.length) {
1520
+ answer = leftProducer.values.every((operand, index) =>
1521
+ atMost(operand, rightProducer.values[index]!))
1522
+ }
1523
+
1524
+ if (!answer && leftProducer?.kind === 'binary'
1525
+ && leftProducer.operator === 'subtract'
1526
+ && nonnegative(leftProducer.right)) {
1527
+ answer = atMost(leftProducer.left, right)
1528
+ }
1529
+ if (!answer && rightProducer?.kind === 'binary' && rightProducer.operator === 'add') {
1530
+ if (nonnegative(rightProducer.right)) answer = atMost(left, rightProducer.left)
1531
+ if (!answer && nonnegative(rightProducer.left)) answer = atMost(left, rightProducer.right)
1532
+ }
1533
+ if (!answer && leftProducer?.kind === 'binary' && leftProducer.operator === 'multiply'
1534
+ && rightProducer?.kind === 'binary' && rightProducer.operator === 'multiply') {
1535
+ const leftForms = [[leftProducer.left, leftProducer.right], [leftProducer.right, leftProducer.left]] as const
1536
+ const rightForms = [[rightProducer.left, rightProducer.right], [rightProducer.right, rightProducer.left]] as const
1537
+ for (const [leftBase, leftFactor] of leftForms) {
1538
+ for (const [rightBase, rightFactor] of rightForms) {
1539
+ if (same(leftFactor, rightFactor)
1540
+ && nonnegative(leftFactor)
1541
+ && atMost(leftBase, rightBase)) answer = true
1542
+ }
1543
+ }
1544
+ }
1545
+
1546
+ // Expanding max(xs) <= min(ys) requires the full xs-by-ys relation. That work grows
1547
+ // quadratically with user-written operands, so aggregate selection proofs compose on
1548
+ // only one side. Bind and assert the relevant component relationship instead.
1549
+ if (!answer && leftProducer?.kind === 'maximum' && rightProducer?.kind === 'minimum') {
1550
+ atMostMemo.set(key, false)
1551
+ return false
1552
+ }
1553
+
1554
+ // Selection rules come last because expanding every operand is the broadest search.
1555
+ // Direct add/subtract and common-factor proofs above usually identify the written
1556
+ // relationship before the broader selection rules need to inspect every operand.
1557
+ if (!answer && leftProducer?.kind === 'maximum') {
1558
+ answer = leftProducer.values.every(operand => atMost(operand, right))
1559
+ }
1560
+ if (!answer && rightProducer?.kind === 'maximum') {
1561
+ answer = rightProducer.values.some(operand => atMost(left, operand))
1562
+ }
1563
+ if (!answer && leftProducer?.kind === 'minimum') {
1564
+ answer = leftProducer.values.some(operand => atMost(operand, right))
1565
+ }
1566
+ if (!answer && rightProducer?.kind === 'minimum') {
1567
+ answer = rightProducer.values.every(operand => atMost(left, operand))
1568
+ }
1569
+
1570
+ atMostMemo.set(key, answer)
1571
+ return answer
1572
+ }
1573
+
1574
+ const strictlyBelow = (rawLeft: ValueID, rawRight: ValueID): boolean => {
1575
+ const left = resolveStoredValue(rawLeft, context)
1576
+ const right = resolveStoredValue(rawRight, context)
1577
+ const leftNumber = heldNumber(left)
1578
+ const rightNumber = heldNumber(right)
1579
+ if (leftNumber != null && rightNumber != null
1580
+ && !leftNumber.mayBeNaN && !rightNumber.mayBeNaN
1581
+ && leftNumber.upper < rightNumber.lower) return true
1582
+
1583
+ const producer = context.instructionByValue[left]
1584
+ if (producer?.kind !== 'binary' || producer.operator !== 'remainder'
1585
+ || !same(producer.right, right)) return false
1586
+ const divisor = heldNumber(producer.right)
1587
+ return divisor != null && !divisor.mayBeNaN && divisor.lower > 0
1588
+ }
1589
+
1590
+ return {same, atMost, strictlyBelow}
1591
+ }
1592
+
1593
+ function compareNumbers(left: AbstractNumber, right: AbstractNumber, operator: ComparisonOperator): AbstractBoolean {
1594
+ if (left.mayBeNaN || right.mayBeNaN) return unknownBoolean()
1595
+ switch (operator) {
1596
+ case 'lessThan': return booleanRange((left.upper < right.lower), (left.lower >= right.upper))
1597
+ case 'lessThanOrEqual': return booleanRange((left.upper <= right.lower), (left.lower > right.upper))
1598
+ case 'greaterThan': return compareNumbers(right, left, 'lessThan')
1599
+ case 'greaterThanOrEqual': return compareNumbers(right, left, 'lessThanOrEqual')
1600
+ case 'equal': {
1601
+ const definitelyEqual = left.lower === left.upper && right.lower === right.upper && left.lower === right.lower
1602
+ const definitelyDifferent = left.upper < right.lower
1603
+ || right.upper < left.lower
1604
+ || (left.lower === left.upper && pointExcluded(right, left.lower))
1605
+ || (right.lower === right.upper && pointExcluded(left, right.lower))
1606
+ return booleanRange(definitelyEqual, definitelyDifferent)
1607
+ }
1608
+ case 'notEqual': {
1609
+ const equal = compareNumbers(left, right, 'equal')
1610
+ return {kind: 'boolean', canBeTrue: equal.canBeFalse, canBeFalse: equal.canBeTrue}
1611
+ }
1612
+ }
1613
+ }
1614
+
1615
+ function compareSameNumber(operand: AbstractNumber, operator: ComparisonOperator): AbstractBoolean {
1616
+ switch (operator) {
1617
+ case 'lessThan':
1618
+ case 'greaterThan': return exactBoolean(false)
1619
+ case 'equal':
1620
+ case 'lessThanOrEqual':
1621
+ case 'greaterThanOrEqual': return operand.mayBeNaN ? unknownBoolean() : exactBoolean(true)
1622
+ case 'notEqual': return operand.mayBeNaN ? unknownBoolean() : exactBoolean(false)
1623
+ }
1624
+ }
1625
+
1626
+ function exactBoolean(answer: boolean): AbstractBoolean {
1627
+ return {kind: 'boolean', canBeTrue: answer, canBeFalse: !answer}
1628
+ }
1629
+
1630
+ // Exact over the two-point lattice: definitely equal when both sides are the same known
1631
+ // constant, definitely different when they are opposite known constants.
1632
+ function compareBooleans(left: AbstractBoolean, right: AbstractBoolean, negated: boolean): AbstractBoolean {
1633
+ const leftKnown = left.canBeTrue !== left.canBeFalse
1634
+ const rightKnown = right.canBeTrue !== right.canBeFalse
1635
+ const definitelyEqual = leftKnown && rightKnown && left.canBeTrue === right.canBeTrue
1636
+ const definitelyDifferent = leftKnown && rightKnown && left.canBeTrue !== right.canBeTrue
1637
+ const equals = booleanRange(definitelyEqual, definitelyDifferent)
1638
+ return negated ? {kind: 'boolean', canBeTrue: equals.canBeFalse, canBeFalse: equals.canBeTrue} : equals
1639
+ }
1640
+
1641
+ function booleanRange(definitelyTrue: boolean, definitelyFalse: boolean): AbstractBoolean {
1642
+ return {
1643
+ kind: 'boolean',
1644
+ canBeTrue: !definitelyFalse,
1645
+ canBeFalse: !definitelyTrue,
1646
+ }
1647
+ }
1648
+
1649
+ function invertedComparison(operator: ComparisonOperator): ComparisonOperator {
1650
+ switch (operator) {
1651
+ case 'lessThan': return 'greaterThanOrEqual'
1652
+ case 'lessThanOrEqual': return 'greaterThan'
1653
+ case 'greaterThan': return 'lessThanOrEqual'
1654
+ case 'greaterThanOrEqual': return 'lessThan'
1655
+ case 'equal': return 'notEqual'
1656
+ case 'notEqual': return 'equal'
1657
+ }
1658
+ }
1659
+
1660
+ // x !== other: when the other side is a single known point, cut it from x — at an
1661
+ // interval endpoint (integers step by one, floats step to the adjacent representable
1662
+ // double), or via the excluded-point cut when the point sits strictly inside the bounds.
1663
+ // One point is the cap: a second !== guard replaces the first cut rather than growing a
1664
+ // set, which is sound (dropping a fact only widens) and keeps the domain flat.
1665
+ function excludePointFrom(value: AbstractNumber, other: AbstractNumber): AbstractNumber {
1666
+ if (other.lower !== other.upper || other.mayBeNaN) return value
1667
+ const point = other.lower
1668
+ let refined = value
1669
+ if (refined.lower === point) refined = {...refined, lower: strictLower(point, refined.integer)}
1670
+ if (refined.upper === point) refined = {...refined, upper: strictUpper(point, refined.integer)}
1671
+ if (refined.lower < point && point < refined.upper) refined = {...refined, excludesPoint: point}
1672
+ return refined
1673
+ }
1674
+
1675
+ function withBounds(value: AbstractNumber, lower: number, upper: number): AbstractNumber {
1676
+ let refinedLower = Math.max(value.lower, lower)
1677
+ let refinedUpper = Math.min(value.upper, upper)
1678
+
1679
+ // An integer interval refined by a non-strict comparison against a non-integer bound
1680
+ // (`if (count >= 3.2)`) would keep the fractional bound. Snap to the integer hull —
1681
+ // exact, since only integers inhabit the interval. Left unsnapped, the bounds and the
1682
+ // integer flag disagree: [3.2, 3.4] passes the lower > upper emptiness check while
1683
+ // containing no value, and a later comparison can prune both branch edges, stranding
1684
+ // the evaluation with no path end at all.
1685
+ if (value.integer) {
1686
+ refinedLower = Math.ceil(refinedLower)
1687
+ refinedUpper = Math.floor(refinedUpper)
1688
+ }
1689
+ // A possibly infinite value lives at its interval's infinite end, so a refinement that
1690
+ // clips the interval to finite bounds also proves finiteness — with finiteness derived
1691
+ // from the bounds, that now holds by construction.
1692
+ return normalizeRefinedNumber({...value, lower: refinedLower, upper: refinedUpper})
1693
+ }
1694
+
1695
+ // Keep the number record internally consistent after an intersection. Exact integer
1696
+ // singletons are integers regardless of how they were reached, and an excluded point
1697
+ // that becomes an endpoint moves the endpoint past that impossible value.
1698
+ function normalizeRefinedNumber(value: AbstractNumber): AbstractNumber {
1699
+ let lower = value.lower
1700
+ let upper = value.upper
1701
+ const integer = value.integer || (lower === upper && Number.isInteger(lower))
1702
+ const {excludesPoint, ...rest} = value
1703
+ if (excludesPoint != null) {
1704
+ if (lower === excludesPoint) lower = strictLower(excludesPoint, integer)
1705
+ if (upper === excludesPoint) upper = strictUpper(excludesPoint, integer)
1706
+ }
1707
+ const normalized: AbstractNumber = {...rest, lower, upper, integer}
1708
+ if (excludesPoint != null && lower < excludesPoint && excludesPoint < upper) {
1709
+ normalized.excludesPoint = excludesPoint
1710
+ }
1711
+ return normalized
1712
+ }
1713
+
1714
+ // The exact refinement for a strict comparison: for integers the next integer, for floats
1715
+ // the adjacent representable double — runtime x > b implies x >= nextUp(b), so `if
1716
+ // (height > 0)` proves the divisor nonzero instead of keeping zero as a closed bound.
1717
+ function strictLower(value: number, integer: boolean): number {
1718
+ return integer ? Math.floor(value) + 1 : nextUp(value)
1719
+ }
1720
+
1721
+ function strictUpper(value: number, integer: boolean): number {
1722
+ return integer ? Math.ceil(value) - 1 : nextDown(value)
1723
+ }