@chenglou/freerange 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,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
+ }