@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.
- package/CHANGELOG.md +15 -0
- package/LICENSE +21 -0
- package/README.md +242 -0
- package/fr.ts +33 -0
- package/package.json +39 -0
- package/src/analyze.ts +16 -0
- package/src/audit.ts +468 -0
- package/src/domain/number.ts +444 -0
- package/src/domain/value.ts +445 -0
- package/src/engine/analyze.ts +745 -0
- package/src/engine/outcome.ts +137 -0
- package/src/engine/state.ts +155 -0
- package/src/engine/transfer.ts +1723 -0
- package/src/index.ts +23 -0
- package/src/ir/finite-inputs.ts +51 -0
- package/src/ir/function-usage.ts +50 -0
- package/src/ir/ids.ts +10 -0
- package/src/ir/instructions.ts +164 -0
- package/src/ir/program.ts +446 -0
- package/src/lower/accept.ts +119 -0
- package/src/lower/context.ts +250 -0
- package/src/lower/expression.ts +1742 -0
- package/src/lower/literals.ts +84 -0
- package/src/lower/module.ts +748 -0
- package/src/lower/platform.ts +81 -0
- package/src/lower/program.ts +310 -0
- package/src/lower/statements.ts +553 -0
- package/src/lower/static-intrinsics.ts +87 -0
- package/src/project.ts +500 -0
- package/src/report/format-requirement.ts +110 -0
- package/src/report/index.ts +1132 -0
- package/src/requirements/infer.ts +334 -0
- package/src/requirements/model.ts +77 -0
- package/src/typescript/check.ts +56 -0
- package/src/typescript/diagnostics.ts +69 -0
- package/src/typescript/project.ts +101 -0
|
@@ -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
|
+
}
|