scrml.dev v0.7.1
Articles › Release announcements

scrml v0.2.0 — what's coming, what's stable

scrml v0.2.0 — what's coming, and what's stable today

scrml has been v0.1.0 for a while now. That version is shipped. The compiler at github.com/bryanmaclee/scrmlTS works, has 8,700+ tests passing, and emits clean HTML, CSS, client JS, and server route handlers for the full v0.1.0 surface. If you're kicking the tires today, you're using v0.1.0. It compiles. It runs.

I'm also in the middle of a much bigger second pass — call it v0.2.0 — that I want to be transparent about. It's a breaking change. It's been planned across multiple sessions of design work. The spec reflects it. The compiler is catching up — the foundational lex+parse pass for v0.2.0 syntax (Phase A1a) just finished, and the resolve+type pass (Phase A1b) is in flight.

This post tells you what's stable, what's coming, where the compiler implementation actually is, and how to read articles that reference scrml right now.


What's shipped (v0.1.0)

  • Compiler: lex, parse, name-resolve, type-check, codegen, full pipeline.
  • Reactive state via ${ @x = 0; @y = ... } blocks inside <program>.
  • bind:value / bind:checked / bind:files for inputs.
  • Server functions via server function name() { ... } — compiler routes them automatically.
  • SQL passthrough via ?{ select * from users } — compiler emits Bun.SQL tagged-template SQL.
  • <schema> blocks for declarative database schemas.
  • <channel> blocks for WebSocket pub/sub (single-instance Bun WS server).
  • <machine> state machines (the legacy keyword for what v0.2.0 calls <engine>).
  • Inline tests via ~{ ... } blocks.
  • 16-module stdlib: auth, crypto, data, format, fs, http, oauth, path, process, redis, cron, regex, router, store, test, time.
  • 32 example apps + a real-world trucking-dispatch multi-page app.
  • LSP server, VSCode extension, neovim treesitter grammar.

The published scrml init scaffold + the kickstarter article (v1) describe v0.1.0 idioms — those compile against the current compiler.


What's in flight (v0.2.0)

A multi-month migration. The headline:

V5-strict access

The declaration site uses <name>. The expression site uses @name. Bare names are local variables only.

<count> = 0                        // declaration — structural form
@count = @count + 1                // expression — canonical access

Today scrml uses @count = 0 for the declaration too. v0.2.0 promotes the angle-bracket form as the canonical decl shape.

Three RHS shapes for state

<count> = 0                                // Shape 1 — plain
<userName req length(>=2)> = <input/>      // Shape 2 — decl coupled with render-spec + validators
const <doubled> = @count * 2               // Shape 3 — derived (read-only)

Shape 2 is the new ergonomic for forms. The compiler dispatches bind:value/checked/files based on the render-spec markup type. The req length(>=2) validators live on the decl itself.

Auto-synthesized validity surface

If a compound state has validators, the compiler synthesizes a reactive validity surface — @form.isValid, @form.errors, @form.touched, @form.submitted, plus per-field versions. Read-only. Reactive.

<signup>
  <name  req length(>=2)>  = <input type="text"/>
  <email req email>        = <input type="email"/>
  <agree req>              = <input type="checkbox"/>
</>

<button disabled=${!@signup.isValid} onclick=submit()>Submit</button>
<errors of=@signup.email/>

No external validation library. No manual if (errors.length > 0) plumbing.

Tier 0/1/2 ladder for case analysis

Apps don't start at the north star. They evolve toward it.

  • Tier 0if= chains and boolean lifecycle flags. No exhaustiveness checking, but you can prototype fast. Compiler nudges via lint when you're accumulating booleans-as-state.
  • Tier 1<match for=Phase> block. Structural exhaustiveness checked. Rules-inert (you can write rule="..." and the compiler stays quiet).
  • Tier 2<engine for=Phase initial=.Idle>. Full state machine: exhaustiveness + active rules + transition handlers + <onTransition> effects.

Promotion is mechanical — the state-children carry forward verbatim from Tier 1 to Tier 2.

