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
M8 — Type metadata system (interface as runtime placeholder) core M8 (in flight) User decided 2026-05-02 as HIGHEST PRIORITY. Strict duck typing: interface Foo { … } for object shapes (gets runtime descriptor + TS interface, both bound to the same name); type X = … STAYS for unions / string-literal aliases (TS-erased — tu-xing depends on these). API: type.of(v) and type.is(v, I) from @tu-lang/std (no new symbols, no ::). Composition value-level only: let b = {...a, extra: 1} merges descriptors. Replaces typeof / instanceof (both permanently banned). Phase 0 design doc landed (docs/TYPE-METADATA-DESIGN.md). Phase 1 runtime API landed (@tu-lang/std/type — primitives, Array(T), Optional(T), struct, native, tag, of, is — 18 tests). Phase 2 landed: interface keyword + codegen emits BOTH TS interface AND runtime descriptor const; auto-import of type from @tu-lang/std; tuTypeToDescriptorExpr translates Tu types to descriptor calls (primitives, arrays, nullable, function fallback); 12 in-repo files migrated (object-shape type X = {…}interface X {…}, unions stay). Phase 2.5 landed: typed-let type.tag(I, …) injection so type.of(alice) === User for let alice: User = {…} (locally-declared interfaces only). Phase 3 next: anonymous interface synthesis for untyped let a = {…} + shape interning (hash-key cache so same-shape literals share a descriptor) + import-graph classification (so imported interface names also tag). Phase 4 (typeof / instanceof ban wiring), Phase 5 (built-in JS-type descriptors + @tu-lang/std/time Temporal wrap). Generics deferred to M9.
JS legacy bans (parser-level rejections + auto-rewrites) core M8 (parallel) User decided 2026-05-02. Tu’s “强化+收敛 JS” stance translates to a concrete list of parser-level rejections — see memory/project_js_bans.md. Banned: ternary ?: (use if cond { … } else { … }), ++ / -- (use += 1), == / != (auto-strict at codegen — emit === / !==), instanceof (use type.is), typeof (use type.of), void operator, class, this, with, var, arguments, new Array(n), sparse [, ] (normalize to [null]), type X = … keyword (replaced by interface), Date (replaced by @tu-lang/time). Unify null and undefinednull only. Rule: bans apply to .tu source ONLY; external JS { … } block bodies pass through unchanged. Each ban ships with a parser error pointing at the replacement + a migration of in-repo .tu usages in the same commit.
@tu-lang/std/time — Temporal-based Date replacement M8 M8 Submodule of @tu-lang/std (decided 2026-05-03 against carving a separate package). Re-exports @js-temporal/polyfill’s namespace under Tu-friendly aliases. Tu rejects raw new Date() in user source. Subpath export so users can import { time } from "@tu-lang/std" or import { Instant } from "@tu-lang/std/time". See memory/project_temporal_date.md.
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.
Tu-native type conversion API (Convert / type.tryFrom) post-M8 TBD Replace JS implicit coercion ("1" + 1 === "11", +expr, etc.) with a typed Result-returning conversion API. Depends on M8 descriptor shape. Use external JS for one-off conversions today. See memory/project_type_conversion.md.
CSS4 nesting / @layer / @scope awareness in style block M1.4 M1.9+ M1.8 ships a regex-style class scanner that handles flat selectors and most nested rules correctly (the regex matches .foo anywhere, including inside nested blocks). Edge cases like :is(), @scope, and selector lists need a real CSS parser.
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.
Default export (export default …) M1.10 TBD Tu’s no-function-keyword aesthetic argues against it; revisit when component-as-file becomes idiomatic.
Object-literal computed keys ({ [expr]: v }) M5.6 TBD M5.6 ships { key: value }, M5.10/M6.5 spread ({ ...rest }), M6.10.1 multi-prop shorthand ({ x, y, z: 1 }). Single-item shorthand { x } stays a Block to preserve last-expression-returns. Computed keys still missing — add when a real use case shows up.
Remaining JS expression operators M5.10 TBD M5.10 + M6.5 covered ||, &&, ??, ?., ?.(), ?.[], obj[k], prefix ! / - / +, postfix ! (TS non-null), new (kept for genuine constructors only — new Promise / new Map / new Error), compound assign (+=, -=, ||=, etc. — for cell idents), spread ... (call args + array + object), template literals `${…}`, parens, TS optional params. PERMANENTLY BANNED (M8 / memory/project_js_bans.md): ternary ?: (use if), ++ / -- (use += 1), instanceof (use type.is), typeof (use type.of), void operator (operator only — type-position : void stays). Still candidates for piecemeal addition: regex literals /…/flags ✓ (already in), bitwise (& / | / ^ / ~ / << / >>), ** exponent.
TS-style as type assertion / cast M6.5 TBD Tu currently has no expr as Type cast. Surfaces as a real-world friction: (e: Event) => e.target.value doesn’t type-check because EventTarget has no .value, and there’s no cast to HTMLInputElement. Workarounds today: route through external JS (clean but heavier than a one-token cast) or take the param as any. Add as per parser-level support; codegen emits (expr as Type) to TS verbatim. The <Type>expr legacy form is intentionally NOT planned (collides with JSX-like generics).
Destructuring (let + param) M6.5 TBD let { a, b } = obj and ({ a, b }) => would let users skip the props.a prefix in component bodies. Needs a new pattern AST shape — invasive enough that it didn’t fit M6.5. M6.1’s named-arg call form already covers most of the prop-typing ergonomics.
generic / enum (no class — permanently rejected) M6.7 TBD class is permanently banned in Tu (M8 decision 2026-05-02 — components are lambdas, types are interfaces). enum and generic-lambda syntax (<T, U>) are still candidates 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).
Param destructuring ({ title, footer }) => … M6.1 M6.2+ M6.1 lets components receive a single props object via named-arg call sites, but the receiver lambda still writes (props) => p { props.title }. Param destructuring would let the body skip the props. prefix. Needs a new param-AST shape — defer until ergonomic friction shows up.
Auto-rewrite ${Name}Props to all-optional + children?: Child[] M6.1 M6.2+ After M6.1’s named-arg form, every prop is optional at the call site by construction; M3.9’s emitted interface should reflect that and always include children?: Child[]. Needs the props-object detection rule wired in.
Deprecate positional component calls M6.1 M6.3 Soft migration: both forms work through the alpha cycle, then warn-then-remove once tu-xing + migration tooling exist.
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.
class: / style: array + object syntax M2 post-LSP DX User asked 2026-05-02 — make class: accept ["a", maybeFalsy && "b"], { a: cond, b: cond2 }, and mixed forms (Vue / clsx / classnames shape); same for style: with object form { color: "red", fontSize: cell }. Today only string. Three deltas: (1) runtime prop-application path in @tu-lang/dom flattens arrays/objects to a string; (2) SSR renderVNode does the same; (3) TS shadow types class: as string | ClassValue where ClassValue covers the recursive shape. Pairs naturally with the style-class literal-type union (#5 below) so object keys can be typed.
Style-class literal-type union in TS emit M2 M6.12 / LSP DX Today the codegen rejects undeclared .classRef at compile time (M1.8). For IDE completion of .foo against the declared set, emit a type ClassesOf_X = "card" | "card__title" and type the class: prop accordingly. Required prerequisite for typing the keys of the new object form class: { card: cond, … }.
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.
Router system (SSR + Next.js-style) M0 M7 User flagged 2026-05-02 — Tu has no router. Two modes wanted: (a) minimal SSR-only routing layered on renderPageAsync / renderToStream; (b) Next.js-style file-based with app/[slug]/page.tu, dynamic segments, layouts, loaders, server functions. Major scope — own milestone. Tu-shu becomes a downstream consumer of mode (a). M6.11 Suspense + streaming is the data-loading substrate. 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.
if let a = x { … } bind-and-test sugar M5.6 post-LSP DX User flagged 2026-05-02 — distinct from the already-supported let a = if cond { 1 } else { 0 } (which is just expression-position if). The new form binds, tests truthiness/non-null, and scopes the binding to the then branch. Emits TS that tsserver narrows (if (const a = expr; a != null) { … }). Small parser + codegen change. See memory/project_if_let_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 M6.12

  • Static-HTML optimization (verify M6.0 actually works) — verified + proof example landed: examples/static-html/ ships Welcome.tu (a deeply-nested static <div> tree) + verify.mjs that compiles the source, asserts the output contains h("$static", {}, [], "<div>…</div>") (vs the ~10 nested h() calls a non-static tree would produce), then runs the result through renderToString and confirms the HTML round-trips with proper escaping. Run pnpm --filter @tu-examples/static-html test. The compiler’s isStaticTree (codegen.ts:1950) + countStaticNodes >= 3 threshold gate the optimization correctly; reactive subtrees fall back to h() calls.

  • Suspense + streaming SSR proof example — landed: examples/suspense/ ships Page.tu with two async UserCards wrapped in Suspense, plus a run.mjs that exercises both renderToStringAsync (await-all flavor) and renderToStream (per-boundary flush). Streaming output shows 5 chunks: shell + placeholders, the $tu_replace polyfill, 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 (renderToStream + <template> per-boundary flush) — landed (#62): renderToStream(thunk, options) returns a Web ReadableStream<Uint8Array> ready to pipe into a Response. 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_replace polyfill 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. New RenderToStreamOptions.onShellReady callback fires after the shell + placeholders enqueue, before resolution work starts. assemblePage was refactored to share assembleShellParts with 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.

  • <Suspense> boundary primitive — landed (#61): new $suspense tag sentinel + Suspense({ fallback, children }) runtime helper. The async render walk catches both Promise rejections and throws inside the boundary’s children pipeline, emitting fallback (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”. Sync renderToString of 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 to Suspense({ fallback: …, children: [...] }). 9 new tests covering rejection / composition / siblings / async fallback / sync fallback path / end-to-end via renderPageAsync.

  • Async path for SSR (renderToStringAsync + renderPageAsync) — landed (#60): Child gained a Promise<Child> member; sync renderToString now throws a typed TuRenderError on Promise children (was silently emitting [object Promise]). New renderToStringAsync walks the same shape and awaits any promise it meets, with Promise.all-style parallelism over each vnode’s children + array nodes so independent await fetch(...) calls don’t serialize. renderPageAsync accepts both sync and async thunks. tu-shu/loadTuPage uses the async path so a .tu page exporting async let Page = … round-trips through to HTML. @tu-lang/dom mount/hydrate keep their sync contract — Promise children there throw a directive pointing at SSR or Suspense (#61). 11 new tests in packages/runtime/tests/index.test.ts; full repo (414 tests across 10 packages) green. Design doc at docs/SSR-ASYNC-DESIGN.md drives the rest of the M6.11 ladder (#61 Suspense, #62 streaming).

Closed in M6.10.1

  • external JS return type can’t start with { — landed: parseRawTypeUntilBrace now 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 new findBalancedBraceClose and keeps consuming type tokens. Otherwise the { is the body opener. Inline object-shape returns like external JS (xs: T): { ms: number; out: any[] } { … } now parse without a type alias workaround.

  • Object-literal multi-prop shorthand { x, y, z: 1 } — landed: peekObjectLitShape now treats Ident Comma (an Ident immediately followed by ,) as an unambiguous object-literal signal; Block bodies don’t separate statements with ,, so there’s no ambiguity. parseObjectProp accepts an Ident with no following : as shorthand and synthesizes an Ident value 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. The examples/js-compat/JsCompat.tu workaround ({ id: id, title: title }) was reverted to native shorthand.

  • Member compound assignment obj.x += 1 / arr[i] ||= "x" — verified+tested: the parser already desugars compound assign on MemberExpr/IndexExpr targets to MemberAssignExpr with a synthetic BinaryExpr value (parser.ts:455-475). The DEFERRED row was stale. Added regression tests covering: bare obj.x += 1, indexed arr[i] += 1, logical obj.x ||= "default", and cell-backed counts.a += 5 (where counts is a top-level Signal.State; the desugar correctly emits counts.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/dom is now its own workspace package owning mount, hydrate, defineCustomElement, the diff/patch path, and typed re-exports of common DOM types (Event, MouseEvent, Element, HTMLElement, Node, RequestInit, AbortController, …). @tu-lang/runtime keeps only the universal half: Signal, h, Fragment, VNode, Child, renderToString, renderPage, renderPageHtml. All four DOM-touching test files (mount/hydrate/diff/custom-element) moved to packages/dom/tests/. The vite plugin’s HMR template now emits import { mount } from '@tu-lang/dom', and every example/playground that mounts in a browser pulls from @tu-lang/dom. The LSP shadow gained a generic TU_PLATFORM_PACKAGES registry — adding a future @tu-lang/node / @tu-lang/workers is 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 style { … } — landed: validateCssBlocks (in the new css-lsp.ts module) walks every style block in the program and runs cssLs.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. checkTuSource augments 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 a code: -1 sentinel since CSS LS doesn’t carry TS-style numeric codes — tu check and the LSP server suppress the [code] tag for those.

Closed in M3.12

  • CSS hover inside style { … } — landed: hoverAtTuPosition checks findCssContextAt first; if the cursor is in a CSS body, it delegates to cssLs.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 the TuHover.contents. Falls through to tsserver only when the cursor is outside any style block.

Closed in M3.11

  • CSS completion inside style { … } blocks — landed: @tu-lang/lsp now depends on vscode-css-languageservice. The parser was extended to record cssStart / cssEnd byte offsets on the StyleBlock AST node so completion can find the inner CSS span; completionsAtTuPosition checks first whether the cursor is inside any StyleBlock’s CSS region and, if so, slices that text into a synthetic CSS TextDocument, calls ls.doComplete() at the cursor’s CSS-relative position, and maps the items to TuCompletionItem. 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: completionsAtTuPosition now 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 {, (, ,, =, :, =>, or else. (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 like class: .) and looking up the host LetDecl’s style-block classes via the new getScopedClassMap export from @tu-lang/compiler. Existing tsserver-based completions stay; this only adds items, deduped by label. CSS-content completion inside style { … } is split out as a fresh open row for later.

Closed in M2.5 (empty-array widening)

  • let xs = [] inferred as Signal.State<never[]> — landed: codegen detects empty ArrayLit initializers (without an explicit type annotation) and emits new Signal.State<any[]>(…) / new Signal.Computed<any[]>(…) so subsequent .set(["a", "b"]) and for item in xs calls don’t trip on never inference. Explicit annotations (let xs: number[] = []) take precedence — the annotation fully determines the type.

Closed in M4 V1

  • Client-side hydrate(thunk, container) — landed: @tu-lang/runtime ships hydrate alongside mount. The first render adopts existing DOM children rather than creating new ones — same <button> / <input> instance pre- and post-hydrate, so focus / scroll / <input>.value survives the server-to-client handoff. Event listeners (which SSR can’t serialize) and DOM-property props (value, checked, selected) are applied during the walk. The runtime 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 [@tu-lang/runtime] hydration mismatch: … warnings and fall back to materializing the offending vnode. A new examples/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 [a, b, c] — landed: parser recognizes [ in expression position as an ArrayLit (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 emits export 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 fictional unknown fields. 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.ts consumers a named, reusable shape.

Closed in M2.4

  • Type vs value namespace + type aliases (type X = …) — landed: type X = … and export type X = … now parse as top-level declarations. type is a contextual keyword (only triggers when followed by Ident = at statement boundary), so users keep the freedom to name a value type (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. Verified let count: Counter = 0 (using a previously-declared alias) and round-trips through tsserver. Type-namespace question dissolves automatically: TS already keeps type and const separate.

Closed in M2.3

  • Imported names are always classified as functions in codegen — landed: compileWithMap / compileToTSWithMap accept an optional importedNameKinds: 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/vite plugin both build this map by AST-classifying each .tu neighbor’s export let bindings; the M2.1 reactivity bug (importing a Signal.State silently 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 private inc / dec / reset lambdas and wires them via onClick: props on three buttons inside the component body. The playground’s external controls: () => [...] 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 fresh items list in-place; tracked in a narrowed open row.

Closed in M1.15

  • LIS-based move minimization in keyed reorder — landed: patchChildren replaces the forward insertBefore pass with a patience-sort longest-increasing-subsequence over newToOld[]. Items whose old indices form an increasing subsequence (the stable middle) skip movement; the position pass walks right-to-left and insertBefores 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

  • :global(.foo) escape hatch for unscoped selectors — landed: scanner skips class tokens inside :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 let X: type = … declarations — landed: parser captures the raw source slice between : and = (depth-tracked across () / {} / <…> so generic args don’t terminate the type early). The slice is plumbed through LetDecl.type and 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 emit Signal.State<T>, computed cells emit Signal.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 .foo.bar() — landed: parser greedily consumes a .foo.bar.baz chain. 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.bar is now valid.
  • Pug-shorthand tag override .foo(tag: "section") — landed: the tag: prop is special-cased inside parsePugShorthandTail — extracted from the args, validated as a StringLit, and used as the synthetic TagCall’s tag. Default stays div when omitted. The tag: 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: renameAtTuPosition calls LanguageService.findRenameLocations and groups the results by fileName, mapping each TS textSpan back through the target shadow’s tokenMappings so cross-.tu references 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 advertises renameProvider: true and assembles WorkspaceEdit { changes } from the per-file edit groups. Verified end-to-end: renaming count rewrites both decl and read; renaming Card from a call-site rewrites the import + call in App.tu AND the declaration in Card.tu.

Closed in M3.7

  • LSP hover: cache LanguageService across hovers — landed: new packages/lsp/src/lsp-session.ts owns a single-slot cache keyed by (rootSource, rootFilename) plus a snapshot of every transitively-imported .tu file’s mtime. Hover / completion / definition all delegate through getOrCreateSession instead of building a fresh ts.LanguageService each call; the duplicate createLsHost was 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

  • tu check CLI command — landed: tu check <file…> in @tu-lang/cli calls checkTuFile from @tu-lang/lsp and pretty-prints each diagnostic as path:line:col: SEVERITY [TS####] message followed by a 3-line code frame with ^^^ carets sized by the source-byte token range from M3.2. Empty input, non-.tu extension, missing files, and any error-severity diagnostic exit 1; clean files print a one-line tu check: N file(s) OK summary and exit 0. The CLI logic is exposed as runCheck(args, options) from @tu-lang/cli so the test suite drives it without spawning a subprocess.

Closed in M3.5

  • LSP V2: goto-definition — landed: same LanguageService + reverse-mapping infrastructure as hover/completion. New definitionAtTuPosition calls getDefinitionAtPosition, then maps each TS DefinitionInfo back through the target shadow’s tokenMappings (the definition might live in a different .tu file when crossing imports). Definitions whose fileName falls outside the shadow graph (e.g. @tu-lang/runtime’s .d.ts) are dropped — we don’t surface internal .ts files as a goto target. LSP server now advertises definitionProvider: true. Verified end-to-end: jumping from a count read to its let count = 0 lands on cols 11…15 of line 0; jumping from a cross-file Card("hi") call lands on the Card ident in Card.tu.

Closed in M3.4

  • LSP V2: completion — landed: completionsAtTuPosition reuses the shadow graph + LanguageService and calls getCompletionsAtPosition. The reverse mapping was extended with an inclusiveEnd flag so cursors sitting at exactly srcEnd of an identifier (the typical case while typing) still resolve — the cap on the interior offset goes from jsWidth - 1 (strict) to jsWidth (inclusive). LSP server advertises completionProvider: { resolveProvider: false } and maps TS ScriptElementKind → LSP CompletionItemKind. Verified that previously-declared idents, typed lambda params, and cross-.tu imported names all surface in the completion list.

Closed in M3.3

  • LSP V2: hover (type / docs at cursor) — landed: built on M3.2’s TokenMapping[]. New mapSourceLineColToTS reverse-maps a (line, col) cursor in .tu to a TS byte offset using the same tightest-token algorithm as the diagnostic forward path. Shared shadow-graph helpers (buildShadowGraph, tuPathToTs, getTuCompilerOptions, Shadow) extracted to packages/lsp/src/shadow-graph.ts so both checkTuSource and the new hoverAtTuPosition use one BFS + one set of compiler options. Hover spins up a ts.LanguageService (one-shot per call) backed by the shadow graph and calls getQuickInfoAtPosition; results are formatted as Markdown-fenced TypeScript with optional JSDoc body. The originating source token’s range — not quickInfo.textSpan — drives the hover range, so hovering on count underlines exactly count, not the surrounding statement. LSP server now advertises hoverProvider: true and routes connection.onHover through. Whitespace, Tu keywords (let, if), and punctuation (=, =>) gracefully return null because no TokenMapping covers them.

Closed in M3.2

  • LSP V2: token-level diagnostic ranges — landed: every AST node now carries start / end byte offsets (plus per-feature anchors like nameStart / tagStart / calleeStart). Codegen was refactored from string-returning emit to a streaming buffer that records a TokenMapping { jsStart, jsEnd, srcStart, srcEnd } for each emitted leaf token (idents, literals, callee names, param names, class refs). compileToTSWithMap returns the full TokenMapping[] alongside the V3 map; the LSP’s mapTSRangeToSource finds the tightest TokenMapping containing the diagnostic’s TS span and uses its source range. Squiggles now bracket the offending token (e.g. just 42 for an arg-type mismatch, or "not a number" for a state-cell assign) instead of the whole let header. 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: buildV3Map now folds the same TokenMapping[] into the V3 mappings field 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-.tu import resolution — landed: checkTuSource now BFS-walks the import graph from the root file. Every reachable .tu is compiled to a TS shadow and registered in the CompilerHost’s virtual fs, so tsserver resolves cross-.tu imports as if they were native .ts files. Compiler options gain allowImportingTsExtensions: true because Tu’s codegen rewrites ./Foo.tu./Foo.ts in 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/lsp package boots a vscode-languageserver server; on document events it runs compileToTS() + the in-process TypeScript Compiler API and publishes diagnostics with positions mapped back to .tu line/col via the embedded V3 source map. vscode-tu activates the client on onLanguage:tu. End-to-end smoke-tested: a typed-param mismatch (G(42) against (name: string) =>) produces TS error 2345 mapped to the right .tu line. Hover / completion / goto-definition are all V2 work tracked in new rows above.

Closed in M2.1

  • Cross-.tu import { X } from "./other.tu" — landed: lexer + parser recognize import { … } from "…", codegen emits the ESM line verbatim (and rewrites .tu.ts in the TS shadow). Imported names are classified as function so 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.
  • export { X } from "./other.tu" re-exports — landed alongside imports.
  • Cross-.tu import follow-through in TS shadow — landed: compileToTS rewrites .tu source paths to .ts so 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.ts emit (tsc --emitDeclarationOnly over the shadow) reflects M1.10’s public-surface decisions exactly: only export let bindings appear. New rows above track the post-V1 polish (component-prop interfaces, style-class literal-type unions, annotated let X: type, tu check CLI, cross-.tu import 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 match — 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 chained if/else if/else. The new feedback_avoid_tc39_conflicts.md rule (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 (export/pub/default) — landed: bare let is module-private, export let is public. Parser accepts an optional export prefix on let-decls; codegen continues to honor the exported flag. New rows above track the still-open module work (cross-.tu import, re-exports, default export, type namespace).

Closed in M1.9

  • Source maps from compiled JS back to .tu source — V3 source map (per-top-level-statement) emitted both as inline data-URL footer and as the map field returned by compileWithMap / the @tu-lang/vite load hook. Token-level granularity tracked as a new row above.
  • Better error messages with source location + caretformatError helper used by lexer + parser produces file:line:col plus a 3-line code-frame caret. Threaded via compile(src, { filename }) and through @tu-lang/vite so 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 [data-tu-…] attribute rewrite) — landed via per-component FNV-1a hash + CSS rewrite
  • Symbolic class ref .card + pug-style .card() {…} shorthand — landed
  • let style = … user variable name conflicts with style { … } block — verified safe in current lexer (no { after style-as-RHS triggers CSS mode); covered by behaviour rather than syntax change