Add CML rich type system + faithful TypeScript codegen#1
Open
tarasmitran-stripe wants to merge 10 commits into
Open
Add CML rich type system + faithful TypeScript codegen#1tarasmitran-stripe wants to merge 10 commits into
tarasmitran-stripe wants to merge 10 commits into
Conversation
Extends lattice toward replacing Stripe billing's CML across three slices. Slice 1 — rich TypeRef in the AST: adds the carried kinds (optional, map, generic, union, carrier) alongside the solved core. Establishes the "carried vs solved" split — carried types are represented for faithful codegen but dropped from derivation/solving (the derivation fold folds through `optional` transparently and to nothing for carried containers). Round-trips through the .lat serializer; all TypeRef consumers made total. Slice 2 — grammar + parser: makes the rich types writable in .lat source. `LatType` is layered (union `|` loosest, then List/Optional/Map/generic/ ref/named); adds a `builtin` declaration for opaque carriers. The parser flattens unions, resolves bare names prim→value→owner→carrier→enum, and normalizes a head `Optional<T>` to the `?` flag so it routes through the existing optional validation uniformly. validateModel recurses into every container so nested unresolved refs/enums/carriers are still reported. Slice 3 — faithful full-model TypeScript emitter: new `codegen` CLI command (`--spec`/`--session`, `--lang ts`) that renders the entire DomainModel to TS types, handling every TypeRef kind and never throwing on rich types. Kept separate from the solver-scoped SQLite reference service. Owned collections embed the child interface; carriers get a top-of-file alias block; head-Optional → optional property; generics/unions rendered faithfully. Reuses the existing loaders, inheriting the parse and derived-name-collision gates. Full suite green (118 files / 1122 tests) + typecheck clean. Docs updated (field-types.md, naming-conventions.md). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Committed-By-Agent: claude
… enums, Boolean, external refs)
Closes the five language gaps found in the CML gap analysis so the codegen
backends have a complete source model. New constructs adopt TypeScript-familiar
syntax; existing containers (List<T>/Map<K,V>/T?) already read as TS generics
and stay as-is.
- Boolean prim — solver-dropped (like Text/Id), codegen maps it to `boolean`.
- External-ref builtins — `builtin Name = "Opus::…::Amount"`; DomainModel.builtins
is now BuiltinDef[] ({name, ref?}). A backend imports the ref instead of emitting.
- TS `type` construct — `type X = T` aliases (resolved/inlined at parse like CML,
retained for round-trip, cycle-guarded) and `type X = { … }` free-form carried
records (no solver restrictions, unlike `value`).
- Sum-type / payload enums — `enum X { monetary(Amount), none }`; codegen lowers to
a TS discriminated union `{ kind; value }`.
Key simplification: a field referencing a record reuses the existing `carrier`
TypeRef (records and builtins are both named/carried/not-solver-encoded), so no new
TypeRef kind and none of the exhaustive TypeRef switches change. mapType resolution
is prim → alias → value → owner → carrier(builtin∪record) → enum.
Verified end-to-end via `codegen --spec … --out …`. Full suite green (118 files /
1141 tests) + typecheck clean; docs (field-types.md, enum.md, naming-conventions.md)
updated and their `lat` blocks parse.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Committed-By-Agent: claude
Foundations for doc-faithful / public-surface codegen ahead of the Ruby backend. - Field-level `///` docs: FieldDecl gains a doc slot (shared rule, so value/entity/ aggregate/event/`type`-record fields all get docs); Field.doc; parsed, round-trips through astToCode, and emitted as a JSDoc block by the faithful TS codegen. - Visibility rides the existing tag system (no grammar/model change): internal is the default (CML has no @internal), `@public` opts a field into the public API surface, `@hookOnly` narrows exposure to hooks. TS codegen renders `@public` / `@public (hook-only)` in the field JSDoc. Docs updated (field-types.md, tags.md, doc-comments.md). Full suite green (1152 pass) + typecheck clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Committed-By-Agent: claude
Add a `module Name { … }` grouping construct inside a context so lattice can
represent CML's Module blocks (which become Ruby module namespaces). Foundation
for the Ruby backend's namespace nesting.
- Grammar: ContextItem splits into ModuleItem | TicksDecl | InvariantDecl |
ModuleDecl; a module contains the namespaceable declarations (no nested modules;
ticks/invariants stay top-level).
- Module is a grouping LABEL, not a name scope: names stay context-globally unique,
refs resolve as before, and a module name may coincide with a declaration name
(its own namespace). The model stays flat — each namespaceable def gains an
optional `module?`.
- Parser flattens module-nested decls (tagging each with its module); module-name
validation (duplicate-module, reserved/invalid, PascalCase warning) with CST
positions. Serializer round-trips `module X { … }` blocks. TS codegen groups a
module's types under a `// ── module: X ──` banner (flat; real nesting is the
Ruby backend's job).
New docs/language/module.md; `module` reserved word added. Full suite green
(1164 pass) + typecheck clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Committed-By-Agent: claude
Empirical audit (fidelity harness) of how well lattice represents the real CML corpus, before building the Ruby backend. Inventoried construct/type/annotation usage across the corpus and classified each against lattice's current capabilities, producing a prioritized gap list. Findings: the data-model layer is fully covered (context/module/aggregate/entity/ value/enum incl. sum-type/type records+aliases/builtins+external refs/generics/ unions/field docs+visibility/Boolean). Remaining gaps, prioritized by real prevalence: qualified intra-aggregate `Agg::Type` refs (pervasive), service tier annotations (pervasive), multi-file `import` translation (pervasive), enum-level docs (common), inline type-body docs (common), and the hooks/extension system (rare — one block — but architecturally significant). Abstract only: order-of-magnitude buckets, construct vocabulary, and the lattice mapping — no real domain names or spec content. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Committed-By-Agent: claude
Closes the small, corpus-confirmed CML gaps ahead of the Ruby backend.
- Enum-level `///` docs: EnumDecl gains a doc slot; EnumDef.doc round-trips and
the TS codegen emits it as a JSDoc block. Removes the old enum-doc-unsupported
friendly error (enum docs are now grammatical).
- Service tier annotation: `service X { tier = appPublic | appPrivate | domain … }`
(uses `tier` since `type` is a keyword; carried metadata for codegen, validated
with `unknown-service-tier`). Default appPublic when omitted.
- docs/cml-coverage.md: corrected the Agg::Type overcount (the audit double-counted
Ruby FQN segments inside @external strings; real usage is ~dozens, concentrated,
a namespacing convention with no genuine collisions) and reclassified it as a
translation convention; added a Translation conventions section (Agg::Type hoisting,
inline type-body docs, multi-file import).
Full suite green (1172 pass) + typecheck clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Committed-By-Agent: claude
Add src/ir/schema.ts: a versioned IR envelope (irVersion "1") in front of
DomainModel — the stable contract external Ruby/Java code generators consume.
toIR() deep-clones the model, normalizes the optional builtins/typeAliases/
records collections to [], and runs a compile-time exhaustive drift guard over
every TypeRef kind.
Add the `emit-ir` CLI command, mirroring `codegen`'s input handling (same
--spec/--session pair and parse/collision gates) but with no --lang, since the
IR is language-neutral by construction.
Tests: a completeness unit test over a hand-built model exercising all 10
TypeRef kinds and every Def collection; a CLI e2e test; and an abstract golden
fixture (test/fixtures/ir/abstract.{lat,ir.json}) as a drift guard. docs/ir.md
documents the contract and version policy. All fixtures abstract.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Committed-By-Agent: claude
Add docs/writing-a-consumer.md — the plugin-writer guide distilling the four-role consumer architecture (IR Load → LayoutStrategy → Emitter → Generator) that turns lattice's emitted IR into code in any target language and folder/architecture convention. Covers the layout-vs-emission split, the reuse-existing-templates principle, the deterministic-target faithfulness caveat, the common gotchas (template-root coupling, formatter fidelity, head-optional normalization, type-map completeness), a worked abstract example, and three validating case studies (layered DDD, hexagonal, JVM). Complements docs/ir.md (the contract). Add pointers from getting-started.md and the README. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Committed-By-Agent: claude
Field and param names previously could not be any .lat keyword, since the grammar lexes them as keywords and validateModel's checkName rejects every RESERVED_WORDS member. Common real field names (state, type, count, from, to) were unusable. Add a `FieldName` grammar rule permitting a small safe subset of keywords (they never start a sibling construct in a field body and aren't FieldDecl flags, so they lex unambiguously in `name :` position), used by FieldDecl and ParamDecl. Carve the same subset (FIELD_NAME_KEYWORDS) out of checkName for field/param kinds. `state` stays rejected on a machine-bearing aggregate (where <Region>.state collides) via checkReservedField, but is now allowed on records/values/entities/events/machineless aggregates. Tests: keyword field names parse + round-trip; `state` gated by machine presence; a non-carve-out reserved word (entity) still rejected as a field name. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Committed-By-Agent: claude
Cross-context type references use an explicit fully-qualified path, never foreign-layout inference. Document the idiomatic form (an external builtin, `builtin X = "FQN"` → a carrier emitted verbatim) in ir.md, and the consumer rule in writing-a-consumer.md: a dotted `ref Context.Type` must be resolved via an explicit Context.Type→FQN mapping and fail loud on an unmapped one — a silent fallback (T.untyped, or the raw dotted string as a type) is a correctness bug. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Committed-By-Agent: claude
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Extends lattice toward replacing Stripe billing's CML across three slices.
Slice 1 — rich TypeRef in the AST: adds the carried kinds (optional, map, generic, union, carrier) alongside the solved core. Establishes the "carried vs solved" split — carried types are represented for faithful codegen but dropped from derivation/solving (the derivation fold folds through
optionaltransparently and to nothing for carried containers). Round-trips through the .lat serializer; all TypeRef consumers made total.Slice 2 — grammar + parser: makes the rich types writable in .lat source.
LatTypeis layered (union|loosest, then List/Optional/Map/generic/ ref/named); adds abuiltindeclaration for opaque carriers. The parser flattens unions, resolves bare names prim→value→owner→carrier→enum, and normalizes a headOptional<T>to the?flag so it routes through the existing optional validation uniformly. validateModel recurses into every container so nested unresolved refs/enums/carriers are still reported.Slice 3 — faithful full-model TypeScript emitter: new
codegenCLI command (--spec/--session,--lang ts) that renders the entire DomainModel to TS types, handling every TypeRef kind and never throwing on rich types. Kept separate from the solver-scoped SQLite reference service. Owned collections embed the child interface; carriers get a top-of-file alias block; head-Optional → optional property; generics/unions rendered faithfully. Reuses the existing loaders, inheriting the parse and derived-name-collision gates.Full suite green (118 files / 1122 tests) + typecheck clean. Docs updated (field-types.md, naming-conventions.md).
Committed-By-Agent: claude