Deferred work — Tu
A living list of every “leave for later” decision made during a milestone, with the milestone that introduced the gap and (when known) where it should land. Rows are removed in the commit that fills them, so the diff shows the loop closing.
Open
| Item | Introduced | Target | Notes |
|---|---|---|---|
| Sandbox API for executing user-provided Tu | post-M8 | TBD | Tu rejects raw eval but the use case (playgrounds, plugin systems, in-browser editors) is real. Default direction: SES (Hardened JS) for portable isolation, fall back to Web Worker for browser-only. Don’t speculate-design ahead of a real consumer. See memory/project_sandbox_design.md. |
| Per-component fine-grained HMR boundaries | M1.6 | post-M1.7 | The @tu-lang/vite plugin currently triggers a full module re-import + re-mount on .tu save. Per-component preserve-state HMR is future work. |
| Local reactivity (per-cell-read subscriptions) | M1.7 | M2+ | Keyed diff is cheap, but the component thunk still re-runs in full on every cell mutation. Solid-style per-binding patches that only touch the affected text node / attribute are a deeper rework — needs a different compiler IR that wraps each cell read in its own reactive scope. |
| generic syntax (no class — permanently rejected) | M6.7 | TBD | class is permanently banned in Tu (M8 decision 2026-05-02 — components are lambdas, types are interfaces). Generic-lambda syntax (<T, U>) is still a candidate for future addition when a use-site asks. Generics will pair with the M9 type-system extensions (interface generics + runtime descriptors that take type params). |
| Qwik-style SSR resumability | M1.7 | post-M6.11 | M6.11 closed the async / Suspense / streaming SSR triad (#60-#62 — renderToStringAsync, Suspense, renderToStream). Still missing: Qwik-style resumability where the client never re-runs the first-frame thunk after hydration. Today hydrate(thunk, container) re-executes thunk once on the client to bind listeners; resumability would serialize listener references into the SSR HTML and have the client adopt them without re-rendering. Big design + runtime change — separate milestone. See docs/SSR-ASYNC-DESIGN.md for the SSR design context. |
| LSP DX comprehensive update | M3 | M6.12 | User-flagged 2026-05-02 as URGENT. Beyond the per-row LSP gaps tracked elsewhere (style-class union types, DOM type isolation), the user reports module-level type errors don’t surface, hover at module scope is patchy, and reference jump (find-references / cross-.tu) is missing. Treat as a focused milestone to comprehensively improve the editor experience: full diagnostics across the import graph, hover/goto-def/find-refs working end-to-end on every identifier kind, completion polish, error-message quality. |
| Lifecycle hooks + element ref sugar | M0 | post-LSP DX | User flagged 2026-05-02 — Tu has no lifecycle hooks (onMount / onUnmount / beforeUnmount) and no element-ref sugar. Wants Vue-2-style explicit ref (NOT React useRef): <button ref: btn /> then read btn.get(), with expose { btn } for children-expose to parents. Independent feature, sizable. See memory/project_lifecycle_and_element_ref.md for the design notes. |
| File-based app router (Next.js-style) | M0 | post-M9 | Minimal SSR/client route matching landed as @tu-lang/router, and tu-shu now reuses its file-path URL helper. Still deferred: Next.js-style app/[slug]/page.tu discovery, layouts, loaders, server functions, and framework-level conventions. See memory/project_router_system.md. |
HTML-section await sugar (Svelte-{#await} analogue) + for await |
M6.6 | post-LSP DX | User flagged 2026-05-02 — wants await user = fetchUser(id) directly in markup, plus for await event in events { … } for async iterables. Codegen lowers to M6.11 Suspense; iteration form needs a new runtime primitive. The else "loading…" fallback shape is the core syntax decision. See memory/project_html_async_sugar.md. |
| Style ↔ JS state interop (CSS variables auto-bound to cells) | M1.8 | TBD | VERIFIED ABSENT 2026-05-02. The compiler’s emitStyleBlock (codegen.ts:1248) emits h("style", {}, [JSON.stringify(css)]) — pure static CSS string with no cell binding, no variable substitution. The user remembered this landing in an earlier version of Tu, but it was never implemented. Design from scratch: --name: $cellRef syntax inside style block (compiler emits style="--name: ${cell.get()}" on the component root + reactive update on cell change), or an @bind cellName rule. Pair with the M1.8 scoping infrastructure. |
| Strict type-level DOM isolation in LSP shadow | M6.10 | TBD | M6.10 split the runtime into universal (@tu-lang/runtime) + browser (@tu-lang/dom) and enforces the boundary at the runtime-function layer: mount/hydrate/defineCustomElement only live in @tu-lang/dom, so SSR/Node consumers can’t accidentally pull DOM impls. Type-level isolation is NOT yet enforced — the LSP shadow keeps lib.dom ambient, so a .tu file can still write document.x / (e: Event) without importing from @tu-lang/dom. Strict isolation needs us to ship our own minimal DOM-types fork inside @tu-lang/dom’s .d.ts (so lib.dom.d.ts can be dropped from getTuCompilerOptions) — sizable but the right long-term shape. |
Closed in M9 (2026-05-03 partial)
Module-scope destructuring— landed as a flat-field MVP: top-level privatelet { a, b } = objlet { a, b } = objnow evaluates the RHS once into a compiler-generated temp and emits oneSignal.Stateper field (afromtmp.a,bfromtmp.b). Reads of those bindings participate in normal.get()injection, and TS shadow output keeps the temp typed from the RHS or optional destructure annotation.export let { … }, rename/default/nested patterns, and array destructuring remain intentionally unsupported.Minimal universal router package— landed as@tu-lang/router: static, dynamic (:id), and trailing catch-all (*slug) matching; deploymentbasestripping; query parsing; fallback handlers; and SSR helpers layered onrenderPageAsync/renderToStream. Playground routes now use shareable paths like/tu/playground/typedinstead of hash fragments, with generated GitHub Pages fallback HTML.Filtered catch + Exception narrowing— landed:catch if ValidationError as e { ... }lowers to one JS catch withtype.isdispatch and narrowseinside the branch through the TS/LSP shadow.catch e { ... }is the fallback branch and gets an Error-like default type, includingmessage: string. Legacycatch (e: T)remains accepted during alpha compatibility, but examples now prefer filtered catch syntax.Enum declarations— landed: Tu now accepts top-levelenum Name { A, B = "custom", C = 3 }with optionalexport. JS emit produces a frozen runtime object (const Name = Object.freeze(...)); TS shadow emit also adds a like-named value-union alias (type Name = (typeof Name)[keyof typeof Name]) so enum values work both at runtime (Name.A) and in annotations ((tone: Tone) => ...).classremains permanently banned, and generic syntax stays deferred.CSS4 nesting /— landed: style-block validation now walks nested CSS rule blocks instead of only depth-0 selectors. Grouping at-rules such as@layer/@scopeawareness in style block@layer,@media,@supports, and@containerrecurse and still enforce class-rooted style rules, selector-list splitting respects commas inside pseudo-class functions like:is(.a, .b), and@scope (.class)permits element selectors inside the scoped body while rejecting non-class scope roots.Deprecate positional component calls— landed: legacy uppercase positional calls such asCard("title") { ... }still compile during the alpha compatibility window, but LSP diagnostics andtu checknow emit a warning on the component callee asking users to migrate to named-prop calls such asCard(title: value) { ... }.docs/LANGUAGE.tunow documents named props as the preferred form and marks positional calls as deprecated.Default export (— landed: Tu now acceptsexport default let …)export default let Name = …for component-as-file modules. JS/TS emit usesconst Name = …followed byexport default Nameso default-exported lambdas, state cells, and computed cells avoid invalidexport default constsyntax. Default imports from.tufiles are classified through the same state/computed/function import-kind path as named imports, so default-imported cells still get.get()injection in Vite/LSP shadow graphs.Tu-native type conversion API (— landed:type.tryFrom)@tu-lang/std/typenow exportstype.tryFrom<T>(value, descriptor, castFn?), a non-throwing sibling oftype.as. It returns{ ok: true, value }when the input or cast result matches the descriptor and{ ok: false, error: TypeMismatchError }on shape mismatch or cast-function failure, giving Tu code a Result-shaped alternative to JS implicit coercion and exception-only parsing paths.LSP type-annotation source mapping polish— landed: TS shadow emit now maps raw type spans for lambda params, exported auto-Props fields, lambda return types, local-let annotations, wrapped state/computed annotations, andtype.tag(User, …)descriptor refs back to the original.tutype token. Hover/definition/references/rename can now work on interface names written inside lambda type annotations instead of only on value-position identifiers, and reference/rename locations are de-duped by final.tusource location so generated helper/interface refs do not show duplicate entries.Object-literal computed keys (— landed:{ [expr]: v })peekObjectLitShapenow recognizes{ [expr]: value }as an object literal by scanning to the matching]and requiring a following:.parseObjectPropstores the key expression oncomputedKey, and JS/TS emit prints bracket properties ({ [key]: value }) while still applying normal cell-read injection inside the key expression. Static anonymous descriptor synthesis and canonical shape merging skip computed-key objects conservatively because their field names are runtime values.Style-class literal-type union in TS emit— landed: TS shadow output now emitsClassesOf_<Component>unions from class selectors declared in each component’sstyle { … }block, plus a TS-only__tu_class<C>()helper. Scoped componentclass:props are wrapped with__tu_class<ClassesOf_X>(…), so object-form keys like{ card: cond }are checked against declared classes while plain string class values remain allowed for utility/global CSS. LSP diagnostics now map unknown object keys such as{ ghost: true }back to the offending Tu key.Remaining JS expression operators compatibility sweep— closed: M5.10/M6.5 already covered\|\|,&&,??, optional chaining,obj[k], prefix!/-/+, postfix!,newfor genuine constructors, compound assignment, spread, template literals, parens, TS optional params, and regex literals. The final missing candidate set landed in M9: computed object keys,**,**=, and bitwise&/\|/^/~/<</>>. Permanent bans remain intentional: ternary?:,++/--,instanceof,typeof, value-positionundefined/void, expression-positionthis/arguments/class/var/with, bareDate/new Date(...),new Array(...), and explicitanyoutsideexternal JS. Future JS compatibility should be added only from concrete use cases, not from blanket parity.— landed: lexer emits**exponent operatorStarStar, parser treats it as the highest-precedence right-associative binary operator (2 ** 3 ** 2parses as2 ** (3 ** 2)), JS/TS emit passes it through, and inference treats exponent expressions asnumber.Bitwise expression operators (— landed: lexer/parser/codegen now support the common numeric bitwise set with JS precedence (&/\|/^/~/<</>>)shiftabove relational, bitwise AND/XOR/OR between equality and logical AND). Prefix~emits as a parenthesized unary expression and inference treats bitwise expressions asnumber.Exponent compound assignment (— landed: lexer emits**=)StarStarEqand the existing compound-assignment desugar now lowersx **= y,obj.x **= y, andarr[i] **= yto assignment with aBinaryExpr("**")RHS, preserving the same cell/member handling as the other compound operators.LSP shadow-graph integration of canonicalizer— landed:buildShadowGraph()now runscanonicalizeShapes()across the parsed import graph and stores the canonical result on each shadow. Hover expansion uses it for merge-aware interface docs, e.g. hovering aUservalue whose shape matchesPersonappendsMerged with: Person (from person.tu)alongside the expanded field list. Emit semantics stay unchanged — LSP uses the canonical map only for editor explanation.M9 — JIT param-type inference (Phase D)— landed: omitted lambda params now infer in the TS shadow from same-file callsites, direct/re-exported cross-file callsites in bundle mode and LSP shadow graphs, same-file helper return values, body member-use shapes, and conservative non-member body-use hints. Supported callsite expressions include primitives, arithmetic/comparison/unary expressions,??/&&/\|\|widening, object literals, arrays, member reads from same-file static object lets (user.profile.name), and indexed reads from same-file static array lets (items[0]). Multiple callsites widen into unions (number | string, object-shape unions). Body fallback covers nested member chains ({ profile: { name: unknown } }) plus number/boolean/array patterns such as arithmetic,!ok,[...items],items[0],for item in items, and literal comparisons. Explicit param annotations and callsite evidence win over body fallback.LocalLet destructuring— landed: object-destructure pattern in block-scoped let. Codegen emits TS-nativelet { a, b } = objlet { a, b } = objso tsserver narrows each binding from RHS’s inferred shape (no type annotation needed — RHS provides the type). Destructured field names extend the block’s local-shadow set so module cells of the same name don’t clash. MVP scope: flat object only (no nesting / defaults / rename / array). Module-scope let destructuring stays deferred — needs cell-wrap design.Param destructuring— landed: object-destructure pattern in lambda param position. Codegen emits TS-native({ title, footer }: T) => …{ a, b }: Tso both TypeScript narrowing and JS runtime get the destructure for free; no body-rewrite. The destructured field names extend the lambda’s shadow set so cells of the same name in module scope don’t clash (export let count = 0; let X = ({ count }: P) => countreads the destructured local, not the cell). Auto-${Name}Propsis suppressed when any param is destructured — the user already named the prop shape via the destructure annotation. Type annotation is REQUIRED — untyped destructure on M9-defaultunknownwould error on every key access. MVP scope: flat object only (no nesting / defaults / rename / array destructure). 3 tests added; tu-xing/Badge.tu migrated as a proof point.— landed:class:/style:array + object syntax@tu-lang/runtimeexportsnormalizeClassValue/normalizeStyleValueplus theClassValue/StyleObjecttype aliases.classacceptsstring | number | (recursive)[] | { [k]: cond };styleacceptsstring | { camelCaseKey: string | number }. Both wired into the SSR sync path, the SSR async path, the streaming-shell renderer, and the@tu-lang/dommount + diff prop-application loops. camelCase → kebab-case for style keys (fontSize→font-size); empty results omit the attribute entirely (no moreclass=""noise). Numeric style values pass through verbatim — no auto-px(avoids the React/Vue corner case where unitless props likelineHeight: 1.5get the wrong unit). 14 new tests (11 runtime + 3 dom).Auto-rewrite— landed: the emitted interface now matches M6.1 named-arg call-site reality. Every prop becomes${Name}Propsto all-optional +children?: Child[]?:optional (callers can omit any key — runtime getsundefined) and a trailingchildren?: Child[]slot is appended. The children-append is suppressed when the lambda already declares its ownchildrenparam so user-typedchildren: Tcontinues to win. Skipped when the user hand-declares${Name}Props(existing collision guard preserved). 2 tests added; downstream tu-xing-demo bundle unchanged.— landed: parsesif let a = x { … }bind-and-test sugarif let Ident [: Type] = expr { body } [else …]via parser-level desugar to an IIFE wrapping{ let a = expr; if ((a !== null) && (a !== undefined)) body else else }. The double-strict cond is what tsserver recognizes forNonNullable<T>narrowing inside thethenbranch under Tu’s==→===rewrite. RHS is parsed undernoBraceBlockso the body’s{doesn’t get eaten as a tag-call children block. Closes the deferred row; 4 codegen tests added.TS-style— landed:astype assertion / castexpr as Typeparses as a postfix-style AsExpr (contextual keywordasfollowed by a type-name ident, optional<...>generics depth-tracked, optional trailing[]array suffixes). TS-emit wraps as(arg as Type); JS-emit erases the cast (no runtime effect). Removes theexternal JSworkaround intu-xing-demofor(e.target as HTMLInputElement).value. Unions / namespaced types deferred — wrap in a named type alias if needed. The<Type>exprlegacy form is intentionally NOT planned (collides with JSX-like generics).Phase B — untyped lambda params default to— landed: codegen emitsunknownin TS shadow(x: unknown)for params with no annotation, replacing TS’s implicit-anyfootgun.Phase C —— landed intype.as<T>(value, descriptor, castFn?): Truntime cast@tu-lang/std/type: 2-arg form is assertion-only (throwsTypeMismatchError), 3-arg form runscastFn(value)first then checks. GenericTinfers from destination annotation. Works with primitive descriptors AND user-declared interfaces (verified by 5-test end-to-end suite).Phase A polish — explicit— landed: every non-anyparser banexternal JSraw type span is scanned for whole-wordanyand rejected with a directive pointing atunknownplus thetype.as(v, T)runtime-narrowing path. The escape hatch stays intact:external JSbodies and their lambda signatures may still useanybecause that code is raw JS/TS by design.JS legacy ban polish — receiver/scope/date footguns— landed: parser directive errors now cover expression-positionvoid,this,arguments,class,var,with, plus bareDate/new Date(...)(directing users to@tu-lang/std/time). Existing bans for ternary,++/--,new Array(...), sparse-array normalization, and==/!=strict-emission remain covered by tests.JS legacy ban polish — null/undefined source unification— landed: value-positionundefinedis rejected in.tusource with a directive to usenull; playground demos and docs now usenullfor intentional empty values, and Monaco no longer advertisesundefinedas a Tu constant. Compiler-generated JS may still useundefinedfor omitted/fallthrough/internal narrowing values, andexternal JS { … }remains the escape hatch.Phase A —— landed: 9 non-external-JSanymigrationanyannotations in playground/examples migrated to typed (unknown+ external-JS body for Monaco-touching code;() => voidfor Promise resolve callbacks;CaseDefinition/CaseFileinterface refs for case-typed params). Residualanyonly insideexternal JSblock bodies (intended escape hatch). Parser-level ban for explicitanyoutside escape hatches is a small follow-up.LSP — interface-expanding hover— landed: tsserver-rendered hover content scanned for known interface names; matches append a Markdown block listing the fields with their declared types and source file. Works for typedletbindings, typed lambda params, prop reads on typed objects.LSP — type-name goto-definition— landed:definition.tsextracts the cursor’s identifier and probes the shadow-graph’s interface decls before AND after the tsserver path, soUserinlet alice: User = …jumps to theinterface User { … }declaration even when the cursor is inside a type-annotation raw-text span tsserver can’t see.M8 build-tool integration — CLI + Vite plugin— landed:tu bundle <files…> [-o outDir] [--ts]ships in@tu-lang/cli;tuBundle()plugin ships in@tu-lang/vite(production bundle by default; opt-in for dev mode). Both runcompileBundle()cross-module + emit shared__tu_types.generated.{ts,js}+ collision-free per-file canonical refs (__tu_canon_<Name>import alias).
Closed in M8
Phase 0 design doc + Phase 1 runtime API— landed:docs/TYPE-METADATA-DESIGN.mddesign doc +@tu-lang/std/typeruntime API (primitives, Array(T), Optional(T), struct, native, tag, of, is — 18 tests).Phase 2 — interface keyword + codegen + 12-file repo migration— landed:interface Foo { … }compiles to BOTH a TS interface AND a runtimeconst Foo = type.struct(…)descriptor; auto-import oftypefrom@tu-lang/std;tuTypeToDescriptorExprmaps Tu types to descriptor calls (primitives, T[], T|null, function fallback). Migrated tu-xing 7 components, examples, playground.Phase 2.5 — typed-let— landed:type.tag(I, …)injectionlet alice: User = { … }wraps the value sotype.of(alice) === User. Triggers only for locally-declared interfaces in Phase 2.5; Phase 3c widens to imported.Phase 3a/3b — anonymous synthesis + shape interning— landed: untypedlet X = { … }synthesizes a hoisted__tu_anon_N = type.struct(…)and wraps the value. Shape interning means same-shape literals share one descriptor (order-insensitive hash). Spread members disable synthesis (Phase 3d work). 10 new tests.Phase 3c — import-graph classification— landed: LSP shadow-graph collectsexport interfacenames per file; cross-.tuimports flow through to codegen soimport { User } from "./other.tu"; let bob: User = { … }also tags. Compiler-only path (no LSP) keeps Phase 2.5’s conservative fallback. 2 new LSP tests.Phase 4 — typeof / instanceof parser bans— landed:parsePrimary+parseStmtthrow directive errors pointing attype.of(value)/type.is(value, Interface). Insideexternal JS { … }block bodies the lexer doesn’t tokenize — escape hatch preserved. 3 new tests.Phase 5 —— landed:@tu-lang/std/timeTemporal-based Date replacement@tu-lang/std/timere-exports@js-temporal/polyfill’s namespace (Instant, ZonedDateTime, PlainDate, etc.). 8 Temporal native descriptors intype.Xsotype.is(v, type.Instant)works. Lazy polyfill load (constructor-name fast path;await preloadTemporal()upgrades to strict instanceof). 15 new tests.Phase 6a — canonicalizeShapes() algorithm core— landed: pure-function cross-module merge in@tu-lang/compiler. Walks N programs, hashes every interface + every untyped object-let shape (FNV-1a 64-bit, sorted-fields-stable), merges identical hashes, returns canonical descriptors + per-file rewrite tables. Anon-vs-named cross-merge supported. 13 new tests. Phase 6b/6c (integration) is the next M8 commit.
Closed in M6.12
-
Static-HTML optimization (verify M6.0 actually works)— verified + proof example landed:examples/static-html/shipsWelcome.tu(a deeply-nested static<div>tree) +verify.mjsthat compiles the source, asserts the output containsh("$static", {}, [], "<div>…</div>")(vs the ~10 nestedh()calls a non-static tree would produce), then runs the result throughrenderToStringand confirms the HTML round-trips with proper escaping. Runpnpm --filter @tu-examples/static-html test. The compiler’sisStaticTree(codegen.ts:1950) +countStaticNodes >= 3threshold gate the optimization correctly; reactive subtrees fall back toh()calls. -
Suspense + streaming SSR proof example— landed:examples/suspense/shipsPage.tuwith two asyncUserCards wrapped inSuspense, plus arun.mjsthat exercises bothrenderToStringAsync(await-all flavor) andrenderToStream(per-boundary flush). Streaming output shows 5 chunks: shell + placeholders, the$tu_replacepolyfill, two<template>chunks in resolution order (the fast card flushes first even though it appears second in source), and the body close. End-to-end proof that the M6.11 ladder works.
Closed in M6.11
-
Streaming SSR (— landed (#62):renderToStream+<template>per-boundary flush)renderToStream(thunk, options)returns a WebReadableStream<Uint8Array>ready to pipe into aResponse. The first chunk holds the full page shell + the body’s sync regions, with a<div data-tu-suspense="N">…fallback…</div>placeholder per pending boundary (or empty placeholder for a bare-Promise child outside any Suspense). A ~150-byte inline$tu_replacepolyfill flushes once before the first per-boundary template arrives. Each boundary then resolves in parallel and flushes a<template id="S:N">…body…</template><script>$tu_replace("N")</script>chunk in resolution order — a fast boundary preceded by a slow one still arrives first. Rejected boundaries leave the placeholder (and its fallback content) in the DOM unmodified. NewRenderToStreamOptions.onShellReadycallback fires after the shell + placeholders enqueue, before resolution work starts.assemblePagewas refactored to shareassembleShellPartswith the streamer so the head + body-open emit identically across the sync/async/streaming flavors. 7 new tests covering chunk ordering, rejection, async-thunk root, bare-Promise placeholder, onShellReady timing. -
— landed (#61): new<Suspense>boundary primitive$suspensetag sentinel +Suspense({ fallback, children })runtime helper. The async render walk catches both Promise rejections and throws inside the boundary’s children pipeline, emittingfallback(which can itself be async) instead of propagating. Boundaries compose — an inner Suspense’s catch produces a fallback string the outer boundary sees as success, so nesting “just works”. SyncrenderToStringof a Suspense emits fallback directly (the body might contain promises the sync path can’t await). Tu call-site uses M6.1 named-args:Suspense(fallback: div { "Loading…" }) { AsyncChild() }— compiles toSuspense({ fallback: …, children: [...] }). 9 new tests covering rejection / composition / siblings / async fallback / sync fallback path / end-to-end viarenderPageAsync. -
Async path for SSR (— landed (#60):renderToStringAsync+renderPageAsync)Childgained aPromise<Child>member; syncrenderToStringnow throws a typedTuRenderErroron Promise children (was silently emitting[object Promise]). NewrenderToStringAsyncwalks the same shape andawaits any promise it meets, withPromise.all-style parallelism over each vnode’s children + array nodes so independentawait fetch(...)calls don’t serialize.renderPageAsyncaccepts both sync and async thunks.tu-shu/loadTuPageuses the async path so a.tupage exportingasync let Page = …round-trips through to HTML.@tu-lang/dommount/hydrate keep their sync contract — Promise children there throw a directive pointing at SSR or Suspense (#61). 11 new tests inpackages/runtime/tests/index.test.ts; full repo (414 tests across 10 packages) green. Design doc atdocs/SSR-ASYNC-DESIGN.mddrives the rest of the M6.11 ladder (#61 Suspense, #62 streaming).
Closed in M6.10.1
-
— landed:external JSreturn type can’t start with{parseRawTypeUntilBracenow disambiguates "type-literal{" vs "body-opener{" by looking at the immediately preceding token. If it’s a type-continuation token (:,&,|,=>,,,[,(,<,?), the{opens a type literal; the parser scans the matching}via the newfindBalancedBraceCloseand keeps consuming type tokens. Otherwise the{is the body opener. Inline object-shape returns likeexternal JS (xs: T): { ms: number; out: any[] } { … }now parse without a type alias workaround. -
Object-literal multi-prop shorthand— landed:{ x, y, z: 1 }peekObjectLitShapenow treatsIdent Comma(an Ident immediately followed by,) as an unambiguous object-literal signal; Block bodies don’t separate statements with,, so there’s no ambiguity.parseObjectPropaccepts an Ident with no following:as shorthand and synthesizes anIdentvalue expression with the same name —{ id, title }desugars to{ id: id, title: title }. Single-item{ x }deliberately stays a Block to preserve last-expression-returns semantics. Theexamples/js-compat/JsCompat.tuworkaround ({ id: id, title: title }) was reverted to native shorthand. -
Member compound assignment— verified+tested: the parser already desugars compound assign onobj.x += 1/arr[i] ||= "x"MemberExpr/IndexExprtargets toMemberAssignExprwith a syntheticBinaryExprvalue (parser.ts:455-475). The DEFERRED row was stale. Added regression tests covering: bareobj.x += 1, indexedarr[i] += 1, logicalobj.x ||= "default", and cell-backedcounts.a += 5(wherecountsis a top-level Signal.State; the desugar correctly emitscounts.get().a = counts.get().a + 5).
Closed in M6.10
Platform-API split — separate the universal runtime from browser-only DOM glue— landed:@tu-lang/domis now its own workspace package owningmount,hydrate,defineCustomElement, the diff/patch path, and typed re-exports of common DOM types (Event,MouseEvent,Element,HTMLElement,Node,RequestInit,AbortController, …).@tu-lang/runtimekeeps only the universal half:Signal,h,Fragment,VNode,Child,renderToString,renderPage,renderPageHtml. All four DOM-touching test files (mount/hydrate/diff/custom-element) moved topackages/dom/tests/. The vite plugin’s HMR template now emitsimport { mount } from '@tu-lang/dom', and every example/playground that mounts in a browser pulls from@tu-lang/dom. The LSP shadow gained a genericTU_PLATFORM_PACKAGESregistry — adding a future@tu-lang/node/@tu-lang/workersis one line. The release workflow (release.yml) was extended to pack@tu-lang/dom; npm “Pending Publisher” needs to be configured for the package’s first publish (mowtwo/tu, release.yml). The user’s design call (2026-05-02): “比如 document api,这玩意其实是 dom 专属的,按道理必须通过 external 来引入” — done at the runtime-function layer; type-level isolation is now its own deferred row above.
Closed in M3.13
CSS diagnostics inside— landed:style { … }validateCssBlocks(in the newcss-lsp.tsmodule) walks every style block in the program and runscssLs.doValidation. Diagnostic ranges come back in CSS-doc-relative coordinates and are translated to source-doc absolute (line, col, length) via the same offset-math the M3.12 hover path uses.checkTuSourceaugments its TS-only diagnostic list with these. Misspellings (colour: red) and other CSS-LS-known issues now squiggle on the offending property/value, not the surrounding let-decl. CSS diagnostics use acode: -1sentinel since CSS LS doesn’t carry TS-style numeric codes —tu checkand the LSP server suppress the[code]tag for those.
Closed in M3.12
CSS hover inside— landed:style { … }hoverAtTuPositionchecksfindCssContextAtfirst; if the cursor is in a CSS body, it delegates tocssLs.doHover()and translates the returned range from CSS-doc coordinates to source-doc coordinates so the LSP underline lands on the property/value the cursor pointed at. CSS hover content (a Markdown-formatted property doc with a link to MDN) flows back as theTuHover.contents. Falls through to tsserver only when the cursor is outside any style block.
Closed in M3.11
CSS completion inside— landed:style { … }blocks@tu-lang/lspnow depends onvscode-css-languageservice. The parser was extended to recordcssStart/cssEndbyte offsets on theStyleBlockAST node so completion can find the inner CSS span;completionsAtTuPositionchecks first whether the cursor is inside any StyleBlock’s CSS region and, if so, slices that text into a synthetic CSSTextDocument, callsls.doComplete()at the cursor’s CSS-relative position, and maps the items toTuCompletionItem. tsserver/HTML-tag/ClassRef paths skip entirely while inside a style block — CSS context is exclusive. CSS hover and diagnostics are split out as a fresh open row.
Closed in M3.10
Tu-aware completion (HTML tags + ClassRefs)— landed:completionsAtTuPositionnow augments tsserver’s results with two Tu-specific sources. (1) HTML tag names appear when the cursor is at expression-head — detected by tokenizing up to the cursor and checking that the previous token is one of{,(,,,=,:,=>, orelse. (2) Declared class names appear when the cursor sits right after a.inside a scoped component — found by parsing the source (with a placeholder-retry for incomplete forms likeclass: .) and looking up the host LetDecl’s style-block classes via the newgetScopedClassMapexport from@tu-lang/compiler. Existing tsserver-based completions stay; this only adds items, deduped by label. CSS-content completion insidestyle { … }is split out as a fresh open row for later.
Closed in M2.5 (empty-array widening)
— landed: codegen detects emptylet xs = []inferred asSignal.State<never[]>ArrayLitinitializers (without an explicit type annotation) and emitsnew Signal.State<any[]>(…)/new Signal.Computed<any[]>(…)so subsequent.set(["a", "b"])andfor item in xscalls don’t trip onneverinference. Explicit annotations (let xs: number[] = []) take precedence — the annotation fully determines the type.
Closed in M4 V1
Client-side— landed:hydrate(thunk, container)hydrateadopted existing DOM children instead of creating new ones, and later moved withmountinto the browser-only@tu-lang/dompackage. The first render keeps the same<button>/<input>instance pre- and post-hydrate, so focus / scroll /<input>.valuesurvives the server-to-client handoff. Event listeners (which SSR can’t serialize) and DOM-property props (value,checked,selected) are applied during the walk. Hydration splits any Text node that SSR fused from adjacent text vnodes (e.g."count = " count→ one Text “count = 0” → two Text nodes) so future cell mutations only touch the dynamic tail. Whitespace-only text nodes between elements (incidental from pretty-printed HTML) are skipped. Structural mismatches log hydration mismatch warnings and fall back to materializing the offending vnode. A newexamples/ssr/runs the full round-trip (renderToString → jsdom-with-pre-stamped-HTML → hydrate → simulated clicks) end-to-end. Pre-resumability — Qwik-style “no first-frame thunk re-execution” is deferred to a later M4 milestone.
Closed in M2.5
Array literals— landed: parser recognizes[a, b, c][in expression position as anArrayLit(the[/]tokens that M2.4 added for type spans now do double duty for value syntax). AST walkers (class-ref + style-block scanners) descend into elements. Codegen emits the JS-equivalent literal verbatim. Todo.tu now owns its empty/one/three-items controls via three private lambdas that build fresh items arrays inline — playground’s external controls block deleted, narrowing M1.14’s scope and closing the “Todo.tu needs its own controls” row.
Closed in M3.9
Synthesize component-prop interfaces in TS emit— landed: codegen now emitsexport interface ${Name}Props { p1: T1; p2: T2 }immediately before each exported lambda whose params are all typed. Lambdas with zero params, any untyped param, or non-exported scope are skipped — no fictionalunknownfields. JS-mode emission stays unchanged. Verified end-to-end via the Greeting.tu example:export interface GreetingProps { name: string }now precedes the const, giving downstream.d.tsconsumers a named, reusable shape.
Closed in M2.4
Type vs value namespace + type aliases (— landed:type X = …)type X = …andexport type X = …now parse as top-level declarations.typeis a contextual keyword (only triggers when followed byIdent =at statement boundary), so users keep the freedom to name a valuetype(lambda param, let-decl name, etc.) without conflict. The RHS is captured as a raw source slice and emitted verbatim into the TS shadow; JS mode erases the alias entirely. Lexer gained[,],|,&,;tokens — currently consumed only inside raw-type spans, but available to future expression syntax. Verifiedlet count: Counter = 0(using a previously-declared alias) and round-trips through tsserver. Type-namespace question dissolves automatically: TS already keepstypeandconstseparate.
Closed in M2.3
Imported names are always classified as functions in codegen— landed:compileWithMap/compileToTSWithMapaccept an optionalimportedNameKinds: Map<string, CellKind>so the caller can tell codegen which imported names are state / computed / function cells. Imports without an explicit kind still default to'function', preserving the standalone-compile path. The LSP’s shadow graph and the@tu-lang/viteplugin both build this map by AST-classifying each.tuneighbor’sexport letbindings; the M2.1 reactivity bug (importing aSignal.Statesilently dropped.get()injection) is fixed for both flows. Multi-hop re-export chains still fall through to'function'— small follow-up.
Closed in M1.14
Counter.tu owns its buttons— landed: Counter.tu now declares privateinc / dec / resetlambdas and wires them viaonClick:props on three buttons inside the component body. The playground’s externalcontrols: () => [...]block was deleted. The SSR run.mjs continues to demonstrate external mutation (mod.count.set(5)from JS), so both interactive and out-of-band paths are exercised. Todo.tu is left externally-driven for now — without Tu array literals, the .tu source can’t build a freshitemslist in-place; tracked in a narrowed open row.
Closed in M1.15
LIS-based move minimization in keyed reorder— landed:patchChildrenreplaces the forwardinsertBeforepass with a patience-sort longest-increasing-subsequence overnewToOld[]. Items whose old indices form an increasing subsequence (the stable middle) skip movement; the position pass walks right-to-left andinsertBefores only the others. Verified with a regression test: swapping list endpoints[A B C D E] → [E B C D A]was 4 moves before, exactly 2 (A and E) now. Same algorithm as Vue 3 / Inferno, O(n log n).
Closed in M1.13
— landed: scanner skips class tokens inside:global(.foo)escape hatch for unscoped selectors:global(...)regions (depth-tracked across nested parens) so they’re never registered as “declared” classes for the component’s hash. The rewriter strips the wrapper itself, emitting.legacy(unhashed) where the source had:global(.legacy). Compound selectors mix freely:.card :global(.icon)becomes.card-tu-h .icon.
Closed in M2.2
Annotated— landed: parser captures the raw source slice betweenlet X: type = …declarations:and=(depth-tracked across()/{}/<…>so generic args don’t terminate the type early). The slice is plumbed throughLetDecl.typeand emitted by codegen in TS mode only — JS mode continues to erase types. Wrapping rules match the value: lambdas pass the annotation through to the const directly, state cells emitSignal.State<T>, computed cells emitSignal.Computed<T>. Lets users override TS inference for opaque cells (let total: BigDecimal = computed(...)) and document component shapes (let App: () => VNode = …).
Closed in M1.12
Multi-class pug shorthand— landed: parser greedily consumes a.foo.bar().foo.bar.bazchain. Single-ref →ClassRef(unchanged). Multi-ref → a+-chain interleaved with" "StringLits, so codegen emits(("foo-tu-h" + " ") + "bar-tu-h")and the runtime sees a space-joined class string. Same chain works in non-shorthand position too:class: .foo.baris now valid.Pug-shorthand tag override— landed: the.foo(tag: "section")tag:prop is special-cased insideparsePugShorthandTail— extracted from the args, validated as aStringLit, and used as the synthetic TagCall’s tag. Default staysdivwhen omitted. Thetag:prop is consumed (never emitted as an HTML attribute) and a non-literal value (e.g.tag: someExpr) throws a parse error since the tag is a compile-time decision.
Closed in M3.8
LSP V2: rename— landed:renameAtTuPositioncallsLanguageService.findRenameLocationsand groups the results byfileName, mapping each TS textSpan back through the target shadow’stokenMappingsso cross-.tureferences receive the same edit as the local declaration. The new identifier is validated against Tu’s identifier rules (/^[A-Za-z_$][A-Za-z0-9_$]*$/) before any TS work, so a malformed rename never produces broken sources. LSP server advertisesrenameProvider: trueand assemblesWorkspaceEdit { changes }from the per-file edit groups. Verified end-to-end: renamingcountrewrites both decl and read; renamingCardfrom a call-site rewrites the import + call inApp.tuAND the declaration inCard.tu.
Closed in M3.7
LSP hover: cache LanguageService across hovers— landed: newpackages/lsp/src/lsp-session.tsowns a single-slot cache keyed by(rootSource, rootFilename)plus a snapshot of every transitively-imported.tufile’s mtime. Hover / completion / definition all delegate throughgetOrCreateSessioninstead of building a freshts.LanguageServiceeach call; the duplicatecreateLsHostwas deleted from each surface. Cache invalidates when the root text changes, the filename changes, or any imported file’s mtime advances. Disposal hook (disposeSessionCache) keeps tests isolated. The interactive loop (hover → click another ident → completion → goto-def) now reuses one TS Program.
Closed in M3.6
— landed:tu checkCLI commandtu check <file…>in@tu-lang/clicallscheckTuFilefrom@tu-lang/lspand pretty-prints each diagnostic aspath:line:col: SEVERITY [TS####] messagefollowed by a 3-line code frame with^^^carets sized by the source-byte token range from M3.2. Empty input, non-.tuextension, missing files, and any error-severity diagnostic exit1; clean files print a one-linetu check: N file(s) OKsummary and exit0. The CLI logic is exposed asrunCheck(args, options)from@tu-lang/cliso the test suite drives it without spawning a subprocess.
Closed in M3.5
LSP V2: goto-definition— landed: sameLanguageService+ reverse-mapping infrastructure as hover/completion. NewdefinitionAtTuPositioncallsgetDefinitionAtPosition, then maps each TSDefinitionInfoback through the target shadow’stokenMappings(the definition might live in a different.tufile when crossing imports). Definitions whosefileNamefalls outside the shadow graph (e.g.@tu-lang/runtime’s.d.ts) are dropped — we don’t surface internal.tsfiles as a goto target. LSP server now advertisesdefinitionProvider: true. Verified end-to-end: jumping from acountread to itslet count = 0lands on cols 11…15 of line 0; jumping from a cross-fileCard("hi")call lands on theCardident inCard.tu.
Closed in M3.4
LSP V2: completion— landed:completionsAtTuPositionreuses the shadow graph +LanguageServiceand callsgetCompletionsAtPosition. The reverse mapping was extended with aninclusiveEndflag so cursors sitting at exactlysrcEndof an identifier (the typical case while typing) still resolve — the cap on the interior offset goes fromjsWidth - 1(strict) tojsWidth(inclusive). LSP server advertisescompletionProvider: { resolveProvider: false }and maps TSScriptElementKind→ LSPCompletionItemKind. Verified that previously-declared idents, typed lambda params, and cross-.tuimported names all surface in the completion list.
Closed in M3.3
LSP V2: hover (type / docs at cursor)— landed: built on M3.2’sTokenMapping[]. NewmapSourceLineColToTSreverse-maps a(line, col)cursor in.tuto a TS byte offset using the same tightest-token algorithm as the diagnostic forward path. Shared shadow-graph helpers (buildShadowGraph,tuPathToTs,getTuCompilerOptions,Shadow) extracted topackages/lsp/src/shadow-graph.tsso bothcheckTuSourceand the newhoverAtTuPositionuse one BFS + one set of compiler options. Hover spins up ats.LanguageService(one-shot per call) backed by the shadow graph and callsgetQuickInfoAtPosition; results are formatted as Markdown-fenced TypeScript with optional JSDoc body. The originating source token’s range — notquickInfo.textSpan— drives the hover range, so hovering oncountunderlines exactlycount, not the surrounding statement. LSP server now advertiseshoverProvider: trueand routesconnection.onHoverthrough. Whitespace, Tu keywords (let,if), and punctuation (=,=>) gracefully returnnullbecause noTokenMappingcovers them.
Closed in M3.2
LSP V2: token-level diagnostic ranges— landed: every AST node now carriesstart/endbyte offsets (plus per-feature anchors likenameStart/tagStart/calleeStart). Codegen was refactored from string-returning emit to a streaming buffer that records aTokenMapping { jsStart, jsEnd, srcStart, srcEnd }for each emitted leaf token (idents, literals, callee names, param names, class refs).compileToTSWithMapreturns the fullTokenMapping[]alongside the V3 map; the LSP’smapTSRangeToSourcefinds the tightest TokenMapping containing the diagnostic’s TS span and uses its source range. Squiggles now bracket the offending token (e.g. just42for an arg-type mismatch, or"not a number"for a state-cell assign) instead of the wholeletheader. Per-statement mapping remains as the fallback for diagnostics that land inside synthetic emit (.get(), runtime import). Same plumbing also enables the M3 hover work — every source token has a known TS counterpart now.Token-level (vs per-statement) source maps— landed as a side-effect of token-level diagnostics:buildV3Mapnow folds the sameTokenMapping[]into the V3mappingsfield as additional segments, so browser stack traces (and any tool that consumes the standard source map) resolve to the precise source token’s start. Per-statement segments remain as anchors for emit regions that have no token (synthetic.get(), runtime import, control-flow scaffolding).
Closed in M3.1
LSP V2: cross-— landed:.tuimport resolutioncheckTuSourcenow BFS-walks the import graph from the root file. Every reachable.tuis compiled to a TS shadow and registered in the CompilerHost’s virtual fs, so tsserver resolves cross-.tuimports as if they were native.tsfiles. Compiler options gainallowImportingTsExtensions: truebecause Tu’s codegen rewrites./Foo.tu→./Foo.tsin the shadow. Cycles tolerated via seen-set. In-memory edits to NON-root files are not yet seen — those still come from disk; lifting this restriction is small follow-up work for incremental multi-doc editing.
Closed in M3 V1
LSP — diagnostics-only V1— landed:@tu-lang/lsppackage boots avscode-languageserverserver; on document events it runscompileToTS()+ the in-process TypeScript Compiler API and publishes diagnostics with positions mapped back to.tuline/col via the embedded V3 source map.vscode-tuactivates the client ononLanguage:tu. End-to-end smoke-tested: a typed-param mismatch (G(42)against(name: string) =>) produces TS error 2345 mapped to the right.tuline. Hover / completion / goto-definition are all V2 work tracked in new rows above.
Closed in M2.1
Cross-— landed: lexer + parser recognize.tuimport { X } from "./other.tu"import { … } from "…", codegen emits the ESM line verbatim (and rewrites.tu→.tsin the TS shadow). Imported names are classified asfunctionso reads emit as plain idents (no.get()). Verified end-to-end via examples/scoped split into 3 files (Scoped.tu imports RedCard.tu + BlueCard.tu) — real browser shows the two cards each with their own M1.8 hash.— landed alongside imports.export { X } from "./other.tu"re-exportsCross-— landed:.tuimportfollow-through in TS shadowcompileToTSrewrites.tusource paths to.tsso tsserver resolves the sibling shadow.
Closed in M2
Type system via TypeScript (Volar pattern) — V1— landed:compileToTS(source, options)emits TypeScript with preserved lambda param type annotations. tsserver INFERS the rest from the existing JS shape (new Signal.State(0)→Signal.State<number>, lambdas → return-typed-from-body, etc.). The.d.tsemit (tsc --emitDeclarationOnlyover the shadow) reflects M1.10’s public-surface decisions exactly: onlyexport letbindings appear. New rows above track the post-V1 polish (component-prop interfaces, style-class literal-type unions, annotatedlet X: type,tu checkCLI, cross-.tuimport follow-through).mount() bug: stop() didn’t clean up DOM— the playground sidebar accumulated stale subtrees when switching demos. Fix landed in the runtime alongside M2:stop()now removes the mount’s own DOM children (sibling DOM in the container is left untouched). Two new regression tests cover this.
Closed in M1.11
Remove— landed: dropped TokenKind.Match + Underscore, MatchExpr / MatchArm / MatchPattern AST nodes, parser branches, codegen emit + AST walkers. examples/todo’s pluralized label rewrites cleanly as a chainedmatchif/else if/else. The newfeedback_avoid_tc39_conflicts.mdrule (saved alongside this milestone) is now the gate for any future sugar — check TC39 stage-2/3 first.
Closed in M1.10
Module visibility design (— landed: bareexport/pub/default)letis module-private,export letis public. Parser accepts an optionalexportprefix on let-decls; codegen continues to honor theexportedflag. New rows above track the still-open module work (cross-.tuimport, re-exports, default export, type namespace).
Closed in M1.9
Source maps from compiled JS back to— V3 source map (per-top-level-statement) emitted both as inline data-URL footer and as the.tusourcemapfield returned bycompileWithMap/ the@tu-lang/viteloadhook. Token-level granularity tracked as a new row above.Better error messages with source location + caret—formatErrorhelper used by lexer + parser producesfile:line:colplus a 3-line code-frame caret. Threaded viacompile(src, { filename })and through@tu-lang/viteso Vite’s overlay shows the formatted message.
Closed in M1.8
These rows tracked the pre-M1.8 scoping gap; M1.8 fills them and they are removed from Open. Kept here briefly so the closure shows in commit diffs:
Style scoping (auto class hash or— landed via per-component FNV-1a hash + CSS rewrite[data-tu-…]attribute rewrite)Symbolic class ref— landed.card+ pug-style.card() {…}shorthand— verified safe in current lexer (nolet style = …user variable name conflicts withstyle { … }block{afterstyle-as-RHS triggers CSS mode); covered by behaviour rather than syntax change