@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,137 @@
|
|
|
1
|
+
import type {AbstractValue} from '../domain/value.ts'
|
|
2
|
+
import type {FunctionID, ModuleBindingID, SiteID} from '../ir/ids.ts'
|
|
3
|
+
import type {FunctionIR, UnsupportedFunctionIR, UnsupportedReason} from '../ir/program.ts'
|
|
4
|
+
import type {BoundsAssumption, InferredPrecondition} from '../requirements/model.ts'
|
|
5
|
+
import type {SharedState, ValueFact} from './state.ts'
|
|
6
|
+
|
|
7
|
+
export type RequirementFailure =
|
|
8
|
+
| {kind: 'declared'; site: SiteID; status: 'refuted' | 'unproven'}
|
|
9
|
+
| {kind: 'finiteInput'; site: SiteID; status: 'refuted' | 'unproven'}
|
|
10
|
+
| {kind: 'nonzeroDivisor'; site: SiteID; operation: 'division' | 'remainder'}
|
|
11
|
+
| {kind: 'elementInBounds'; site: SiteID}
|
|
12
|
+
|
|
13
|
+
// Why one function's evaluation stopped on some path. Code branches only on `kind`; prose
|
|
14
|
+
// is composed only in src/report.
|
|
15
|
+
export type StopReason =
|
|
16
|
+
| {kind: 'recursion'; callee: FunctionID}
|
|
17
|
+
// The called function never lowered, or its own evaluation has stops. The callee's report
|
|
18
|
+
// entry carries the next hop or the root cause.
|
|
19
|
+
| {kind: 'calleeStopped'; callee: FunctionID}
|
|
20
|
+
// A loop header's abstract state kept changing through the fixed-point backstop. No
|
|
21
|
+
// result computed before convergence is published as a guarantee.
|
|
22
|
+
| {kind: 'loopLimit'; updates: number}
|
|
23
|
+
// A loop whose exit edge is never taken on any analyzed path, e.g.
|
|
24
|
+
// `for (let index = 0; true; index += 1) {}`. The fixed point converged with every path
|
|
25
|
+
// still inside the loop, so the function has no reachable return.
|
|
26
|
+
| {kind: 'nonExitingLoop'}
|
|
27
|
+
// A stop terminator was reached. Only the file-wide rejections (eval, type-check
|
|
28
|
+
// suppression) produce one today, as the whole-file initializer replacement.
|
|
29
|
+
| {kind: 'unsupportedCode'; reason: UnsupportedReason}
|
|
30
|
+
// A moduleRead found nothing usable in the slot. Report prose comes from the binding's
|
|
31
|
+
// category: imported, an untracked object, an unsupported type, or read before its
|
|
32
|
+
// initialization.
|
|
33
|
+
| {kind: 'moduleRead'; binding: ModuleBindingID}
|
|
34
|
+
// A value reached an operation that needs a concrete runtime kind, but the analysis
|
|
35
|
+
// carries it without one. This includes opaque values and narrowing shapes the engine
|
|
36
|
+
// does not model. The path stops; the rest of the function and file report normally.
|
|
37
|
+
| {kind: 'kindMismatch'}
|
|
38
|
+
// A bare array read (arr[i]) remained possibly undefined and reached an operation that
|
|
39
|
+
// needs a present value. This can be diagnostic-clean when noUncheckedIndexedAccess is
|
|
40
|
+
// disabled, so it has its own actionable reason instead of looking like unsupported
|
|
41
|
+
// TypeScript narrowing.
|
|
42
|
+
| {kind: 'possiblyMissingElement'}
|
|
43
|
+
// A written or inferred requirement failed. A direct failure has no callee; propagation
|
|
44
|
+
// through a same-file call names the callee visible at that call while retaining the
|
|
45
|
+
// original requirement site.
|
|
46
|
+
| {
|
|
47
|
+
kind: 'requirementFailure'
|
|
48
|
+
failure: RequirementFailure
|
|
49
|
+
callee: FunctionID | null
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type Stop = {
|
|
53
|
+
site: SiteID
|
|
54
|
+
reason: StopReason
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// The result for one interior console.assert. A blocked result means its function did not
|
|
58
|
+
// finish analysis on every path without a site-specific assumption.
|
|
59
|
+
export type AssertionVerdict = {
|
|
60
|
+
site: SiteID
|
|
61
|
+
text: string
|
|
62
|
+
verdict: 'proven' | 'refuted' | 'unproven' | 'dead' | 'blocked'
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// One evaluation can hold BOTH a normal outcome and stops: in
|
|
66
|
+
// `if (flag > 0) return 10; unsupportedThing()` the true branch returns 10 while the other
|
|
67
|
+
// path stops. Empty `stops` means every path completed.
|
|
68
|
+
export type FunctionEvaluation = {
|
|
69
|
+
normal: {returnValue: AbstractValue; sharedState: SharedState; valueFacts: ValueFact[]} | null
|
|
70
|
+
preconditions: InferredPrecondition[]
|
|
71
|
+
boundsAssumptions: BoundsAssumption[]
|
|
72
|
+
assertions: AssertionVerdict[]
|
|
73
|
+
stops: Stop[]
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// The result of a fully completed evaluation: the only data that may reach contract
|
|
77
|
+
// consumers (a caller adopting callee state, the report's requires/ensures lines).
|
|
78
|
+
// completedEvaluation below is the single way to obtain it, and refuses whenever any stop
|
|
79
|
+
// exists, so partial results structurally cannot flow into those consumers.
|
|
80
|
+
export type CompletedEvaluation = {
|
|
81
|
+
returnValue: AbstractValue
|
|
82
|
+
sharedState: SharedState
|
|
83
|
+
valueFacts: ValueFact[]
|
|
84
|
+
preconditions: InferredPrecondition[]
|
|
85
|
+
boundsAssumptions: BoundsAssumption[]
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function completedEvaluation(evaluation: FunctionEvaluation): CompletedEvaluation | null {
|
|
89
|
+
if (evaluation.stops.length > 0 || evaluation.normal == null) return null
|
|
90
|
+
return {
|
|
91
|
+
returnValue: evaluation.normal.returnValue,
|
|
92
|
+
sharedState: evaluation.normal.sharedState,
|
|
93
|
+
valueFacts: evaluation.normal.valueFacts,
|
|
94
|
+
preconditions: evaluation.preconditions,
|
|
95
|
+
boundsAssumptions: evaluation.boundsAssumptions,
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export type FunctionAnalysis =
|
|
100
|
+
| {
|
|
101
|
+
kind: 'analyzed'
|
|
102
|
+
lowering: FunctionIR
|
|
103
|
+
preconditions: InferredPrecondition[]
|
|
104
|
+
boundsAssumptions: BoundsAssumption[]
|
|
105
|
+
returnValue: AbstractValue
|
|
106
|
+
assertions: AssertionVerdict[]
|
|
107
|
+
}
|
|
108
|
+
// Some path stopped. The evidence fields share no names with the contract fields above,
|
|
109
|
+
// so report code cannot consume them interchangeably: observedReturn describes only the
|
|
110
|
+
// paths that completed, and observedNeeds the requirements inferred on the path prefixes
|
|
111
|
+
// the analysis did evaluate (a sibling path may contribute one after another path stopped).
|
|
112
|
+
| {
|
|
113
|
+
kind: 'partial'
|
|
114
|
+
lowering: FunctionIR
|
|
115
|
+
stops: [Stop, ...Stop[]]
|
|
116
|
+
observedReturn: {value: AbstractValue} | null
|
|
117
|
+
observedNeeds: InferredPrecondition[]
|
|
118
|
+
observedBoundsAssumptions: BoundsAssumption[]
|
|
119
|
+
assertions: AssertionVerdict[]
|
|
120
|
+
}
|
|
121
|
+
// The function did not lower. The variant carries a reference (not a copy) to its
|
|
122
|
+
// lowering record, so a mismatched pair is unrepresentable and the report needs no
|
|
123
|
+
// defensive re-checks.
|
|
124
|
+
| {kind: 'notLowered'; lowering: UnsupportedFunctionIR}
|
|
125
|
+
|
|
126
|
+
export type LoweredFunctionAnalysis = Exclude<FunctionAnalysis, {kind: 'notLowered'}>
|
|
127
|
+
|
|
128
|
+
export type ProgramAnalysis = {
|
|
129
|
+
// Dense, index-aligned with ProgramIR.functions.
|
|
130
|
+
functions: FunctionAnalysis[]
|
|
131
|
+
// The synthetic module initializer's own analysis. Reports print it only when it stopped.
|
|
132
|
+
initializer: LoweredFunctionAnalysis
|
|
133
|
+
// Indexed by ModuleBindingID: the exact value functions may trust, or null when only the
|
|
134
|
+
// binding's declared kind is known. Reports use this to print assumes lines for reads of
|
|
135
|
+
// assumed-finite module numbers.
|
|
136
|
+
moduleValues: Array<AbstractValue | null>
|
|
137
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import type {AbstractValue} from '../domain/value.ts'
|
|
2
|
+
import {joinValues, sameValues, widenValue} from '../domain/value.ts'
|
|
3
|
+
|
|
4
|
+
// Indexed by ModuleBindingID, fixed length per program. Flows through calls, so a
|
|
5
|
+
// callee's module writes are visible to the caller after a completed call. Null means the
|
|
6
|
+
// binding is not initialized; reading it stops the path.
|
|
7
|
+
export type SharedState = Array<AbstractValue | null>
|
|
8
|
+
|
|
9
|
+
export type ExecutionState = {
|
|
10
|
+
values: Array<AbstractValue | undefined>
|
|
11
|
+
shared: SharedState
|
|
12
|
+
// Conditions established by a guard or by a requirement/assumption already recorded
|
|
13
|
+
// on this path. Value keys name immutable runtime values, so assignment naturally uses
|
|
14
|
+
// a new key. Joins intersect the list. This is deliberately a closed set of three facts,
|
|
15
|
+
// with no transitivity or arithmetic, stored as a small deduplicated array.
|
|
16
|
+
valueFacts: ValueFact[]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type ValueFact =
|
|
20
|
+
| {kind: 'nonzero'; value: string}
|
|
21
|
+
// The strict `index < array.length` half of a bounds guard. The index's own abstract
|
|
22
|
+
// number must still prove integer, non-NaN, and nonnegative.
|
|
23
|
+
| {kind: 'belowLength'; index: string; array: string}
|
|
24
|
+
// A requirement or assumption for an asserted read proves the complete condition.
|
|
25
|
+
| {kind: 'validIndex'; index: string; array: string}
|
|
26
|
+
|
|
27
|
+
export function hasNonzeroFact(facts: ValueFact[], value: string): boolean {
|
|
28
|
+
return facts.some(fact => fact.kind === 'nonzero' && fact.value === value)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function hasIndexFact(
|
|
32
|
+
facts: ValueFact[],
|
|
33
|
+
kind: 'belowLength' | 'validIndex',
|
|
34
|
+
index: string,
|
|
35
|
+
array: string,
|
|
36
|
+
): boolean {
|
|
37
|
+
return facts.some(fact => fact.kind === kind && fact.index === index && fact.array === array)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function addValueFact(facts: ValueFact[], candidate: ValueFact): void {
|
|
41
|
+
if (!facts.some(fact => sameValueFact(fact, candidate))) facts.push(candidate)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function intersectValueFacts(left: ValueFact[], right: ValueFact[]): ValueFact[] {
|
|
45
|
+
const intersection: ValueFact[] = []
|
|
46
|
+
for (const leftFact of left) {
|
|
47
|
+
for (const rightFact of right) {
|
|
48
|
+
const shared = intersectValueFact(leftFact, rightFact)
|
|
49
|
+
if (shared != null) addValueFact(intersection, shared)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return intersection
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function intersectValueFact(left: ValueFact, right: ValueFact): ValueFact | null {
|
|
56
|
+
if (sameValueFact(left, right)) return left
|
|
57
|
+
if (left.kind === 'nonzero' || right.kind === 'nonzero') return null
|
|
58
|
+
if (left.index !== right.index || left.array !== right.array) return null
|
|
59
|
+
return {kind: 'belowLength', index: left.index, array: left.array}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function sameValueFact(left: ValueFact, right: ValueFact): boolean {
|
|
63
|
+
if (left.kind !== right.kind) return false
|
|
64
|
+
if (left.kind === 'nonzero' && right.kind === 'nonzero') return left.value === right.value
|
|
65
|
+
if (left.kind === 'nonzero' || right.kind === 'nonzero') return false
|
|
66
|
+
return left.index === right.index && left.array === right.array
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function emptySharedState(moduleCount: number): SharedState {
|
|
70
|
+
return Array.from({length: moduleCount}, () => null)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function cloneSharedState(state: SharedState): SharedState {
|
|
74
|
+
// Slots are replaced whole on write, never mutated, so a shallow copy suffices.
|
|
75
|
+
return state.slice()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function cloneState(state: ExecutionState): ExecutionState {
|
|
79
|
+
return {
|
|
80
|
+
values: state.values.slice(),
|
|
81
|
+
shared: cloneSharedState(state.shared),
|
|
82
|
+
// Facts are never mutated, only appended or filtered, so a shallow copy suffices.
|
|
83
|
+
valueFacts: state.valueFacts.slice(),
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// A new state field must participate in both the merged value and `changed`. Listing the
|
|
88
|
+
// fields here makes that review mandatory when ExecutionState grows.
|
|
89
|
+
const mergedStateFields: Record<keyof ExecutionState, true> = {values: true, shared: true, valueFacts: true}
|
|
90
|
+
|
|
91
|
+
// Joins one incoming state into the block's previous state and reports whether the block
|
|
92
|
+
// must run again. The comparison happens while each joined value is already in hand, so
|
|
93
|
+
// propagation does not walk the complete historical frame a second time.
|
|
94
|
+
export function mergeStates(previous: ExecutionState, candidate: ExecutionState, widen: boolean): {state: ExecutionState; changed: boolean} {
|
|
95
|
+
void mergedStateFields
|
|
96
|
+
const values: ExecutionState['values'] = []
|
|
97
|
+
const length = Math.max(previous.values.length, candidate.values.length)
|
|
98
|
+
let changed = previous.values.length !== length
|
|
99
|
+
for (let index = 0; index < length; index++) {
|
|
100
|
+
const previousValue = previous.values[index]
|
|
101
|
+
const candidateValue = candidate.values[index]
|
|
102
|
+
if (previousValue == null) {
|
|
103
|
+
values[index] = candidateValue
|
|
104
|
+
if (candidateValue != null) changed = true
|
|
105
|
+
} else if (candidateValue == null) {
|
|
106
|
+
values[index] = previousValue
|
|
107
|
+
} else {
|
|
108
|
+
const joined = joinValues(previousValue, candidateValue)
|
|
109
|
+
const merged = widen ? widenValue(previousValue, joined) : joined
|
|
110
|
+
values[index] = merged
|
|
111
|
+
if (!sameValues(previousValue, merged)) changed = true
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const shared: SharedState = []
|
|
115
|
+
for (let index = 0; index < previous.shared.length; index++) {
|
|
116
|
+
const previousValue = previous.shared[index]
|
|
117
|
+
const candidateValue = candidate.shared[index]
|
|
118
|
+
if (previousValue == null || candidateValue == null) {
|
|
119
|
+
shared.push(null)
|
|
120
|
+
if (previousValue != null) changed = true
|
|
121
|
+
} else {
|
|
122
|
+
const joined = joinValues(previousValue, candidateValue)
|
|
123
|
+
const merged = widen ? widenValue(previousValue, joined) : joined
|
|
124
|
+
shared.push(merged)
|
|
125
|
+
if (!sameValues(previousValue, merged)) changed = true
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const valueFacts = intersectValueFacts(previous.valueFacts, candidate.valueFacts)
|
|
129
|
+
if (
|
|
130
|
+
valueFacts.length !== previous.valueFacts.length
|
|
131
|
+
|| valueFacts.some(fact => !previous.valueFacts.some(previousFact => sameValueFact(fact, previousFact)))
|
|
132
|
+
) changed = true
|
|
133
|
+
const state: ExecutionState = {
|
|
134
|
+
values,
|
|
135
|
+
shared,
|
|
136
|
+
valueFacts,
|
|
137
|
+
}
|
|
138
|
+
return {state, changed}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Uninitialized dominates: a binding is only initialized when every joined path
|
|
142
|
+
// initialized it.
|
|
143
|
+
export function joinModuleSlots(left: SharedState, right: SharedState): SharedState {
|
|
144
|
+
const joined: SharedState = []
|
|
145
|
+
for (let index = 0; index < left.length; index++) {
|
|
146
|
+
const leftValue = left[index]
|
|
147
|
+
const rightValue = right[index]
|
|
148
|
+
joined.push(
|
|
149
|
+
leftValue == null || rightValue == null
|
|
150
|
+
? null
|
|
151
|
+
: joinValues(leftValue, rightValue),
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
return joined
|
|
155
|
+
}
|