File-level channels

<channel name="chat" topic="lobby">
  <messages> = []                                    // V5-strict — auto-syncs across clients
  server function postMessage(author, body) {
    @messages = [...@messages, { author, body, ts: Date.now() }]
  }
</>

<program>
  ${ const count = @messages.length }                // cross-scope canonical access
</>

<channel> becomes a sibling of <program> (not a child). The @shared modifier is removed; cells in channel body auto-sync by being there.

Schema vocabulary unification

<schema>
  users {
    email: text not null unique           // SQL-mirror native — canonical
    name:  text req length(>=2)           // shared-core additive — lowers to NOT NULL + CHECK
    age:   integer min(18) max(120)       // lowers to CHECK constraints
  }
</>

The same predicate vocabulary (req, length, pattern, min, max, gte, lte, etc.) works in three contexts: state-cell validators, refinement type predicates, and now schema column constraints. Lowered to standard SQL DDL — passthrough remains inviolable.

Refinement-type predicates

<email>: string(pattern(/^[^@]+@[^@]+$/)) req = <input type="email"/>

Predicates on type annotations fire at compile-time AND runtime boundary. Stronger than state validators (runtime-only-reactive); composes cleanly with them.

Full L1-L21 lock list

There are 21 architectural locks ratified across sessions S55-S59. Markup-as-first-class-value (the pillar held since the very first prototype). Variant C compound state. Multi-statement event handlers force named function. pinned modifier. reset(@cell) keyword. default= attribute. derived=expr engine attribute. is some vs req distinction. <errors of=expr/> first-class element. E-DERIVED-VALUE-MUTATE (no in-place mutation of const-derived cells). And about a dozen more.

Full list with cross-references: docs/PA-SCRML-PRIMER.md — a primer that distills the whole language design as it'll exist post-v0.2.0.


Where the compiler is today (2026-05-05)

The implementation is split into phases. Each lands as a sequence of focused, tested sub-steps before the next phase opens.

  • Phase A1a — foundational lex+parse for v0.2.0 syntax.Done. 20 sub-steps, +184 tests, 0 regressions. The parser now recognizes <count> = 0 decl form, the three RHS shapes (plain / decl-with-spec / derived), Variant C compound state, the pinned modifier, the default= attribute, the reset(@cell) keyword, and the rest of A1a's surface. Final state: 8,902 tests passing across 439 files.
  • Phase A1b — resolve + type. 🟡 In flight. 22 focused sub-steps (B1-B22) totaling roughly 85-120 hours. B1 (per-scope state-cell symbol-table extension) is dispatched as of this writing. A1b is the enforcement phase — it walks the A1a-produced AST, builds symbol tables, fires diagnostics for the architectural locks (V5-strict, derived-cell read-only, validity-surface synthesis), and produces an annotated AST that A1c can emit from.
  • Phase A1c — codegen + runtime + PIPELINE prose pass. ⏸️ Pending A1b. Roughly 96-136 hours. Emits JavaScript for the new AST shapes, lowers the validity surface to runtime cells, wires file-level channels to WebSocket endpoints, lowers shared-core schema vocabulary to SQL DDL.
  • Phases A2-A6 (structural elements, validators-end-to-end, schema/refinement/pinned, resolver/typer alignment, codegen/runtime full sweep) — pending after A1.
  • Parallel tracks B1-B5 (examples rewrite, sample curation, stdlib audit, self-host bootstrap, editor support) — most can run alongside A2+.
  • Documentation tracks C1-C3 (tutorial rewrite, articles triage, README + scrml.dev refresh) — gated on the appropriate compiler phase.

What this means concretely: the v0.2.0 syntax now PARSES into the right AST shape. It doesn't yet RESOLVE, TYPE, or CODEGEN — those are A1b and A1c. So if you write a v0.2.0-shaped file today, the parser accepts it, but downstream stages will fail until A1b/A1c land.

