feat(model): persistent semantic model per workspace, invalidated per document - #316
Conversation
…validated per document Co-Authored-By: jason.han <hanhuijun@gmail.com>
Co-Authored-By: jason.han <hanhuijun@gmail.com>
OOSEMMethodPass, MOSAPass and IdentityMetadataPass gather each workspace document once into a workspace-owned Gathers and judge over the union, so an analysis costs one gather per document rather than one per document per analyzed document. A change re-gathers the changed document only and drops the judgments whose union answer moved. Co-Authored-By: jason.han <hanhuijun@gmail.com>
…tered frames A frame's journal held one closure per memoized entry; it now holds one ledger per memo table with the keys written there, so dropping a document deletes its entries without a call each. Entering a document's frame from another records the dependency once per recently entered document rather than once per read. Direct-child lookups read the namespace they enumerate, and the identity gather spells its facts when judged rather than when gathered. Co-Authored-By: jason.han <hanhuijun@gmail.com>
… loading and editing it Split writes the library, one document per plane and the constellation joining them; BenchmarkLoadFiles analyzes the split through one workspace and BenchmarkEditImported edits the library every plane imports. Co-Authored-By: jason.han <hanhuijun@gmail.com>
Co-Authored-By: jason.han <hanhuijun@gmail.com>
…ints Co-Authored-By: jason.han <hanhuijun@gmail.com>
…rnaling Co-Authored-By: jason.han <hanhuijun@gmail.com>
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
…cument bodyApplicationOf wrote bodyIndexed and bodyApplications outside any frame, so every replacement of a document with collection bodies kept its previous root scope and body expressions reachable. scalarTable memoized the stdlib scalar symbols once per model with nothing to drop it, so a document declaring a scalar name kept its stale symbols after it changed. Both now join the resolver's document frames: the body index is journaled under the document holding the body, and the scalar table is built in its shared frame, dropped with its readers when a declaring document changes. Co-Authored-By: jason.han <hanhuijun@gmail.com>
Merging with
|
Co-Authored-By: jason.han <hanhuijun@gmail.com>
A change to a gather's judgment names carries no index registration, so it need not drop the frames that enumerated the whole name table nor rebuild the suggestion table; both now survive until a namespace or document actually changes. Frames that read the whole table keep recording the judgment names they read, so a judgment change still reaches them. Co-Authored-By: jason.han <hanhuijun@gmail.com>
…-semantic-model Co-Authored-By: jason.han <hanhuijun@gmail.com> # Conflicts: # README.md # docs/project/spec-compliance.md
…declaring document A reader served a cached member-source or lookup-order closure from another document recorded no dependency on the document declaring the symbol, so editing that document left the reader's diagnostics and derived state cached. Enter the declaring document's frame before the memo lookup, as the other symbol-keyed memos do. Co-Authored-By: jason.han <hanhuijun@gmail.com>
…-semantic-model Co-Authored-By: jason.han <hanhuijun@gmail.com> # Conflicts: # README.md
…-semantic-model Co-Authored-By: jason.han <hanhuijun@gmail.com> # Conflicts: # README.md # docs/project/spec-compliance.md
…-semantic-model Co-Authored-By: jason.han <hanhuijun@gmail.com> # Conflicts: # README.md # docs/project/spec-compliance.md
What and why
Implements "Approach 1 — a persistent semantic model, invalidated per document" (§3, 3.1–3.4) of
docs/project/large-model-scaling-design.md. Every edit used to clear the whole workspace —diagCache, the reverse-reference index — and the next request started from a freshresolve.Resolverandsemantics.Model, so a keystroke in a two-line file beside a large model cost a re-analysis of the model.model.Workspace(workspace.go:semanticsLocked, made on first use, handed to everypasses.Contextthe workspace builds throughpasses.AnalyzeShared/Context.Share). A context built outside a workspace still constructs its own; CLI paths and tests are unchanged.symbols.Indexreports every read to aReadRecorder(a name looked up, a namespace enumerated or its children read, a document's root or kind, the whole table) and every write to aChangesset (symbols/changes.go,index.go,layer.go). The resolver (resolve/frames.go) keeps a frame per document — what was memoized while that document was analyzed, and what it read — and records a dependency on another document when a resolution returned that document's symbol (Resolver.found→Depend(sym.DocName)), when it imports a namespace the other contributes to, or when both contribute to one namespace.Index.TakeChanges+Resolver.Invalidatecompute the transitive closure of the frames a change reaches.workspace.go:invalidateLocked→Resolver.Invalidate): drops the changed document's frame and its dependents', their memo entries (resolver, semantic model and argument typer all journal through the resolver's existingJournalNew, now a per-table ledger), their diagnostics and their reverse references.SetConformanceModestill invalidates everything. The semantic model's memoizing methods own their entries withdefer m.own(sym).LeaveDoc()before the cache lookup, so a hit also records that the reader depends on the entry's owner — needed for compound-keyed memos such ascomposed[(S, T)].passes/gathers.go,oosem_gather.go,mosa_gather.go,identity_gather.go, andsemantics/coherent_quantity.goviaModel.shared): each document's OOSEM/MOSA/identity facts are gathered once intopasses.Gathers, owned by the workspace, regathered for the changed documents only (Gathers.Regather), and each analysis judges over the union. Unions live in named resolver frames so that a regather that moves a shared judgment's input drops its readers. Populated gathers serve concurrent readers under a mutex.internal/lsp/files.go):refreshOpenDiagnosticsrepublishes from the cache for open documents outside the changed one's dependent set.acceptFrom/dropStaleDebugSessions/keepIdentitiesOfneeded no change; the transcript is one document and a submission that only adds an unrelated member still leaves the debugger running (internal/repltests unchanged). The file-load path (%load,sysml -validate a b) is being changed separately to one document per file and is not touched here.Where the implementation differs from the design: §3.2 suggests the REPL treating each top-level submission as its own document. That was not needed for the contract stated (the debugger's supersession is declaration-granular and already independent of the workspace's caching), so it is not done here.
Which path pays the bookkeeping, and which does not
Tracking is always on for a workspace, as the design states. Every memoized read records "the current document depends on this entry's owner" — about 25 M reads × ~8 ns on the 200-satellite constellation — so a one-shot
sysml -validate, which analyzes through a REPL session's workspace, pays it with nothing to invalidate:sysml -validate -memstats, 200 satellites, one file, 3 runs/usr/bin/time)At 1 600 satellites: 17.7 s / 5.5 GiB → 20.5 s / 5.8 GiB. This is a measured regression of the batch path, reported rather than hidden. What removes it: #312 (
perf/parallel-batch-validation, stacked on #309) — itsWorkspace.DiagnosticsAllruns each uncached document throughanalyze(name, doc, batch)→passes.AnalyzeInBatch, which builds a privatepasses.Contextper document (NewContextWithOptions: its own resolver and model over the read-only index) and never touches the workspace's persistent resolver, so the batch path there records nothing and pays none of this. Verified against #312'sinternal/core/model/batch.goandpasses.Context.Batch.Follow-up, not done here:
Context.Batchis where #312's workers should receive this branch'spasses.Gathers, so that the audits gather each document once for the whole batch rather than once per worker. It is described indocs/internals/performance.mdand the stress-test record's further work.Overlap to reconcile on whichever lands second: #309/#312 also edit
workspace.go(Open/Update/SetOnDiskreindex viaAddBuiltDocument;diagnosticsLockeddelegating toanalyze). This branch is based ondevelop, not on either.How it was verified
Incremental equals fresh.
TestIncrementalEqualsFresh(internal/core/model/incremental_test.go) replays a scripted sequence and a seeded random one — edit a document to a mutation or back to an earlier version, close, open — over the fixture sets, andTestIncrementalEqualsFreshCorporaover the four OMG corpora; after every step it compares diagnostics, the resolution of every written reference and the reverse references with a workspace built fresh from the same documents. Both run ingo test ./...(the corpus variant skips without the corpora, fails with the require variables set).Focused tests added: resolver frames, ledgers and transitive invalidation (
resolve/frames_test.go); index read recording and change sets (symbols/changes_test.go); gathers gather each document once, regather only the changed one, propagate a union change to its readers, serve concurrent analyses (passes/gathers_test.go); workspace edits across documents, union-dependent diagnostics, retained state (model/workspace_test.go,stressmodel/satnet_test.go).Measurements (Intel Xeon Platinum 8559C, 8 CPUs, 31 GiB RAM, Go 1.25.0, Linux;
go test -run '^$' -bench ... -benchmem -benchtime=5x -count=3 ./internal/stressmodel, medians; "develop" isbcc6b13e0built on the same machine):BenchmarkEditBeside/32BenchmarkEditBeside/128BenchmarkEditBeside/512BenchmarkEditImported/512(6 files; edit the library, then every file's diagnostics)BenchmarkLoadFiles/512(6 files through one workspace)BenchmarkLoad/512, one fileTestEditsHoldNoStaleState)The design's target (512 satellites: ~1.53 s → < 50 ms) is met; develop on this machine measures 861 ms rather than the 1.53 s in the record, so both columns above were taken here. Editing the imported library costs one analysis of the model (the same 5.26 s as loading the six files). The 34-file figure is the §5 workload: the three audits gathered every document once per document analyzed; now once. A loaded workspace holds about twice the heap it did (the memo tables that used to be discarded after each analysis).
Gates:
training_examples_expected.txtand the pilot expectations are untouched.Checklist
make testandmake lintpass locallydocs/internals/performance.md,docs/project/satellite-network-stress-test.md)changes/unreleased/persistent-semantic-model.performance.mdmake docs-countsrun (test count moved)