Skip to content

Commit 9e9e17e

Browse files
authored
feat(vnext): define normalized syntax contract (#176)
## Summary - define the internal normalized syntax state and parser-analysis contracts - bind every artifact, diagnostic, analysis, conformance claim, and parser to exact source plus authenticated backend/configuration/dialect authority - add one mandatory parser runner that normalizes failures and returns runner-created analyzed state - add runtime and compile-time invariant tests plus ADR 0002 - keep the stable vNext export surface unchanged ## Safety and correctness - direct invalidity requires matching conformance authority - compatibility rejection cannot become authoritative invalid SQL - malformed locations fail closed instead of becoming unavailable - parser input is capped at 1 MiB; messages, diagnostics, and limitations are bounded - hostile custom iterators cannot bypass collection limits - full AbortSignal runtime validation matches the declared callback surface - raw ASTs and backend payloads remain private ## Validation - 875 unit tests passed, with 1 governed expected failure - changed syntax.ts coverage: 97.68% statements, 95.42% branches, 100% functions, 97.66% lines - full typecheck and oxlint passed - browser tests passed - test integrity passed - packed-consumer smoke passed - demo build passed - two independent adversarial reviewers approved exact commit db43409 after four fix/review loops ## Scope Internal contract only. This PR does not add a parser adapter, cache/session wiring, catalog types, or stable public exports. <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Defines an internal normalized SQL syntax contract with a single parser runner that authenticates outputs and separates lexical eligibility from parser analysis. Strengthens cancellation: preserves the exact `AbortSignal` reason, rejects pre-aborted requests without invoking the backend, and races cancellation vs. backend completion correctly; public vNext exports stay the same and ADR 0002 documents the boundary. - New Features - Normalized statement states and parser analyses. - Branded statement-relative ranges; parser receives only exact statement text and an AbortSignal. - Parser, authority, and conformance identities; artifacts/diagnostics bound to exact source and authority; backend data stays private. - Runner validates identities, normalizes sync/async failures, handles cancellation races, and returns authenticated analyzed state. Adds ADR 0002 and tests; no change to stable exports. - Safety - 1 MiB input cap; bounded messages and collections; hostile iterators contained. - `AbortSignal` surface validated (cross-realm supported); preserves exact abort reason; pre-aborted requests skip backend; late aborts don’t override completed backend outcomes. - Malformed locations and fabricated results fail closed as malformed-output. - Compatibility rejection is never treated as invalid; invalidity requires matching conformance and authority identities. <sup>Written for commit ed58bac. Summary will update on new commits.</sup> <a href="https://cubic.dev/pr/marimo-team/codemirror-sql/pull/176?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
1 parent 99dbd84 commit 9e9e17e

5 files changed

Lines changed: 3016 additions & 1 deletion

File tree

docs/adr/0001-language-service-and-session.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,8 @@ service.dispose();
9191
`syntax` is an opaque module accepted by the stable service factory. Its parser
9292
and semantic-model SPI is not stable in this ADR. Official adapters can use an
9393
experimental subpath until two materially different backends validate the
94-
normalized artifacts.
94+
normalized artifacts. The first internal boundary is specified by
95+
[ADR 0002](./0002-normalized-syntax-contract.md).
9596

9697
## Document context
9798

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
# ADR 0002: Internal Normalized Syntax Contract
2+
3+
Status: accepted
4+
Date: 2026-07-24
5+
6+
## Context
7+
8+
The legacy parser API accepts a complete CodeMirror `EditorState`, mutates
9+
offset bookkeeping, exposes backend-specific AST shapes, and can report success
10+
without a usable tree. Dialect support and location quality are also easy to
11+
overstate: a compatibility grammar rejecting input is not evidence that the
12+
target dialect is invalid.
13+
14+
The vNext statement index already separates exact, incomplete, and opaque
15+
lexical boundaries. The next layer needs a narrow parser boundary that can
16+
support multiple backends without making the first backend's AST public or
17+
mixing lexical eligibility, parser evidence, and request cancellation into one
18+
ambiguous result.
19+
20+
This contract is internal. It must be exercised by a real adapter and semantic
21+
consumer before any part is considered for the stable `/vnext` API.
22+
23+
## Decision
24+
25+
Syntax processing has three separate state machines:
26+
27+
1. Lexical eligibility decides whether a statement is empty, incomplete,
28+
opaque, unavailable, or eligible for an analyzed result.
29+
2. Parser analysis reports parsed, invalid, unsupported, or failed evidence.
30+
3. The session request lifecycle owns caller cancellation, supersession,
31+
disposal, revision applicability, and timeouts.
32+
33+
Parser outcomes never contain `empty`, `incomplete`, `opaque`, `cancelled`, or
34+
`superseded`. Keeping those layers separate prevents a parser failure from
35+
being mistaken for invalid SQL and prevents an incomplete lexical construct
36+
from being sent to a backend.
37+
38+
The parser runner transports cancellation without classifying it: a
39+
pre-aborted request does not invoke the backend, and an in-flight abort rejects
40+
with the signal's exact reason if it wins the race with backend completion.
41+
Late backend rejection remains handled. The session decides whether that
42+
rejection represents cancellation, supersession, disposal, or another request
43+
lifecycle event.
44+
45+
### Eligibility
46+
47+
| State | Meaning | Parser invoked |
48+
| --- | --- | --- |
49+
| `empty` | Exact slot has no code | No |
50+
| `incomplete` | Exact slot ends inside a known lexical construct | No |
51+
| `opaque` | Statement boundaries are not trustworthy | No |
52+
| `unavailable` | No parser is configured for the resolved dialect | No |
53+
| `analyzed` | The parser returned authenticated normalized evidence | Yes |
54+
55+
Incomplete and opaque states preserve the scanner's closed construct/reason
56+
unions and a statement-relative location. They do not accept arbitrary strings.
57+
58+
### Parser analysis
59+
60+
`parsed` has two mutually exclusive modes:
61+
62+
- `direct` carries a conformance identity. The backend is configured for the
63+
target grammar and may make authoritative syntax claims within that grammar.
64+
- `compatibility` carries one or more explicit limitations. It may provide a
65+
useful artifact, but it does not claim target-dialect conformance.
66+
67+
`invalid` requires a direct conformance identity and at least one normalized
68+
syntax diagnostic. Compatibility rejection is therefore `unsupported` with
69+
reason `compatibility-rejected`, never `invalid`.
70+
71+
`unsupported` is an expected capability boundary: backend capability,
72+
compatibility rejection, uncovered construct, or resource limit. `failed`
73+
means the backend failed or violated its output contract; it carries a bounded
74+
safe message and explicit retryability.
75+
76+
Parser-reported locations are either an exact statement-relative range or
77+
explicitly `unavailable/not-reported`. An adapter that claims a malformed
78+
location returns `failed/malformed-output`; it must not erase the defect by
79+
turning the location into `unavailable`.
80+
81+
### Coordinates and input
82+
83+
The parser receives only:
84+
85+
- The exact code-bearing normal slot's `source` slice
86+
- An `AbortSignal`
87+
88+
The slice excludes the statement terminator and is not trimmed. It contains no
89+
editor state, document offsets, cursor, catalog, focus, or host context.
90+
Parser requests are package-created and frozen. Input is bounded to 1 MiB; a
91+
larger statement is an explicit coordinator resource limit and is not passed to
92+
the backend. Abort signals are checked by their cross-realm-compatible
93+
`AbortSignal` surface instead of `instanceof`; adapters may safely use every
94+
member declared by that interface.
95+
96+
All artifact and diagnostic ranges are branded half-open UTF-16 offsets
97+
relative to that exact slice:
98+
99+
```text
100+
0 <= from <= to <= statementText.length
101+
```
102+
103+
Absolute document ranges and statement-relative ranges are not assignable.
104+
This lets an unchanged statement artifact move with an edited prefix without
105+
rewriting its coordinates.
106+
107+
### Artifacts and identity
108+
109+
The normalized artifact initially exposes only a closed statement kind and its
110+
full statement-relative range. It exposes no generic AST, facts bag, backend
111+
node, source text, or mutable payload.
112+
113+
Package-private metadata binds every artifact, diagnostic, and analysis to its
114+
exact immutable statement text. This text is not exposed or copied into the
115+
artifact, but exact equality provides collision-free reuse for identical
116+
statements and prevents same-length text from sharing evidence.
117+
118+
Artifacts, diagnostics, analyses, states, ranges, and identities are created by
119+
package constructors, frozen, and authenticated through package-owned weak
120+
identity sets. Structural copies and fabricated objects are rejected at
121+
runtime. Future official adapters may key private `WeakMap` payloads by the
122+
artifact so relation extraction can reuse a parse without exposing backend
123+
data.
124+
125+
Cache authority requires three distinct package-owned identities:
126+
127+
- Backend/module identity
128+
- Parser-configuration identity
129+
- Dialect-syntax identity
130+
131+
One frozen package-owned parser-authority handle captures that exact tuple.
132+
Parsers, artifacts, diagnostics, analyses, and future private backend payloads
133+
all retain the handle in private metadata. The runner rejects every outcome
134+
whose handle differs from its parser, including compatibility, unsupported, and
135+
failed outcomes. A conformance identity is scoped to the same handle, so direct
136+
or invalid evidence cannot pair one backend's artifact with another backend's
137+
authority.
138+
139+
Direct validity also carries a conformance identity. String dialect IDs never
140+
substitute for these authorities. A conformance identity is created for and
141+
retains the exact backend, parser-configuration, and dialect-syntax identity
142+
tuple; a parser cannot use another tuple's conformance to make an authoritative
143+
claim.
144+
145+
Parsers are also package-created frozen descriptors. Their callback stays in
146+
private metadata and can be invoked only through the contract runner. The
147+
runner:
148+
149+
- Accepts only authentic parser and request objects
150+
- Normalizes synchronous throws and rejected promises without retaining raw
151+
errors
152+
- Rejects fabricated results
153+
- Verifies that every authentic artifact or diagnostic was constructed for the
154+
exact request text
155+
- Verifies direct/invalid conformance against the parser's exact authority
156+
tuple
157+
158+
The runner returns the authenticated `analyzed` lexical state directly; there
159+
is no separately callable analyzed-state constructor. This prevents authentic
160+
evidence created for one statement from being replayed against another.
161+
162+
Compatibility limitations are a unique, non-empty subset of the three closed
163+
values and are capped accordingly. Diagnostic collections, messages, parser
164+
input, and retained strings are likewise bounded before backend-controlled work
165+
can cause unbounded copying.
166+
167+
## Consequences
168+
169+
- Impossible result combinations are rejected by TypeScript and constructor
170+
validation.
171+
- Backends can be changed without exposing their AST as API.
172+
- Unsupported dialect coverage remains honest.
173+
- Parser artifacts can be cached independently of catalog generations.
174+
- The contract has more explicit result variants, but consumers narrow on
175+
stable discriminants instead of interpreting nullable fields or exceptions.
176+
177+
The initial `node-sql-parser` adapter will lazy-load dialect-specific builds,
178+
request locations, retain backend data privately, and normalize every boundary
179+
before constructing these results. Cache/session wiring follows only after the
180+
adapter passes the contract suite.
181+
182+
## Non-goals
183+
184+
This decision does not define:
185+
186+
- A public parser plugin SPI
187+
- A universal SQL AST
188+
- Semantic relations, scopes, types, or catalog lookup
189+
- Recovery-node semantics
190+
- Document-level validation
191+
- Session cancellation or stale-result publication
192+
- Parser cache sizing and eviction
193+
194+
Those layers depend on this contract but remain independently reviewable
195+
decisions.

0 commit comments

Comments
 (0)