The methodology is per-step focused dispatches with PA cherry-pick to main between steps and a depth-of-survey discount applied at every dispatch (existing infrastructure routinely covers more than initial audits assume — confirmed 9× across A1a). Steady cadence, no regressions, full test suite green at every step.


What this means for code you read today

If you read articles, blog posts, or LLM-generated scrml that shows:

  • <count> = 0 decl form (without a leading @)
  • <engine for=Phase initial=.Idle> block-form engines
  • <match for=Type> block-form match
  • @form.isValid validity surface reads
  • <errors of=expr/> elements
  • default= attribute on a state-cell decl
  • pinned modifier
  • const <derived> = expr (with the angle brackets on the LHS)

Those describe v0.2.0 design. They will not compile against the current v0.1.0 compiler. The article authors aren't lying — the spec does say those things. But the compiler hasn't caught up yet. We are explicit about this so readers don't waste time thinking they're typing the wrong thing.

If you read code that uses @count = 0-style declarations inside ${...} blocks — that's v0.1.0 and compiles cleanly today.

The v1 kickstarter article (llm-kickstarter-v1) describes v0.1.0 idioms. The v2 kickstarter (llm-kickstarter-v2) describes v0.2.0 design and is intended for LLM dev-agent dispatches only, not for human onboarding to current scrml.


Why we're doing this now

A few reasons. Honest:

1. The angle-bracket-on-decl-site form was always the intent. v0.1.0 ended up with @-on-decl because the parser was easier that way. It's been visibly drifting from the language envisioned for some time. The migration is a correction. 2. State machines deserve to be first-class. v0.1.0's <machine> keyword works but doesn't feel like the centerpiece of the language. v0.2.0 promotes engines to where they belong: the structural shape of the UI tree IS the structural shape of the application's state. That's the north star. 3. Forms are 80% of what apps actually do. v0.1.0 makes you wire validation by hand. v0.2.0 auto-synthesizes the validity surface from the validator decls themselves. No external library. No hooks rules. It just works. 4. No production adopters means no migration tooling needed. Every other language migration in flight requires building a v0.compat path or a scrml migrate translation tool. We have neither. We have a small set of ourselves and some early experimenters; the migration is a coordinated change-everything pass, not a backward-compat negotiation.


Timeline

The compiler-source critical path — Phase A1 (foundational lex/parse + resolve/type + codegen — itself split into A1a / A1b / A1c) → A2 (structural elements) → A3 (validators end-to-end) → A4 (schema/refinement/pinned) → A5 (resolver/typer alignment) → A6 (full codegen/runtime sweep) — is roughly 145-235 hours of focused engineering. A1a is done; A1b/A1c together are roughly 180-260 hours and are the next two phases. Parallel tracks (examples rewrite, sample curation, stdlib audit, self-host bootstrap, editor support) add another ~100-200 hours, much of which runs alongside the critical path.

Realistic calendar: 3-9 months from the original v0.2.0 design freeze (S58, 2026-05-04) depending on session cadence. A1a finished one day after design freeze. It will ship when it's done, not on a date.

We'll post incremental milestones as each Phase lands. The full inventory and live dashboard are visible to anyone reading the repo:


TL;DR

  • Today: v0.1.0. Use @x = 0 for declarations inside ${...} blocks. Compiles, runs, ships.
  • In flight: v0.2.0. Will land <x> = 0 decl form, Tier 0/1/2 ladder, auto-synth validity surface, file-level channels, shared-core schema vocab, refinement-type predicates, plus the rest of the L1-L21 lock list.
  • Compiler implementation status: Phase A1a (foundational lex+parse) ✅ done as of 2026-05-05. Phase A1b (resolve+type) 🟡 in flight. A1c through A6 + parallel tracks ⏸️ ahead.
  • Articles showing v0.2.0 syntax describe the spec target, not the current compiler. Don't waste cycles trying to compile them yet.
  • No migration tooling is planned. v0.2.0 supersedes v0.1.0 cleanly; v0.1.0 code does not get a backward-compat path.

If you want to follow along, watch the repo. If you want to wait until v0.2.0 ships, that's also a reasonable call.