al-sem 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/LICENSE +21 -0
- package/README.md +361 -0
- package/package.json +64 -0
- package/scripts/d40-diff.ts +44 -0
- package/scripts/fetch-native-parser.ts +179 -0
- package/scripts/precision-sample.ts +99 -0
- package/scripts/precision-study.ts +42 -0
- package/scripts/precision-tabulate.ts +52 -0
- package/src/cli/baseline.ts +31 -0
- package/src/cli/diff.ts +199 -0
- package/src/cli/events-chains.ts +56 -0
- package/src/cli/events-fanout.ts +87 -0
- package/src/cli/exit-code.ts +30 -0
- package/src/cli/fingerprint-indexes.ts +130 -0
- package/src/cli/fingerprint-query.ts +543 -0
- package/src/cli/fingerprint-witness.ts +493 -0
- package/src/cli/fingerprint.ts +292 -0
- package/src/cli/format-compact-json.ts +45 -0
- package/src/cli/format-events.ts +77 -0
- package/src/cli/format-fingerprint.ts +295 -0
- package/src/cli/format-html.ts +503 -0
- package/src/cli/format-json.ts +13 -0
- package/src/cli/format-policy.ts +95 -0
- package/src/cli/format-sarif.ts +186 -0
- package/src/cli/format-terminal.ts +153 -0
- package/src/cli/index.ts +566 -0
- package/src/cli/policy.ts +204 -0
- package/src/config/roots-config.ts +302 -0
- package/src/deps/cache-versions.ts +74 -0
- package/src/deps/canonical-json.ts +27 -0
- package/src/deps/dependency-artifact.ts +144 -0
- package/src/deps/dependency-cache.ts +262 -0
- package/src/deps/dependency-dag.ts +128 -0
- package/src/deps/dependency-package-discovery.ts +85 -0
- package/src/deps/dependency-pipeline.ts +483 -0
- package/src/deps/dependency-projection.ts +211 -0
- package/src/deps/dependency-resolver.ts +154 -0
- package/src/deps/workspace-dependencies.ts +114 -0
- package/src/detectors/capability-query.ts +145 -0
- package/src/detectors/confidence.ts +52 -0
- package/src/detectors/d1-db-op-in-loop.ts +457 -0
- package/src/detectors/d10-self-modifying-loop.ts +114 -0
- package/src/detectors/d11-modify-without-get.ts +129 -0
- package/src/detectors/d12-dead-integration-event.ts +81 -0
- package/src/detectors/d13-cross-app-internal-call.ts +105 -0
- package/src/detectors/d14-dead-routine.ts +151 -0
- package/src/detectors/d16-obsolete-routine-call.ts +94 -0
- package/src/detectors/d17-min-version-drift.ts +157 -0
- package/src/detectors/d18-constant-filter-in-loop.ts +151 -0
- package/src/detectors/d19-unused-parameter.ts +116 -0
- package/src/detectors/d2-event-fanout-in-loop.ts +240 -0
- package/src/detectors/d20-unreachable-after-exit.ts +92 -0
- package/src/detectors/d21-read-without-load.ts +128 -0
- package/src/detectors/d22-flowfield-without-calcfields.ts +168 -0
- package/src/detectors/d29-subscriber-modify-on-event-record.ts +163 -0
- package/src/detectors/d3-load-state.ts +72 -0
- package/src/detectors/d3-missing-setloadfields.ts +234 -0
- package/src/detectors/d32-constant-boolean-parameter.ts +185 -0
- package/src/detectors/d33-unfiltered-bulk-write.ts +173 -0
- package/src/detectors/d34-commit-in-loop.ts +206 -0
- package/src/detectors/d35-commit-in-event-subscriber.ts +138 -0
- package/src/detectors/d36-late-setloadfields.ts +162 -0
- package/src/detectors/d37-validate-without-persist.ts +271 -0
- package/src/detectors/d38-subscriber-to-obsolete-event.ts +140 -0
- package/src/detectors/d39-record-left-dirty-across-chain.ts +165 -0
- package/src/detectors/d4-repeated-lookup-in-loop.ts +128 -0
- package/src/detectors/d40-transitive-load-missing.ts +217 -0
- package/src/detectors/d41-transitive-filter-loss.ts +200 -0
- package/src/detectors/d42-cross-call-wrong-setloadfields.ts +243 -0
- package/src/detectors/d43-event-ishandled-skip.ts +257 -0
- package/src/detectors/d44-event-multi-subscriber-overlap.ts +223 -0
- package/src/detectors/d45-event-transitive-table-exposure.ts +159 -0
- package/src/detectors/d5-set-based-opportunity.ts +162 -0
- package/src/detectors/d7-recursive-event-expansion.ts +151 -0
- package/src/detectors/d8-commit-in-transaction.ts +132 -0
- package/src/detectors/d9-transaction-span-summary.ts +107 -0
- package/src/detectors/detector-context.ts +121 -0
- package/src/detectors/finding-grouping.ts +61 -0
- package/src/detectors/path-merge.ts +174 -0
- package/src/detectors/registry.ts +176 -0
- package/src/detectors/table-display.ts +42 -0
- package/src/diff/diff-abi.ts +195 -0
- package/src/diff/diff-capabilities.ts +179 -0
- package/src/diff/diff-engine.ts +146 -0
- package/src/diff/diff-events.ts +323 -0
- package/src/diff/diff-identity.ts +73 -0
- package/src/diff/diff-indexes.ts +199 -0
- package/src/diff/diff-permissions.ts +260 -0
- package/src/diff/diff-policy.ts +101 -0
- package/src/diff/diff-preflight.ts +66 -0
- package/src/diff/diff-renames.ts +104 -0
- package/src/diff/diff-schema.ts +232 -0
- package/src/diff/format-diff.ts +148 -0
- package/src/engine/attribute-parser.ts +50 -0
- package/src/engine/capability-cone.ts +531 -0
- package/src/engine/combined-graph.ts +357 -0
- package/src/engine/control-flow-walker.ts +1317 -0
- package/src/engine/dispatch-sites.ts +199 -0
- package/src/engine/effect-lattice.ts +81 -0
- package/src/engine/entry-points.ts +57 -0
- package/src/engine/event-flow.ts +524 -0
- package/src/engine/event-relay.ts +92 -0
- package/src/engine/op-classification.ts +92 -0
- package/src/engine/path-walker.ts +189 -0
- package/src/engine/reverse-call-graph.ts +23 -0
- package/src/engine/root-classifier-overlay.ts +194 -0
- package/src/engine/root-classifier.ts +135 -0
- package/src/engine/scc.ts +110 -0
- package/src/engine/source-anchor.ts +25 -0
- package/src/engine/summary-context.ts +104 -0
- package/src/engine/summary-engine.ts +296 -0
- package/src/engine/summary-runner.ts +560 -0
- package/src/engine/transaction-spans.ts +112 -0
- package/src/engine/uncertainty-util.ts +54 -0
- package/src/hash.ts +31 -0
- package/src/index/attribute-from-node.ts +141 -0
- package/src/index/callee-from-node.ts +181 -0
- package/src/index/capability/background.ts +90 -0
- package/src/index/capability/commit.ts +44 -0
- package/src/index/capability/dispatch.ts +164 -0
- package/src/index/capability/events.ts +65 -0
- package/src/index/capability/extractor.ts +124 -0
- package/src/index/capability/file-blob.ts +137 -0
- package/src/index/capability/http.ts +159 -0
- package/src/index/capability/hyperlink.ts +60 -0
- package/src/index/capability/isolated-storage.ts +179 -0
- package/src/index/capability/table.ts +113 -0
- package/src/index/capability/telemetry.ts +84 -0
- package/src/index/capability/ui.ts +55 -0
- package/src/index/capability/value-source.ts +202 -0
- package/src/index/expression-from-node.ts +117 -0
- package/src/index/indexer.ts +102 -0
- package/src/index/intraprocedural-body.ts +1467 -0
- package/src/index/intraprocedural-ops.ts +253 -0
- package/src/index/intraprocedural-refs.ts +188 -0
- package/src/index/object-indexer.ts +279 -0
- package/src/index/routine-indexer.ts +282 -0
- package/src/index/routine-signature.ts +46 -0
- package/src/index/variable-indexer.ts +134 -0
- package/src/index/variable-initializer-extractor.ts +155 -0
- package/src/index/variable-type-normalizer.ts +83 -0
- package/src/index.ts +267 -0
- package/src/mcp/server.ts +72 -0
- package/src/mcp/session.ts +49 -0
- package/src/mcp/tools/explain-path.ts +75 -0
- package/src/mcp/tools/get-analysis-health.ts +62 -0
- package/src/mcp/tools/get-finding.ts +47 -0
- package/src/mcp/tools/get-routine-summary.ts +126 -0
- package/src/mcp/tools/list-findings.ts +85 -0
- package/src/mcp/tools/list-hotspots.ts +78 -0
- package/src/mcp/tools/list-rollups.ts +103 -0
- package/src/mcp/tools/validators.ts +25 -0
- package/src/model/attributes.ts +120 -0
- package/src/model/callee.ts +45 -0
- package/src/model/capability.ts +187 -0
- package/src/model/coverage.ts +85 -0
- package/src/model/entities.ts +628 -0
- package/src/model/expression.ts +98 -0
- package/src/model/finding.ts +110 -0
- package/src/model/graph-edge.ts +93 -0
- package/src/model/graph.ts +62 -0
- package/src/model/identity.ts +81 -0
- package/src/model/ids.ts +90 -0
- package/src/model/index.ts +13 -0
- package/src/model/model.ts +51 -0
- package/src/model/permission.ts +76 -0
- package/src/model/root-classification.ts +116 -0
- package/src/model/stable-identity.ts +102 -0
- package/src/model/summary.ts +96 -0
- package/src/parser/ast.ts +82 -0
- package/src/parser/native/ffi.ts +145 -0
- package/src/parser/native/parse-index-pool.ts +148 -0
- package/src/parser/native/parse-index-worker.ts +94 -0
- package/src/parser/native/wrapper.ts +353 -0
- package/src/parser/parser-init.ts +43 -0
- package/src/perf/profiler.ts +66 -0
- package/src/policy/policy-default.yaml +83 -0
- package/src/policy/policy-engine.ts +339 -0
- package/src/policy/policy-loader.ts +257 -0
- package/src/policy/policy-schema.json +379 -0
- package/src/policy/policy-types.ts +81 -0
- package/src/policy/predicate-compiler.ts +151 -0
- package/src/policy/predicate-evaluator.ts +267 -0
- package/src/policy/predicate-fields.ts +439 -0
- package/src/projection/actionable-anchor.ts +48 -0
- package/src/projection/finding-filters.ts +44 -0
- package/src/projection/finding-fingerprint.ts +54 -0
- package/src/projection/finding-groups.ts +41 -0
- package/src/projection/finding-summary.ts +110 -0
- package/src/projection/rollup-findings.ts +105 -0
- package/src/providers/discover.ts +88 -0
- package/src/providers/external.ts +46 -0
- package/src/providers/types.ts +36 -0
- package/src/providers/workspace.ts +117 -0
- package/src/resolve/call-resolver.ts +117 -0
- package/src/resolve/coverage.ts +61 -0
- package/src/resolve/event-graph.ts +166 -0
- package/src/resolve/implicit-edges.ts +53 -0
- package/src/resolve/record-types.ts +36 -0
- package/src/resolve/resolver.ts +23 -0
- package/src/resolve/semantic-graph.ts +29 -0
- package/src/resolve/symbol-table.ts +69 -0
- package/src/snapshot/app-snapshot.ts +74 -0
- package/src/snapshot/compose.ts +100 -0
- package/src/snapshot/derive/callsite-evidence.ts +76 -0
- package/src/snapshot/derive/capability-facts.ts +70 -0
- package/src/snapshot/derive/contracts.ts +131 -0
- package/src/snapshot/derive/coverage.ts +35 -0
- package/src/snapshot/derive/event-declarations.ts +140 -0
- package/src/snapshot/derive/identity-table.ts +58 -0
- package/src/snapshot/derive/inputs.ts +91 -0
- package/src/snapshot/derive/operation-evidence.ts +70 -0
- package/src/snapshot/derive/permissions.ts +186 -0
- package/src/snapshot/derive/root-classifications.ts +56 -0
- package/src/snapshot/derive/schema.ts +130 -0
- package/src/snapshot/derive/typed-edges.ts +60 -0
- package/src/snapshot/derive/workspace-fingerprint.ts +19 -0
- package/src/snapshot/deserialize.ts +40 -0
- package/src/snapshot/serialize-cbor-gz.ts +12 -0
- package/src/snapshot/serialize-cbor.ts +19 -0
- package/src/snapshot/serialize-json.ts +22 -0
- package/src/snapshot/shard.ts +134 -0
- package/src/snapshot/types.ts +181 -0
- package/src/symbols/app-manifest.ts +96 -0
- package/src/symbols/app-package-zip.ts +50 -0
- package/src/symbols/embedded-source-reader.ts +41 -0
- package/src/symbols/package-hash.ts +81 -0
- package/src/symbols/symbol-reader.ts +101 -0
- package/src/symbols/symbol-reference-parser.ts +378 -0
- package/src/symbols/symbol-reference-reader.ts +27 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,1317 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CallArgumentBinding,
|
|
3
|
+
CallSite,
|
|
4
|
+
ControlFlowNode,
|
|
5
|
+
FieldAccess,
|
|
6
|
+
RecordOperation,
|
|
7
|
+
Routine,
|
|
8
|
+
} from "../model/entities.ts";
|
|
9
|
+
import type { CallsiteId, FieldId, RoutineId } from "../model/ids.ts";
|
|
10
|
+
import type { EffectPresence, RoutineSummary } from "../model/summary.ts";
|
|
11
|
+
import { joinPresence } from "./effect-lattice.ts";
|
|
12
|
+
import { recordFlowRoleOf } from "./op-classification.ts";
|
|
13
|
+
import type { SummaryContext } from "./summary-context.ts";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Per-parameter path-aware state computed by the walker.
|
|
17
|
+
*
|
|
18
|
+
* Phase 6 rewrite: the walker now does a recursive AL-statement-tree traversal
|
|
19
|
+
* (via `routine.features.statementTree`) maintaining branch-aware per-parameter
|
|
20
|
+
* state. Output facts:
|
|
21
|
+
* - Entry requirements: `requiresLoadedAtEntry`, `requiredLoadedFieldsAtEntry`,
|
|
22
|
+
* `mutatesBeforeLoad` (same shape as Phase 4 — strictly more precise now).
|
|
23
|
+
* - Exit effects: `dirtyAtExit`, `currentLoadedFieldsAtExit` (path-proven).
|
|
24
|
+
* - Per-callsite snapshot: `currentLoadedFieldsAtCallsite` (for D42 in P8).
|
|
25
|
+
*/
|
|
26
|
+
export interface PathAwareFacts {
|
|
27
|
+
parameterIndex: number;
|
|
28
|
+
|
|
29
|
+
// Entry requirements (Phase 4 — preserved with higher precision in Phase 6)
|
|
30
|
+
requiresLoadedAtEntry: EffectPresence;
|
|
31
|
+
requiredLoadedFieldsAtEntry: FieldId[] | "unknown";
|
|
32
|
+
mutatesBeforeLoad: EffectPresence;
|
|
33
|
+
|
|
34
|
+
// Per-callsite snapshots — currentLoadedFields visible to the callee at
|
|
35
|
+
// each forwarding callsite. Indexed by callsite id. Populated in Phase 6.
|
|
36
|
+
currentLoadedFieldsAtCallsite: Map<string, FieldId[] | "full" | "unknown">;
|
|
37
|
+
|
|
38
|
+
// Exit-effect facts (Phase 6).
|
|
39
|
+
dirtyAtExit: EffectPresence;
|
|
40
|
+
currentLoadedFieldsAtExit: FieldId[] | "full" | "unknown";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ============================================================================
|
|
44
|
+
// Internal state machine
|
|
45
|
+
// ============================================================================
|
|
46
|
+
|
|
47
|
+
type Loaded = "yes" | "no" | "unknown";
|
|
48
|
+
type Dirty = "pristine" | "dirty" | "persisted" | "unknown";
|
|
49
|
+
type LoadedFields = FieldId[] | "full" | "unknown";
|
|
50
|
+
type PendingNarrow = FieldId[] | "none" | "unknown";
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Per-parameter mutable state threaded along control flow. Lists are kept sorted
|
|
54
|
+
* and unique so equality comparison + deterministic output are cheap.
|
|
55
|
+
*
|
|
56
|
+
* Conventions:
|
|
57
|
+
* - `loaded`: `"yes"` after a load/init/copyInto op or call that loads on the
|
|
58
|
+
* callee var-param side; `"unknown"` after a branch join where branches disagree.
|
|
59
|
+
* - `dirty`: lattice element from §(b) of the spec. `pristine | dirty | persisted | unknown`.
|
|
60
|
+
* - `pendingNarrow`: the pending SetLoadFields/AddLoadFields set that the NEXT
|
|
61
|
+
* Get/Find/Next would consume into `currentLoadedFields`. `"none"` means no
|
|
62
|
+
* pending narrow (next load loads everything).
|
|
63
|
+
* - `currentLoadedFields`: what's currently loaded in the record. `"full"` at init
|
|
64
|
+
* (matches AL semantics: a fresh record loads all normal fields on first Get).
|
|
65
|
+
* - `requiresLoadedAtEntry` / `mutatesBeforeLoad`: ⊔-accumulated contributions
|
|
66
|
+
* across the walk, never lowered.
|
|
67
|
+
* - `requiredFields`: raw field-name strings (case-preserved from the indexer).
|
|
68
|
+
* Cast to FieldId at output time to match the v1 walker's shape.
|
|
69
|
+
* `"unknown"` absorbs.
|
|
70
|
+
*/
|
|
71
|
+
interface PerParamState {
|
|
72
|
+
loaded: Loaded;
|
|
73
|
+
dirty: Dirty;
|
|
74
|
+
pendingNarrow: PendingNarrow;
|
|
75
|
+
currentLoadedFields: LoadedFields;
|
|
76
|
+
requiresLoadedAtEntry: EffectPresence;
|
|
77
|
+
mutatesBeforeLoad: EffectPresence;
|
|
78
|
+
requiredFields: Set<string> | "unknown";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function initialState(): PerParamState {
|
|
82
|
+
return {
|
|
83
|
+
loaded: "no",
|
|
84
|
+
dirty: "pristine",
|
|
85
|
+
pendingNarrow: "none",
|
|
86
|
+
currentLoadedFields: "full",
|
|
87
|
+
requiresLoadedAtEntry: "no",
|
|
88
|
+
mutatesBeforeLoad: "no",
|
|
89
|
+
requiredFields: new Set(),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ----- lattice joins --------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
function joinLoaded(a: Loaded, b: Loaded): Loaded {
|
|
96
|
+
if (a === b) return a;
|
|
97
|
+
return "unknown";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function joinDirty(a: Dirty, b: Dirty): Dirty {
|
|
101
|
+
if (a === b) return a;
|
|
102
|
+
// "dirty" dominates anything else (sound: at least one path is dirty).
|
|
103
|
+
if (a === "dirty" || b === "dirty") return "dirty";
|
|
104
|
+
// otherwise unknown (mixed pristine/persisted/unknown).
|
|
105
|
+
return "unknown";
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function joinPending(a: PendingNarrow, b: PendingNarrow): PendingNarrow {
|
|
109
|
+
if (a === "unknown" || b === "unknown") return "unknown";
|
|
110
|
+
if (a === "none" && b === "none") return "none";
|
|
111
|
+
if (a === "none" || b === "none") return "unknown";
|
|
112
|
+
// Both are lists — they must agree or join to unknown.
|
|
113
|
+
if (a.length !== b.length) return "unknown";
|
|
114
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return "unknown";
|
|
115
|
+
return a;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function joinLoadedFields(a: LoadedFields, b: LoadedFields): LoadedFields {
|
|
119
|
+
if (a === "unknown" || b === "unknown") return "unknown";
|
|
120
|
+
if (a === "full" && b === "full") return "full";
|
|
121
|
+
if (a === "full" || b === "full") return "unknown";
|
|
122
|
+
// Both are lists — equal lists merge; differing -> unknown (sound, less precise).
|
|
123
|
+
if (a.length !== b.length) return "unknown";
|
|
124
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return "unknown";
|
|
125
|
+
return a;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function joinRequiredFields(
|
|
129
|
+
a: Set<string> | "unknown",
|
|
130
|
+
b: Set<string> | "unknown",
|
|
131
|
+
): Set<string> | "unknown" {
|
|
132
|
+
if (a === "unknown" || b === "unknown") return "unknown";
|
|
133
|
+
const out = new Set<string>(a);
|
|
134
|
+
for (const f of b) out.add(f);
|
|
135
|
+
return out;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function joinStates(a: PerParamState, b: PerParamState): PerParamState {
|
|
139
|
+
return {
|
|
140
|
+
loaded: joinLoaded(a.loaded, b.loaded),
|
|
141
|
+
dirty: joinDirty(a.dirty, b.dirty),
|
|
142
|
+
pendingNarrow: joinPending(a.pendingNarrow, b.pendingNarrow),
|
|
143
|
+
currentLoadedFields: joinLoadedFields(a.currentLoadedFields, b.currentLoadedFields),
|
|
144
|
+
requiresLoadedAtEntry: joinPresence(a.requiresLoadedAtEntry, b.requiresLoadedAtEntry),
|
|
145
|
+
mutatesBeforeLoad: joinPresence(a.mutatesBeforeLoad, b.mutatesBeforeLoad),
|
|
146
|
+
requiredFields: joinRequiredFields(a.requiredFields, b.requiredFields),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function statesEqual(a: PerParamState, b: PerParamState): boolean {
|
|
151
|
+
if (a.loaded !== b.loaded) return false;
|
|
152
|
+
if (a.dirty !== b.dirty) return false;
|
|
153
|
+
if (a.requiresLoadedAtEntry !== b.requiresLoadedAtEntry) return false;
|
|
154
|
+
if (a.mutatesBeforeLoad !== b.mutatesBeforeLoad) return false;
|
|
155
|
+
if (!pendingEqual(a.pendingNarrow, b.pendingNarrow)) return false;
|
|
156
|
+
if (!loadedFieldsEqual(a.currentLoadedFields, b.currentLoadedFields)) return false;
|
|
157
|
+
if (!requiredFieldsEqual(a.requiredFields, b.requiredFields)) return false;
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function pendingEqual(a: PendingNarrow, b: PendingNarrow): boolean {
|
|
162
|
+
if (a === b) return true;
|
|
163
|
+
if (typeof a === "string" || typeof b === "string") return false;
|
|
164
|
+
if (a.length !== b.length) return false;
|
|
165
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function loadedFieldsEqual(a: LoadedFields, b: LoadedFields): boolean {
|
|
170
|
+
if (a === b) return true;
|
|
171
|
+
if (typeof a === "string" || typeof b === "string") return false;
|
|
172
|
+
if (a.length !== b.length) return false;
|
|
173
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function requiredFieldsEqual(a: Set<string> | "unknown", b: Set<string> | "unknown"): boolean {
|
|
178
|
+
if (a === "unknown" && b === "unknown") return true;
|
|
179
|
+
if (a === "unknown" || b === "unknown") return false;
|
|
180
|
+
if (a.size !== b.size) return false;
|
|
181
|
+
for (const f of a) if (!b.has(f)) return false;
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Saturate all "decidable" fields to unknown — used by:
|
|
187
|
+
* - bounded loop overshoot (state did not converge),
|
|
188
|
+
* - opaque callee on a var-param (cannot reason about effect),
|
|
189
|
+
* - "other" / unrecognised statement nodes (conservative).
|
|
190
|
+
*
|
|
191
|
+
* Accumulated entry-requirement contributions (`requiresLoadedAtEntry`,
|
|
192
|
+
* `mutatesBeforeLoad`, `requiredFields`) are NOT lowered — they're monotone
|
|
193
|
+
* lattice elements that only grow.
|
|
194
|
+
*/
|
|
195
|
+
function saturateUnknown(s: PerParamState): PerParamState {
|
|
196
|
+
return {
|
|
197
|
+
loaded: "unknown",
|
|
198
|
+
dirty: "unknown",
|
|
199
|
+
pendingNarrow: "unknown",
|
|
200
|
+
currentLoadedFields: "unknown",
|
|
201
|
+
requiresLoadedAtEntry: s.requiresLoadedAtEntry,
|
|
202
|
+
mutatesBeforeLoad: s.mutatesBeforeLoad,
|
|
203
|
+
requiredFields: "unknown",
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ============================================================================
|
|
208
|
+
// Walker entry point
|
|
209
|
+
// ============================================================================
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Walk the routine's body with per-parameter state tracking.
|
|
213
|
+
*
|
|
214
|
+
* Phase 6 implementation: recursive AL-statement-tree walker using
|
|
215
|
+
* `routine.features.statementTree` (populated in P6.T1). Maintains
|
|
216
|
+
* branch-aware state per record-parameter, joining state-sets at
|
|
217
|
+
* `if`/`case`/loop joins. Bounded loop fixed-point (max 3) with
|
|
218
|
+
* `"unknown"`-saturation on overshoot.
|
|
219
|
+
*
|
|
220
|
+
* Falls back to a single straight-line pass over `recordOperations` +
|
|
221
|
+
* `fieldAccesses` + `callSites` when `statementTree` is absent — keeps the
|
|
222
|
+
* walker usable on routines whose body wasn't indexed with the CFN builder
|
|
223
|
+
* (e.g. older fixtures, parse-incomplete bodies that still expose flat features).
|
|
224
|
+
*
|
|
225
|
+
* When `lookup` is provided, the walker uses it for callee summaries during the
|
|
226
|
+
* fixed-point (current iteration). When absent, falls back to `routine.summary`
|
|
227
|
+
* on the callee.
|
|
228
|
+
*/
|
|
229
|
+
export function walkRoutine(
|
|
230
|
+
routine: Routine,
|
|
231
|
+
ctx: SummaryContext,
|
|
232
|
+
lookup?: (id: RoutineId) => RoutineSummary | undefined,
|
|
233
|
+
): PathAwareFacts[] {
|
|
234
|
+
const facts: PathAwareFacts[] = [];
|
|
235
|
+
|
|
236
|
+
for (const param of routine.parameters) {
|
|
237
|
+
if (!param.isRecord) continue;
|
|
238
|
+
|
|
239
|
+
const recVar = routine.features.recordVariables.find(
|
|
240
|
+
(rv) => rv.isParameter && rv.parameterIndex === param.index,
|
|
241
|
+
);
|
|
242
|
+
const recVarNameLc = recVar?.name.toLowerCase() ?? param.name.toLowerCase();
|
|
243
|
+
const recVarId = recVar?.id;
|
|
244
|
+
const paramCtx: ParamCtx = { index: param.index, nameLc: recVarNameLc, recVarId };
|
|
245
|
+
|
|
246
|
+
// Per-walk snapshots collected at every forwarding callsite for this param.
|
|
247
|
+
const callsiteSnapshots = new Map<CallsiteId, LoadedFields>();
|
|
248
|
+
// Exit states collected at every `exit`/`error` reached by the walker.
|
|
249
|
+
const exitStates: PerParamState[] = [];
|
|
250
|
+
|
|
251
|
+
const tree = routine.features.statementTree;
|
|
252
|
+
const initial = initialState();
|
|
253
|
+
|
|
254
|
+
let finalState: PerParamState;
|
|
255
|
+
if (tree !== undefined) {
|
|
256
|
+
const opIndex = indexOps(routine);
|
|
257
|
+
const callIndex = indexCalls(routine);
|
|
258
|
+
const faIndex = indexFieldAccesses(routine);
|
|
259
|
+
finalState = walkCFG(
|
|
260
|
+
tree,
|
|
261
|
+
initial,
|
|
262
|
+
paramCtx,
|
|
263
|
+
routine,
|
|
264
|
+
ctx,
|
|
265
|
+
lookup,
|
|
266
|
+
exitStates,
|
|
267
|
+
callsiteSnapshots,
|
|
268
|
+
opIndex,
|
|
269
|
+
callIndex,
|
|
270
|
+
faIndex,
|
|
271
|
+
);
|
|
272
|
+
} else {
|
|
273
|
+
// No statement tree — straight-line fallback using flat features.
|
|
274
|
+
finalState = walkFlat(routine, paramCtx, ctx, lookup, initial, callsiteSnapshots);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Aggregate exit + fallthrough states for the exit-effect facts.
|
|
278
|
+
const allExitStates: PerParamState[] = [...exitStates, finalState];
|
|
279
|
+
const dirtyAtExit = computeDirtyAtExit(allExitStates);
|
|
280
|
+
const currentLoadedFieldsAtExit = computeCurrentLoadedAtExit(allExitStates);
|
|
281
|
+
|
|
282
|
+
// Entry-requirement facts: take the JOIN across all exit + fallthrough states
|
|
283
|
+
// — every path's contribution counts. (These fields are accumulator-style so
|
|
284
|
+
// joinPresence yields the same as the JOIN of contributions on every reached
|
|
285
|
+
// exit state.)
|
|
286
|
+
let requires: EffectPresence = "no";
|
|
287
|
+
let mutates: EffectPresence = "no";
|
|
288
|
+
let required: Set<string> | "unknown" = new Set();
|
|
289
|
+
for (const s of allExitStates) {
|
|
290
|
+
requires = joinPresence(requires, s.requiresLoadedAtEntry);
|
|
291
|
+
mutates = joinPresence(mutates, s.mutatesBeforeLoad);
|
|
292
|
+
required = joinRequiredFields(required, s.requiredFields);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const requiredFieldsFinal: FieldId[] | "unknown" =
|
|
296
|
+
required === "unknown" ? "unknown" : ([...required].sort() as FieldId[]);
|
|
297
|
+
|
|
298
|
+
facts.push({
|
|
299
|
+
parameterIndex: param.index,
|
|
300
|
+
requiresLoadedAtEntry: requires,
|
|
301
|
+
requiredLoadedFieldsAtEntry: requiredFieldsFinal,
|
|
302
|
+
mutatesBeforeLoad: mutates,
|
|
303
|
+
currentLoadedFieldsAtCallsite: callsiteSnapshots,
|
|
304
|
+
dirtyAtExit,
|
|
305
|
+
currentLoadedFieldsAtExit,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
return facts;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// ============================================================================
|
|
312
|
+
// Recursive walker
|
|
313
|
+
// ============================================================================
|
|
314
|
+
|
|
315
|
+
interface ParamCtx {
|
|
316
|
+
index: number;
|
|
317
|
+
nameLc: string;
|
|
318
|
+
recVarId: string | undefined;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
type OpIndex = Map<string, RecordOperation>;
|
|
322
|
+
type CallIndex = Map<string, CallSite>;
|
|
323
|
+
type FaIndex = Map<string, FieldAccess[]>; // keyed by `${line}:${column}`
|
|
324
|
+
|
|
325
|
+
function indexOps(routine: Routine): OpIndex {
|
|
326
|
+
const m: OpIndex = new Map();
|
|
327
|
+
for (const op of routine.features.recordOperations) m.set(op.id, op);
|
|
328
|
+
return m;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function indexCalls(routine: Routine): CallIndex {
|
|
332
|
+
const m: CallIndex = new Map();
|
|
333
|
+
for (const cs of routine.features.callSites) m.set(cs.id, cs);
|
|
334
|
+
return m;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Field accesses are not represented as their own CFN leaves (the indexer
|
|
339
|
+
* doesn't anchor them in the statement tree). To attribute field reads to
|
|
340
|
+
* the correct CFN position, we index every FA by `(line, column)` of its
|
|
341
|
+
* source anchor. Each block-walker call (`case "block"` in `walkCFG`) then
|
|
342
|
+
* picks the FAs whose source position falls inside the block but NOT inside
|
|
343
|
+
* any RECURSIVE child (block / if / case / case-branch / loops / try /
|
|
344
|
+
* "other"-with-children) — those recursive children will attribute them
|
|
345
|
+
* themselves. FAs inside a non-recursive leaf (op / call / exit / error /
|
|
346
|
+
* leaf "other") are correctly attributed by the enclosing block.
|
|
347
|
+
*
|
|
348
|
+
* This yields branch-aware attribution: e.g. an FA inside the `then` branch
|
|
349
|
+
* of an `if` is walked by the `if`'s then-branch block, so its contribution
|
|
350
|
+
* is joined with the else-branch contribution at the `if` join point.
|
|
351
|
+
*/
|
|
352
|
+
function indexFieldAccesses(routine: Routine): FaIndex {
|
|
353
|
+
const m: FaIndex = new Map();
|
|
354
|
+
for (const fa of routine.features.fieldAccesses) {
|
|
355
|
+
const key = `${fa.sourceAnchor.range.startLine}:${fa.sourceAnchor.range.startColumn}`;
|
|
356
|
+
const list = m.get(key);
|
|
357
|
+
if (list !== undefined) list.push(fa);
|
|
358
|
+
else m.set(key, [fa]);
|
|
359
|
+
}
|
|
360
|
+
return m;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function walkCFG(
|
|
364
|
+
node: ControlFlowNode,
|
|
365
|
+
pre: PerParamState,
|
|
366
|
+
param: ParamCtx,
|
|
367
|
+
routine: Routine,
|
|
368
|
+
ctx: SummaryContext,
|
|
369
|
+
lookup: ((id: RoutineId) => RoutineSummary | undefined) | undefined,
|
|
370
|
+
exitStates: PerParamState[],
|
|
371
|
+
snapshots: Map<CallsiteId, LoadedFields>,
|
|
372
|
+
opIndex: OpIndex,
|
|
373
|
+
callIndex: CallIndex,
|
|
374
|
+
faIndex: FaIndex,
|
|
375
|
+
): PerParamState {
|
|
376
|
+
switch (node.kind) {
|
|
377
|
+
case "block": {
|
|
378
|
+
let state = pre;
|
|
379
|
+
// Interleave field-access events with child CFN nodes by source position.
|
|
380
|
+
// A field read like `Cust.Name` fires at its own line, NOT at the block
|
|
381
|
+
// entry. This matters when an op (e.g. `Cust.Get(...)`) precedes the FA
|
|
382
|
+
// in the same block: the Get walks first → loaded=yes → the FA no
|
|
383
|
+
// longer contributes to requiresLoadedAtEntry.
|
|
384
|
+
const children = node.children ?? [];
|
|
385
|
+
const fas = collectFieldAccessesInBlock(node, children, param, faIndex);
|
|
386
|
+
const events: Array<
|
|
387
|
+
| { kind: "child"; node: ControlFlowNode; line: number; col: number }
|
|
388
|
+
| { kind: "fa"; fa: FieldAccess; line: number; col: number }
|
|
389
|
+
> = [];
|
|
390
|
+
for (const c of children) {
|
|
391
|
+
events.push({
|
|
392
|
+
kind: "child",
|
|
393
|
+
node: c,
|
|
394
|
+
line: c.sourceAnchor.range.startLine,
|
|
395
|
+
col: c.sourceAnchor.range.startColumn,
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
for (const fa of fas) {
|
|
399
|
+
events.push({
|
|
400
|
+
kind: "fa",
|
|
401
|
+
fa,
|
|
402
|
+
line: fa.sourceAnchor.range.startLine,
|
|
403
|
+
col: fa.sourceAnchor.range.startColumn,
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
events.sort((a, b) => (a.line !== b.line ? a.line - b.line : a.col - b.col));
|
|
407
|
+
for (const e of events) {
|
|
408
|
+
if (e.kind === "child") {
|
|
409
|
+
state = walkCFG(
|
|
410
|
+
e.node,
|
|
411
|
+
state,
|
|
412
|
+
param,
|
|
413
|
+
routine,
|
|
414
|
+
ctx,
|
|
415
|
+
lookup,
|
|
416
|
+
exitStates,
|
|
417
|
+
snapshots,
|
|
418
|
+
opIndex,
|
|
419
|
+
callIndex,
|
|
420
|
+
faIndex,
|
|
421
|
+
);
|
|
422
|
+
} else {
|
|
423
|
+
state = applyFieldRead(state, e.fa);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return state;
|
|
427
|
+
}
|
|
428
|
+
case "if": {
|
|
429
|
+
// P7.5: condition-position ops/calls evaluate BEFORE branch selection.
|
|
430
|
+
const preBranch = applyConditionLeaves(
|
|
431
|
+
pre,
|
|
432
|
+
node.conditionLeaves,
|
|
433
|
+
param,
|
|
434
|
+
routine,
|
|
435
|
+
ctx,
|
|
436
|
+
lookup,
|
|
437
|
+
exitStates,
|
|
438
|
+
snapshots,
|
|
439
|
+
opIndex,
|
|
440
|
+
callIndex,
|
|
441
|
+
faIndex,
|
|
442
|
+
);
|
|
443
|
+
const thenBranch = node.children?.[0];
|
|
444
|
+
const elseBranch = node.elseChildren?.[0];
|
|
445
|
+
const thenState =
|
|
446
|
+
thenBranch !== undefined
|
|
447
|
+
? walkCFG(
|
|
448
|
+
thenBranch,
|
|
449
|
+
preBranch,
|
|
450
|
+
param,
|
|
451
|
+
routine,
|
|
452
|
+
ctx,
|
|
453
|
+
lookup,
|
|
454
|
+
exitStates,
|
|
455
|
+
snapshots,
|
|
456
|
+
opIndex,
|
|
457
|
+
callIndex,
|
|
458
|
+
faIndex,
|
|
459
|
+
)
|
|
460
|
+
: preBranch;
|
|
461
|
+
const elseState =
|
|
462
|
+
elseBranch !== undefined
|
|
463
|
+
? walkCFG(
|
|
464
|
+
elseBranch,
|
|
465
|
+
preBranch,
|
|
466
|
+
param,
|
|
467
|
+
routine,
|
|
468
|
+
ctx,
|
|
469
|
+
lookup,
|
|
470
|
+
exitStates,
|
|
471
|
+
snapshots,
|
|
472
|
+
opIndex,
|
|
473
|
+
callIndex,
|
|
474
|
+
faIndex,
|
|
475
|
+
)
|
|
476
|
+
: preBranch; // missing else = the no-match path = preBranch.
|
|
477
|
+
return joinStates(thenState, elseState);
|
|
478
|
+
}
|
|
479
|
+
case "case": {
|
|
480
|
+
// Walk each case-branch from `pre`; join all post-states.
|
|
481
|
+
// case-else-branch is one of the children (the CFN builder uses the
|
|
482
|
+
// same `case-branch` kind for both, since the lattice doesn't care).
|
|
483
|
+
// If the case has no `else`, the no-match path = pre, which we include
|
|
484
|
+
// in the join (spec §(b): "missing default = pre-state").
|
|
485
|
+
//
|
|
486
|
+
// P7.5: case-value expression evaluates ONCE before branch selection.
|
|
487
|
+
const preBranch = applyConditionLeaves(
|
|
488
|
+
pre,
|
|
489
|
+
node.conditionLeaves,
|
|
490
|
+
param,
|
|
491
|
+
routine,
|
|
492
|
+
ctx,
|
|
493
|
+
lookup,
|
|
494
|
+
exitStates,
|
|
495
|
+
snapshots,
|
|
496
|
+
opIndex,
|
|
497
|
+
callIndex,
|
|
498
|
+
faIndex,
|
|
499
|
+
);
|
|
500
|
+
const branches = node.children ?? [];
|
|
501
|
+
let hasElse = false;
|
|
502
|
+
let acc: PerParamState | undefined;
|
|
503
|
+
for (const c of branches) {
|
|
504
|
+
if (c.sourceAnchor.syntaxKind === "case_else_branch") hasElse = true;
|
|
505
|
+
const post = walkCFG(
|
|
506
|
+
c,
|
|
507
|
+
preBranch,
|
|
508
|
+
param,
|
|
509
|
+
routine,
|
|
510
|
+
ctx,
|
|
511
|
+
lookup,
|
|
512
|
+
exitStates,
|
|
513
|
+
snapshots,
|
|
514
|
+
opIndex,
|
|
515
|
+
callIndex,
|
|
516
|
+
faIndex,
|
|
517
|
+
);
|
|
518
|
+
acc = acc === undefined ? post : joinStates(acc, post);
|
|
519
|
+
}
|
|
520
|
+
if (acc === undefined) return preBranch; // empty case
|
|
521
|
+
if (!hasElse) acc = joinStates(acc, preBranch);
|
|
522
|
+
return acc;
|
|
523
|
+
}
|
|
524
|
+
case "case-branch": {
|
|
525
|
+
// case-branch wraps a single body block as its first child.
|
|
526
|
+
let state = pre;
|
|
527
|
+
for (const c of node.children ?? []) {
|
|
528
|
+
state = walkCFG(
|
|
529
|
+
c,
|
|
530
|
+
state,
|
|
531
|
+
param,
|
|
532
|
+
routine,
|
|
533
|
+
ctx,
|
|
534
|
+
lookup,
|
|
535
|
+
exitStates,
|
|
536
|
+
snapshots,
|
|
537
|
+
opIndex,
|
|
538
|
+
callIndex,
|
|
539
|
+
faIndex,
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
return state;
|
|
543
|
+
}
|
|
544
|
+
case "while":
|
|
545
|
+
case "for":
|
|
546
|
+
case "foreach": {
|
|
547
|
+
// Bounded fixed-point: walk body up to 3 times; if state isn't yet
|
|
548
|
+
// converged, saturate decidable lattice fields to "unknown" but keep
|
|
549
|
+
// accumulated entry-requirement contributions.
|
|
550
|
+
//
|
|
551
|
+
// Body iteration semantics: at the start of each iteration we are at
|
|
552
|
+
// the join of (pre, previous body post). The body itself may use
|
|
553
|
+
// `exit`/`error` — those snapshots are captured into `exitStates`.
|
|
554
|
+
//
|
|
555
|
+
// P7.5: condition / range / iterable expression-position ops fire BEFORE
|
|
556
|
+
// each body iteration (pre-condition test). The fixed-point naturally
|
|
557
|
+
// folds them into each iteration: we apply conditionLeaves first, then
|
|
558
|
+
// the body. The post-loop "loop did not run" path also requires the
|
|
559
|
+
// condition to have been evaluated at least once.
|
|
560
|
+
const bodyNode = node.children?.[0];
|
|
561
|
+
// Apply the (zero-or-many) condition leaves once for the "loop ran zero
|
|
562
|
+
// times" path: even if the body never runs, the condition was tested
|
|
563
|
+
// (whether true or false). This ensures the loaded-after-cond effect
|
|
564
|
+
// reaches the post-loop state.
|
|
565
|
+
const preCond = applyConditionLeaves(
|
|
566
|
+
pre,
|
|
567
|
+
node.conditionLeaves,
|
|
568
|
+
param,
|
|
569
|
+
routine,
|
|
570
|
+
ctx,
|
|
571
|
+
lookup,
|
|
572
|
+
exitStates,
|
|
573
|
+
snapshots,
|
|
574
|
+
opIndex,
|
|
575
|
+
callIndex,
|
|
576
|
+
faIndex,
|
|
577
|
+
);
|
|
578
|
+
if (bodyNode === undefined) return preCond;
|
|
579
|
+
let bodyPre = preCond;
|
|
580
|
+
for (let i = 0; i < 3; i++) {
|
|
581
|
+
const bodyPost = walkCFG(
|
|
582
|
+
bodyNode,
|
|
583
|
+
bodyPre,
|
|
584
|
+
param,
|
|
585
|
+
routine,
|
|
586
|
+
ctx,
|
|
587
|
+
lookup,
|
|
588
|
+
exitStates,
|
|
589
|
+
snapshots,
|
|
590
|
+
opIndex,
|
|
591
|
+
callIndex,
|
|
592
|
+
faIndex,
|
|
593
|
+
);
|
|
594
|
+
// Re-apply condition leaves at start of next iteration (pre-condition
|
|
595
|
+
// loops test before each iteration's body).
|
|
596
|
+
const nextIterPre = applyConditionLeaves(
|
|
597
|
+
bodyPost,
|
|
598
|
+
node.conditionLeaves,
|
|
599
|
+
param,
|
|
600
|
+
routine,
|
|
601
|
+
ctx,
|
|
602
|
+
lookup,
|
|
603
|
+
exitStates,
|
|
604
|
+
snapshots,
|
|
605
|
+
opIndex,
|
|
606
|
+
callIndex,
|
|
607
|
+
faIndex,
|
|
608
|
+
);
|
|
609
|
+
const joined = joinStates(bodyPre, nextIterPre);
|
|
610
|
+
if (statesEqual(joined, bodyPre)) {
|
|
611
|
+
return joined;
|
|
612
|
+
}
|
|
613
|
+
bodyPre = joined;
|
|
614
|
+
}
|
|
615
|
+
return saturateUnknown(bodyPre);
|
|
616
|
+
}
|
|
617
|
+
case "repeat": {
|
|
618
|
+
// repeat ... until: body executes at least once, condition tested AFTER body.
|
|
619
|
+
// P7.5: the until-expression's ops fire at the END of each iteration.
|
|
620
|
+
const bodyNode = node.children?.[0];
|
|
621
|
+
if (bodyNode === undefined) {
|
|
622
|
+
// No body — just apply the until-condition (degenerate; grammar shouldn't
|
|
623
|
+
// produce this, but stay safe).
|
|
624
|
+
return applyConditionLeaves(
|
|
625
|
+
pre,
|
|
626
|
+
node.conditionLeaves,
|
|
627
|
+
param,
|
|
628
|
+
routine,
|
|
629
|
+
ctx,
|
|
630
|
+
lookup,
|
|
631
|
+
exitStates,
|
|
632
|
+
snapshots,
|
|
633
|
+
opIndex,
|
|
634
|
+
callIndex,
|
|
635
|
+
faIndex,
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
let bodyPre = pre;
|
|
639
|
+
for (let i = 0; i < 3; i++) {
|
|
640
|
+
const bodyPost = walkCFG(
|
|
641
|
+
bodyNode,
|
|
642
|
+
bodyPre,
|
|
643
|
+
param,
|
|
644
|
+
routine,
|
|
645
|
+
ctx,
|
|
646
|
+
lookup,
|
|
647
|
+
exitStates,
|
|
648
|
+
snapshots,
|
|
649
|
+
opIndex,
|
|
650
|
+
callIndex,
|
|
651
|
+
faIndex,
|
|
652
|
+
);
|
|
653
|
+
// Apply until-condition leaves AFTER body (post-condition semantics).
|
|
654
|
+
const afterCond = applyConditionLeaves(
|
|
655
|
+
bodyPost,
|
|
656
|
+
node.conditionLeaves,
|
|
657
|
+
param,
|
|
658
|
+
routine,
|
|
659
|
+
ctx,
|
|
660
|
+
lookup,
|
|
661
|
+
exitStates,
|
|
662
|
+
snapshots,
|
|
663
|
+
opIndex,
|
|
664
|
+
callIndex,
|
|
665
|
+
faIndex,
|
|
666
|
+
);
|
|
667
|
+
const joined = joinStates(bodyPre, afterCond);
|
|
668
|
+
if (statesEqual(joined, bodyPre)) {
|
|
669
|
+
return joined;
|
|
670
|
+
}
|
|
671
|
+
bodyPre = joined;
|
|
672
|
+
}
|
|
673
|
+
return saturateUnknown(bodyPre);
|
|
674
|
+
}
|
|
675
|
+
case "exit": {
|
|
676
|
+
exitStates.push(pre);
|
|
677
|
+
return pre;
|
|
678
|
+
}
|
|
679
|
+
case "error": {
|
|
680
|
+
// P7.5: any argument-position ops on a bare Error(...) call still
|
|
681
|
+
// execute before the Error exits (argument evaluation precedes call).
|
|
682
|
+
const post = applyConditionLeaves(
|
|
683
|
+
pre,
|
|
684
|
+
node.conditionLeaves,
|
|
685
|
+
param,
|
|
686
|
+
routine,
|
|
687
|
+
ctx,
|
|
688
|
+
lookup,
|
|
689
|
+
exitStates,
|
|
690
|
+
snapshots,
|
|
691
|
+
opIndex,
|
|
692
|
+
callIndex,
|
|
693
|
+
faIndex,
|
|
694
|
+
);
|
|
695
|
+
exitStates.push(post);
|
|
696
|
+
return post;
|
|
697
|
+
}
|
|
698
|
+
case "op": {
|
|
699
|
+
// P7.5: nested ops in arguments evaluate BEFORE this op (`foo(bar.X())`).
|
|
700
|
+
const preOp = applyConditionLeaves(
|
|
701
|
+
pre,
|
|
702
|
+
node.conditionLeaves,
|
|
703
|
+
param,
|
|
704
|
+
routine,
|
|
705
|
+
ctx,
|
|
706
|
+
lookup,
|
|
707
|
+
exitStates,
|
|
708
|
+
snapshots,
|
|
709
|
+
opIndex,
|
|
710
|
+
callIndex,
|
|
711
|
+
faIndex,
|
|
712
|
+
);
|
|
713
|
+
const op = opIndex.get(node.operationId ?? "");
|
|
714
|
+
if (op === undefined) return preOp;
|
|
715
|
+
return applyOp(preOp, op, param);
|
|
716
|
+
}
|
|
717
|
+
case "call": {
|
|
718
|
+
// P7.5: argument-position ops evaluate BEFORE the call applies.
|
|
719
|
+
const preCall = applyConditionLeaves(
|
|
720
|
+
pre,
|
|
721
|
+
node.conditionLeaves,
|
|
722
|
+
param,
|
|
723
|
+
routine,
|
|
724
|
+
ctx,
|
|
725
|
+
lookup,
|
|
726
|
+
exitStates,
|
|
727
|
+
snapshots,
|
|
728
|
+
opIndex,
|
|
729
|
+
callIndex,
|
|
730
|
+
faIndex,
|
|
731
|
+
);
|
|
732
|
+
const cs = callIndex.get(node.callsiteId ?? "");
|
|
733
|
+
if (cs === undefined) return preCall;
|
|
734
|
+
return applyCall(preCall, cs, param, ctx, lookup, snapshots);
|
|
735
|
+
}
|
|
736
|
+
case "try": {
|
|
737
|
+
// AL grammar currently does not expose a try_statement, so children
|
|
738
|
+
// is empty. Treat as opaque-with-possible-exit: snapshot a sat-unknown
|
|
739
|
+
// exit state and return saturated.
|
|
740
|
+
const sat = saturateUnknown(pre);
|
|
741
|
+
exitStates.push(sat);
|
|
742
|
+
return sat;
|
|
743
|
+
}
|
|
744
|
+
default: {
|
|
745
|
+
// "other" (and any future unrecognised kind) wraps assignment / message /
|
|
746
|
+
// with / asserterror / etc. Recursively
|
|
747
|
+
// walk any children (e.g. `with_statement` body) so embedded ops/calls
|
|
748
|
+
// inside the wrapped statement still affect state.
|
|
749
|
+
// P7.5: also apply any argument-position leaves harvested by the indexer
|
|
750
|
+
// (e.g. a call_expression statement whose function is unresolved but whose
|
|
751
|
+
// arguments contain a record-op).
|
|
752
|
+
let state = applyConditionLeaves(
|
|
753
|
+
pre,
|
|
754
|
+
node.conditionLeaves,
|
|
755
|
+
param,
|
|
756
|
+
routine,
|
|
757
|
+
ctx,
|
|
758
|
+
lookup,
|
|
759
|
+
exitStates,
|
|
760
|
+
snapshots,
|
|
761
|
+
opIndex,
|
|
762
|
+
callIndex,
|
|
763
|
+
faIndex,
|
|
764
|
+
);
|
|
765
|
+
for (const c of node.children ?? []) {
|
|
766
|
+
state = walkCFG(
|
|
767
|
+
c,
|
|
768
|
+
state,
|
|
769
|
+
param,
|
|
770
|
+
routine,
|
|
771
|
+
ctx,
|
|
772
|
+
lookup,
|
|
773
|
+
exitStates,
|
|
774
|
+
snapshots,
|
|
775
|
+
opIndex,
|
|
776
|
+
callIndex,
|
|
777
|
+
faIndex,
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
return state;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* P7.5: process a node's harvested expression-position leaves (`conditionLeaves`)
|
|
787
|
+
* in source order. Each leaf is an `op` / `call` / `error` ControlFlowNode produced
|
|
788
|
+
* by the indexer's `harvestExpressionLeaves`. We re-enter `walkCFG` for each leaf so
|
|
789
|
+
* nested argument leaves (e.g. `Helper(Cust.FindSet())` → call leaf with op
|
|
790
|
+
* conditionLeaves) propagate correctly.
|
|
791
|
+
*
|
|
792
|
+
* The CALLER controls timing: pre-body (if/case/while/for/foreach), post-body
|
|
793
|
+
* (repeat), or pre-effect (op/call/error/other-with-args). This helper is purely
|
|
794
|
+
* a sequencer — it doesn't decide.
|
|
795
|
+
*/
|
|
796
|
+
function applyConditionLeaves(
|
|
797
|
+
pre: PerParamState,
|
|
798
|
+
leaves: ControlFlowNode[] | undefined,
|
|
799
|
+
param: ParamCtx,
|
|
800
|
+
routine: Routine,
|
|
801
|
+
ctx: SummaryContext,
|
|
802
|
+
lookup: ((id: RoutineId) => RoutineSummary | undefined) | undefined,
|
|
803
|
+
exitStates: PerParamState[],
|
|
804
|
+
snapshots: Map<CallsiteId, LoadedFields>,
|
|
805
|
+
opIndex: OpIndex,
|
|
806
|
+
callIndex: CallIndex,
|
|
807
|
+
faIndex: FaIndex,
|
|
808
|
+
): PerParamState {
|
|
809
|
+
if (leaves === undefined || leaves.length === 0) return pre;
|
|
810
|
+
let state = pre;
|
|
811
|
+
for (const leaf of leaves) {
|
|
812
|
+
state = walkCFG(
|
|
813
|
+
leaf,
|
|
814
|
+
state,
|
|
815
|
+
param,
|
|
816
|
+
routine,
|
|
817
|
+
ctx,
|
|
818
|
+
lookup,
|
|
819
|
+
exitStates,
|
|
820
|
+
snapshots,
|
|
821
|
+
opIndex,
|
|
822
|
+
callIndex,
|
|
823
|
+
faIndex,
|
|
824
|
+
);
|
|
825
|
+
}
|
|
826
|
+
return state;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// ============================================================================
|
|
830
|
+
// Op + call application
|
|
831
|
+
// ============================================================================
|
|
832
|
+
|
|
833
|
+
function opAffectsParam(op: RecordOperation, param: ParamCtx): boolean {
|
|
834
|
+
if (param.recVarId !== undefined && op.recordVariableId === param.recVarId) return true;
|
|
835
|
+
return op.recordVariableName.toLowerCase() === param.nameLc;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
function applyOp(state: PerParamState, op: RecordOperation, param: ParamCtx): PerParamState {
|
|
839
|
+
if (!opAffectsParam(op, param)) return state;
|
|
840
|
+
const role = recordFlowRoleOf(op.op);
|
|
841
|
+
const out: PerParamState = {
|
|
842
|
+
...state,
|
|
843
|
+
requiredFields: state.requiredFields === "unknown" ? "unknown" : new Set(state.requiredFields),
|
|
844
|
+
};
|
|
845
|
+
switch (role) {
|
|
846
|
+
case "loadsFromDb": {
|
|
847
|
+
out.loaded = "yes";
|
|
848
|
+
if (out.pendingNarrow === "unknown") {
|
|
849
|
+
out.currentLoadedFields = "unknown";
|
|
850
|
+
} else if (out.pendingNarrow === "none") {
|
|
851
|
+
out.currentLoadedFields = "full";
|
|
852
|
+
} else {
|
|
853
|
+
out.currentLoadedFields = [...out.pendingNarrow].sort() as FieldId[];
|
|
854
|
+
}
|
|
855
|
+
out.pendingNarrow = "none";
|
|
856
|
+
out.dirty = "pristine";
|
|
857
|
+
break;
|
|
858
|
+
}
|
|
859
|
+
case "initialises": {
|
|
860
|
+
out.loaded = "yes";
|
|
861
|
+
out.currentLoadedFields = "full";
|
|
862
|
+
out.pendingNarrow = "none";
|
|
863
|
+
out.dirty = "pristine";
|
|
864
|
+
break;
|
|
865
|
+
}
|
|
866
|
+
case "copiesInto": {
|
|
867
|
+
out.loaded = "yes";
|
|
868
|
+
// currentLoadedFields after Copy/TransferFields is conservatively unknown
|
|
869
|
+
// (depends on the source record), but we keep prior state — copy doesn't
|
|
870
|
+
// reduce knowledge about what's loaded.
|
|
871
|
+
out.dirty = "pristine";
|
|
872
|
+
break;
|
|
873
|
+
}
|
|
874
|
+
case "persistsCurrent": {
|
|
875
|
+
if (out.loaded !== "yes") {
|
|
876
|
+
out.requiresLoadedAtEntry = "yes";
|
|
877
|
+
out.mutatesBeforeLoad = "yes";
|
|
878
|
+
}
|
|
879
|
+
out.dirty = "persisted";
|
|
880
|
+
break;
|
|
881
|
+
}
|
|
882
|
+
case "validates": {
|
|
883
|
+
if (out.loaded !== "yes") {
|
|
884
|
+
out.requiresLoadedAtEntry = "yes";
|
|
885
|
+
out.mutatesBeforeLoad = "yes";
|
|
886
|
+
}
|
|
887
|
+
// Validate transitions to dirty (raises in-memory dirty); doesn't lower
|
|
888
|
+
// `persisted` because once persisted the Modify+Validate sequence is its
|
|
889
|
+
// own dirty cycle. Spec wording: "Validate → dirty (if was pristine) /
|
|
890
|
+
// unchanged (otherwise)". We model "unchanged" conservatively here so
|
|
891
|
+
// `persisted → Validate` stays `persisted` (downgrading to dirty would
|
|
892
|
+
// invent a may-fact); but if prior was pristine OR unknown we raise to
|
|
893
|
+
// dirty. This is the sound choice.
|
|
894
|
+
if (out.dirty === "pristine") out.dirty = "dirty";
|
|
895
|
+
else if (out.dirty === "unknown") out.dirty = "dirty";
|
|
896
|
+
break;
|
|
897
|
+
}
|
|
898
|
+
case "setBasedWrite": {
|
|
899
|
+
// ModifyAll / DeleteAll are set-based and do NOT transition the current
|
|
900
|
+
// record's dirty state. They DO require the record to be in a valid
|
|
901
|
+
// filter-state — but that's covered by D41, not the dirty lattice.
|
|
902
|
+
break;
|
|
903
|
+
}
|
|
904
|
+
case "resetsFilter": {
|
|
905
|
+
out.pendingNarrow = "none";
|
|
906
|
+
// Reset does NOT reload; currentLoadedFields unchanged.
|
|
907
|
+
break;
|
|
908
|
+
}
|
|
909
|
+
case "neutral": {
|
|
910
|
+
if (op.op === "SetLoadFields") {
|
|
911
|
+
const fields = [...new Set(op.fieldArguments ?? [])].sort();
|
|
912
|
+
out.pendingNarrow = fields as FieldId[];
|
|
913
|
+
} else if (op.op === "AddLoadFields") {
|
|
914
|
+
const additions = op.fieldArguments ?? [];
|
|
915
|
+
if (out.pendingNarrow === "unknown") {
|
|
916
|
+
// stay unknown
|
|
917
|
+
} else if (out.pendingNarrow === "none") {
|
|
918
|
+
out.pendingNarrow = [...new Set(additions)].sort() as FieldId[];
|
|
919
|
+
} else {
|
|
920
|
+
out.pendingNarrow = [
|
|
921
|
+
...new Set([...out.pendingNarrow, ...additions]),
|
|
922
|
+
].sort() as FieldId[];
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
// Other neutral ops (SetRange/SetFilter/TestField/SetCurrentKey/Count/etc.)
|
|
926
|
+
// — no state change here.
|
|
927
|
+
break;
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
return out;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
function applyCall(
|
|
934
|
+
state: PerParamState,
|
|
935
|
+
cs: CallSite,
|
|
936
|
+
param: ParamCtx,
|
|
937
|
+
ctx: SummaryContext,
|
|
938
|
+
lookup: ((id: RoutineId) => RoutineSummary | undefined) | undefined,
|
|
939
|
+
snapshots: Map<CallsiteId, LoadedFields>,
|
|
940
|
+
): PerParamState {
|
|
941
|
+
// Find the binding that forwards THIS param to the callee.
|
|
942
|
+
const binding = cs.argumentBindings.find((b) => {
|
|
943
|
+
if (b.bindingResolution !== "resolved") return false;
|
|
944
|
+
if (param.recVarId !== undefined && b.sourceRecordVariableId === param.recVarId) return true;
|
|
945
|
+
return b.sourceVariableName === param.nameLc;
|
|
946
|
+
});
|
|
947
|
+
|
|
948
|
+
// Even if no binding for this param, the call might still be a relevant snapshot
|
|
949
|
+
// for OTHER params; but we only record snapshots when this param IS forwarded.
|
|
950
|
+
if (binding === undefined) return state;
|
|
951
|
+
|
|
952
|
+
// Snapshot the current loaded fields at this callsite for D42's use.
|
|
953
|
+
snapshots.set(cs.id, state.currentLoadedFields);
|
|
954
|
+
|
|
955
|
+
const edge = ctx.resolvedCallEdgeByCallsite.get(cs.id);
|
|
956
|
+
const callee = edge?.to !== undefined ? ctx.routineById.get(edge.to) : undefined;
|
|
957
|
+
if (callee === undefined || callee.bodyAvailable === false) {
|
|
958
|
+
// Opaque callee — sound c1b: var-on-var means we lose state. Entry-req
|
|
959
|
+
// composition (c1a) is also unknown for an opaque callee — JOIN unknown.
|
|
960
|
+
const out: PerParamState = {
|
|
961
|
+
...state,
|
|
962
|
+
requiredFields:
|
|
963
|
+
state.requiredFields === "unknown" ? "unknown" : new Set(state.requiredFields),
|
|
964
|
+
};
|
|
965
|
+
if (state.loaded !== "yes") {
|
|
966
|
+
out.requiresLoadedAtEntry = joinPresence(state.requiresLoadedAtEntry, "unknown");
|
|
967
|
+
out.mutatesBeforeLoad = joinPresence(state.mutatesBeforeLoad, "unknown");
|
|
968
|
+
}
|
|
969
|
+
if (binding.callerSourceParameterIsVar && binding.calleeParameterIsVar) {
|
|
970
|
+
out.loaded = "unknown";
|
|
971
|
+
out.dirty = "unknown";
|
|
972
|
+
out.pendingNarrow = "unknown";
|
|
973
|
+
out.currentLoadedFields = "unknown";
|
|
974
|
+
}
|
|
975
|
+
return out;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
const calleeSummary = lookup !== undefined ? lookup(callee.id) : callee.summary;
|
|
979
|
+
const calleeRole = calleeSummary?.parameterRoles.find(
|
|
980
|
+
(r) => r.parameterIndex === binding.parameterIndex,
|
|
981
|
+
);
|
|
982
|
+
if (calleeRole === undefined) {
|
|
983
|
+
// Callee body available but no role for this param — treat as opaque-ish.
|
|
984
|
+
const out: PerParamState = {
|
|
985
|
+
...state,
|
|
986
|
+
requiredFields:
|
|
987
|
+
state.requiredFields === "unknown" ? "unknown" : new Set(state.requiredFields),
|
|
988
|
+
};
|
|
989
|
+
if (state.loaded !== "yes") {
|
|
990
|
+
out.requiresLoadedAtEntry = joinPresence(state.requiresLoadedAtEntry, "unknown");
|
|
991
|
+
out.mutatesBeforeLoad = joinPresence(state.mutatesBeforeLoad, "unknown");
|
|
992
|
+
}
|
|
993
|
+
if (binding.callerSourceParameterIsVar && binding.calleeParameterIsVar) {
|
|
994
|
+
out.loaded = "unknown";
|
|
995
|
+
out.dirty = "unknown";
|
|
996
|
+
out.pendingNarrow = "unknown";
|
|
997
|
+
out.currentLoadedFields = "unknown";
|
|
998
|
+
}
|
|
999
|
+
return out;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
const out: PerParamState = {
|
|
1003
|
+
...state,
|
|
1004
|
+
requiredFields: state.requiredFields === "unknown" ? "unknown" : new Set(state.requiredFields),
|
|
1005
|
+
};
|
|
1006
|
+
|
|
1007
|
+
// c1a — entry requirements compose regardless of var-ness, but only when
|
|
1008
|
+
// we haven't loaded yet on this path.
|
|
1009
|
+
if (state.loaded !== "yes") {
|
|
1010
|
+
out.requiresLoadedAtEntry = joinPresence(
|
|
1011
|
+
state.requiresLoadedAtEntry,
|
|
1012
|
+
calleeRole.requiresLoadedAtEntry,
|
|
1013
|
+
);
|
|
1014
|
+
out.mutatesBeforeLoad = joinPresence(state.mutatesBeforeLoad, calleeRole.mutatesBeforeLoad);
|
|
1015
|
+
if (out.requiredFields !== "unknown") {
|
|
1016
|
+
if (calleeRole.requiredLoadedFieldsAtEntry === "unknown") {
|
|
1017
|
+
out.requiredFields = "unknown";
|
|
1018
|
+
} else {
|
|
1019
|
+
for (const f of calleeRole.requiredLoadedFieldsAtEntry) out.requiredFields.add(f);
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// c1b — exit effects compose only when BOTH the caller-side source and the
|
|
1025
|
+
// callee-side parameter are var.
|
|
1026
|
+
if (binding.callerSourceParameterIsVar && binding.calleeParameterIsVar) {
|
|
1027
|
+
// loadsFromDbParam / initialisesParam / copiesIntoParam all establish
|
|
1028
|
+
// loaded=yes on the caller's record handle when the callee runs.
|
|
1029
|
+
if (
|
|
1030
|
+
calleeRole.loadsFromDbParam === "yes" ||
|
|
1031
|
+
calleeRole.initialisesParam === "yes" ||
|
|
1032
|
+
calleeRole.copiesIntoParam === "yes"
|
|
1033
|
+
) {
|
|
1034
|
+
out.loaded = "yes";
|
|
1035
|
+
// We don't know what fields the callee loaded — conservatively trust
|
|
1036
|
+
// the callee's exit-loaded set if it's not unknown, else mark unknown.
|
|
1037
|
+
out.currentLoadedFields = calleeRole.currentLoadedFieldsAtExit;
|
|
1038
|
+
out.pendingNarrow = "none";
|
|
1039
|
+
} else if (
|
|
1040
|
+
calleeRole.loadsFromDbParam === "unknown" ||
|
|
1041
|
+
calleeRole.initialisesParam === "unknown" ||
|
|
1042
|
+
calleeRole.copiesIntoParam === "unknown"
|
|
1043
|
+
) {
|
|
1044
|
+
out.loaded = "unknown";
|
|
1045
|
+
out.currentLoadedFields = "unknown";
|
|
1046
|
+
out.pendingNarrow = "unknown";
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
// dirty state transitions: persisted dominates dirty dominates pristine.
|
|
1050
|
+
// We must respect the spec's "Validate dominates if any branch dirty" rule
|
|
1051
|
+
// at branch joins; here at a callsite, the callee's may-facts are an OR.
|
|
1052
|
+
// Conservative rules:
|
|
1053
|
+
// - persistsCurrentRecord=yes (may persist): for non-[TryFunction] callees,
|
|
1054
|
+
// transition out.dirty -> "persisted" (best-case we trust the persist).
|
|
1055
|
+
// For [TryFunction] callees, the persist may not happen — keep as unknown.
|
|
1056
|
+
// v1 simplification: we conservatively join, since we cannot tell
|
|
1057
|
+
// [TryFunction] from here. The spec calls this out as a documented
|
|
1058
|
+
// unknown-clearing case; we mirror it by NOT downgrading dirty just
|
|
1059
|
+
// because callee may persist (sound-but-imprecise).
|
|
1060
|
+
if (calleeRole.persistsCurrentRecord === "yes") {
|
|
1061
|
+
// Spec: treat persist-contribution conservatively. Keep dirty if it's
|
|
1062
|
+
// already dirty (this is the dirtyAtExit soundness anchor); transition
|
|
1063
|
+
// pristine -> persisted; unknown -> unknown.
|
|
1064
|
+
if (out.dirty === "pristine") out.dirty = "persisted";
|
|
1065
|
+
// dirty stays dirty (don't optimistically clear)
|
|
1066
|
+
// persisted stays persisted
|
|
1067
|
+
// unknown stays unknown
|
|
1068
|
+
}
|
|
1069
|
+
if (calleeRole.validatesParam === "yes" || calleeRole.copiesIntoParam === "yes") {
|
|
1070
|
+
// Validate raises dirty if was pristine; otherwise keep.
|
|
1071
|
+
if (out.dirty === "pristine") out.dirty = "dirty";
|
|
1072
|
+
else if (out.dirty === "unknown") out.dirty = "dirty";
|
|
1073
|
+
}
|
|
1074
|
+
if (calleeRole.resetsFiltersOnParam === "yes") {
|
|
1075
|
+
out.pendingNarrow = "none";
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
// If any of the dirty-affecting may-facts are unknown, conservatively
|
|
1079
|
+
// raise out.dirty to unknown (don't claim certainty).
|
|
1080
|
+
if (
|
|
1081
|
+
calleeRole.persistsCurrentRecord === "unknown" ||
|
|
1082
|
+
calleeRole.validatesParam === "unknown" ||
|
|
1083
|
+
calleeRole.copiesIntoParam === "unknown"
|
|
1084
|
+
) {
|
|
1085
|
+
if (out.dirty === "pristine" || out.dirty === "persisted") out.dirty = "unknown";
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
return out;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
// ============================================================================
|
|
1093
|
+
// Field-read accumulation (sound-but-imprecise — see indexFieldAccesses doc)
|
|
1094
|
+
// ============================================================================
|
|
1095
|
+
|
|
1096
|
+
/**
|
|
1097
|
+
* Collect field accesses on `param` that fall inside this block's source range
|
|
1098
|
+
* but NOT inside any of its direct children's ranges. These are the "bare" FAs
|
|
1099
|
+
* that belong to this block level (e.g. `Message(Cust.Name)` in a routine body).
|
|
1100
|
+
*
|
|
1101
|
+
* FAs nested inside a child node (e.g. inside an `if` branch body block) are
|
|
1102
|
+
* attributed by THAT child's recursive walk, not here.
|
|
1103
|
+
*/
|
|
1104
|
+
function collectFieldAccessesInBlock(
|
|
1105
|
+
block: ControlFlowNode,
|
|
1106
|
+
children: ControlFlowNode[],
|
|
1107
|
+
param: ParamCtx,
|
|
1108
|
+
faIndex: FaIndex,
|
|
1109
|
+
): FieldAccess[] {
|
|
1110
|
+
const result: FieldAccess[] = [];
|
|
1111
|
+
const blockRange = block.sourceAnchor.range;
|
|
1112
|
+
|
|
1113
|
+
function fallsInRange(
|
|
1114
|
+
faLine: number,
|
|
1115
|
+
faCol: number,
|
|
1116
|
+
startLine: number,
|
|
1117
|
+
startCol: number,
|
|
1118
|
+
endLine: number,
|
|
1119
|
+
endCol: number,
|
|
1120
|
+
): boolean {
|
|
1121
|
+
if (faLine < startLine || faLine > endLine) return false;
|
|
1122
|
+
if (faLine === startLine && faCol < startCol) return false;
|
|
1123
|
+
if (faLine === endLine && faCol > endCol) return false;
|
|
1124
|
+
return true;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
for (const list of faIndex.values()) {
|
|
1128
|
+
for (const fa of list) {
|
|
1129
|
+
if (fa.recordVariableName.toLowerCase() !== param.nameLc) continue;
|
|
1130
|
+
const fr = fa.sourceAnchor.range;
|
|
1131
|
+
// Must lie inside the block range.
|
|
1132
|
+
if (
|
|
1133
|
+
!fallsInRange(
|
|
1134
|
+
fr.startLine,
|
|
1135
|
+
fr.startColumn,
|
|
1136
|
+
blockRange.startLine,
|
|
1137
|
+
blockRange.startColumn,
|
|
1138
|
+
blockRange.endLine,
|
|
1139
|
+
blockRange.endColumn,
|
|
1140
|
+
)
|
|
1141
|
+
)
|
|
1142
|
+
continue;
|
|
1143
|
+
// MUST NOT lie inside any direct child that ITSELF recurses into FAs.
|
|
1144
|
+
// Leaf nodes (op/call/exit/error, "other" without children) do not
|
|
1145
|
+
// recurse, so an FA at e.g. `Message(Cust.Name)` (which becomes a
|
|
1146
|
+
// call leaf covering the FA's position) MUST be attributed here.
|
|
1147
|
+
// Composite children (block / if / case / case-branch / loops /
|
|
1148
|
+
// try / "other"-with-children) WILL recurse and attribute the FA
|
|
1149
|
+
// themselves; exclude those.
|
|
1150
|
+
let inRecursiveChild = false;
|
|
1151
|
+
for (const c of children) {
|
|
1152
|
+
if (!childRecursesIntoFAs(c)) continue;
|
|
1153
|
+
const cr = c.sourceAnchor.range;
|
|
1154
|
+
if (
|
|
1155
|
+
fallsInRange(
|
|
1156
|
+
fr.startLine,
|
|
1157
|
+
fr.startColumn,
|
|
1158
|
+
cr.startLine,
|
|
1159
|
+
cr.startColumn,
|
|
1160
|
+
cr.endLine,
|
|
1161
|
+
cr.endColumn,
|
|
1162
|
+
)
|
|
1163
|
+
) {
|
|
1164
|
+
inRecursiveChild = true;
|
|
1165
|
+
break;
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
if (inRecursiveChild) continue;
|
|
1169
|
+
result.push(fa);
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
return result;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
/**
|
|
1176
|
+
* True iff `walkCFG` would recurse into a child of this node — implying it
|
|
1177
|
+
* could re-attribute FAs inside its range. False for terminal/opaque nodes
|
|
1178
|
+
* (op/call/exit/error and "other" without children) — FAs inside their range
|
|
1179
|
+
* must be attributed by the enclosing block.
|
|
1180
|
+
*/
|
|
1181
|
+
function childRecursesIntoFAs(c: ControlFlowNode): boolean {
|
|
1182
|
+
switch (c.kind) {
|
|
1183
|
+
case "op":
|
|
1184
|
+
case "call":
|
|
1185
|
+
case "exit":
|
|
1186
|
+
case "error":
|
|
1187
|
+
return false;
|
|
1188
|
+
case "other":
|
|
1189
|
+
return (c.children ?? []).length > 0;
|
|
1190
|
+
default:
|
|
1191
|
+
// block / if / case / case-branch / while / repeat / for / foreach / try
|
|
1192
|
+
return true;
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
function applyFieldRead(state: PerParamState, fa: FieldAccess): PerParamState {
|
|
1197
|
+
if (state.loaded === "yes") return state;
|
|
1198
|
+
const out: PerParamState = {
|
|
1199
|
+
...state,
|
|
1200
|
+
requiredFields: state.requiredFields === "unknown" ? "unknown" : new Set(state.requiredFields),
|
|
1201
|
+
};
|
|
1202
|
+
out.requiresLoadedAtEntry = "yes";
|
|
1203
|
+
if (out.requiredFields !== "unknown") out.requiredFields.add(fa.fieldName);
|
|
1204
|
+
return out;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
// ============================================================================
|
|
1208
|
+
// Exit-fact aggregation
|
|
1209
|
+
// ============================================================================
|
|
1210
|
+
|
|
1211
|
+
function computeDirtyAtExit(states: PerParamState[]): EffectPresence {
|
|
1212
|
+
// Spec §(b): if any exit state has dirty -> "yes"; else if any has unknown
|
|
1213
|
+
// -> "unknown"; else "no". `persisted` and `pristine` are clean.
|
|
1214
|
+
let anyDirty = false;
|
|
1215
|
+
let anyUnknown = false;
|
|
1216
|
+
for (const s of states) {
|
|
1217
|
+
if (s.dirty === "dirty") {
|
|
1218
|
+
anyDirty = true;
|
|
1219
|
+
break;
|
|
1220
|
+
}
|
|
1221
|
+
if (s.dirty === "unknown") anyUnknown = true;
|
|
1222
|
+
}
|
|
1223
|
+
if (anyDirty) return "yes";
|
|
1224
|
+
if (anyUnknown) return "unknown";
|
|
1225
|
+
return "no";
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
function computeCurrentLoadedAtExit(states: PerParamState[]): LoadedFields {
|
|
1229
|
+
let acc: LoadedFields | undefined;
|
|
1230
|
+
for (const s of states) {
|
|
1231
|
+
acc = acc === undefined ? s.currentLoadedFields : joinLoadedFields(acc, s.currentLoadedFields);
|
|
1232
|
+
}
|
|
1233
|
+
return acc ?? "unknown";
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
// ============================================================================
|
|
1237
|
+
// Flat fallback (no statementTree available)
|
|
1238
|
+
// ============================================================================
|
|
1239
|
+
|
|
1240
|
+
type FlatEvent =
|
|
1241
|
+
| { kind: "op"; op: RecordOperation; line: number; col: number }
|
|
1242
|
+
| { kind: "field"; fa: FieldAccess; line: number; col: number }
|
|
1243
|
+
| {
|
|
1244
|
+
kind: "call";
|
|
1245
|
+
cs: CallSite;
|
|
1246
|
+
binding: CallArgumentBinding;
|
|
1247
|
+
line: number;
|
|
1248
|
+
col: number;
|
|
1249
|
+
};
|
|
1250
|
+
|
|
1251
|
+
function walkFlat(
|
|
1252
|
+
routine: Routine,
|
|
1253
|
+
param: ParamCtx,
|
|
1254
|
+
ctx: SummaryContext,
|
|
1255
|
+
lookup: ((id: RoutineId) => RoutineSummary | undefined) | undefined,
|
|
1256
|
+
pre: PerParamState,
|
|
1257
|
+
snapshots: Map<CallsiteId, LoadedFields>,
|
|
1258
|
+
): PerParamState {
|
|
1259
|
+
const events: FlatEvent[] = [];
|
|
1260
|
+
for (const op of routine.features.recordOperations) {
|
|
1261
|
+
if (!opAffectsParam(op, param)) continue;
|
|
1262
|
+
events.push({
|
|
1263
|
+
kind: "op",
|
|
1264
|
+
op,
|
|
1265
|
+
line: op.sourceAnchor.range.startLine,
|
|
1266
|
+
col: op.sourceAnchor.range.startColumn,
|
|
1267
|
+
});
|
|
1268
|
+
}
|
|
1269
|
+
for (const fa of routine.features.fieldAccesses) {
|
|
1270
|
+
if (fa.recordVariableName.toLowerCase() !== param.nameLc) continue;
|
|
1271
|
+
events.push({
|
|
1272
|
+
kind: "field",
|
|
1273
|
+
fa,
|
|
1274
|
+
line: fa.sourceAnchor.range.startLine,
|
|
1275
|
+
col: fa.sourceAnchor.range.startColumn,
|
|
1276
|
+
});
|
|
1277
|
+
}
|
|
1278
|
+
for (const cs of routine.features.callSites) {
|
|
1279
|
+
for (const binding of cs.argumentBindings) {
|
|
1280
|
+
if (binding.bindingResolution !== "resolved") continue;
|
|
1281
|
+
const byId =
|
|
1282
|
+
param.recVarId !== undefined && binding.sourceRecordVariableId === param.recVarId;
|
|
1283
|
+
const byName = binding.sourceVariableName === param.nameLc;
|
|
1284
|
+
if (!byId && !byName) continue;
|
|
1285
|
+
events.push({
|
|
1286
|
+
kind: "call",
|
|
1287
|
+
cs,
|
|
1288
|
+
binding,
|
|
1289
|
+
line: cs.sourceAnchor.range.startLine,
|
|
1290
|
+
col: cs.sourceAnchor.range.startColumn,
|
|
1291
|
+
});
|
|
1292
|
+
break;
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
events.sort((a, b) => (a.line !== b.line ? a.line - b.line : a.col - b.col));
|
|
1296
|
+
|
|
1297
|
+
let state = pre;
|
|
1298
|
+
for (const e of events) {
|
|
1299
|
+
if (e.kind === "op") {
|
|
1300
|
+
state = applyOp(state, e.op, param);
|
|
1301
|
+
} else if (e.kind === "field") {
|
|
1302
|
+
if (state.loaded !== "yes") {
|
|
1303
|
+
const out: PerParamState = {
|
|
1304
|
+
...state,
|
|
1305
|
+
requiredFields:
|
|
1306
|
+
state.requiredFields === "unknown" ? "unknown" : new Set(state.requiredFields),
|
|
1307
|
+
};
|
|
1308
|
+
out.requiresLoadedAtEntry = "yes";
|
|
1309
|
+
if (out.requiredFields !== "unknown") out.requiredFields.add(e.fa.fieldName);
|
|
1310
|
+
state = out;
|
|
1311
|
+
}
|
|
1312
|
+
} else {
|
|
1313
|
+
state = applyCall(state, e.cs, param, ctx, lookup, snapshots);
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
return state;
|
|
1317
|
+
}
|