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 undefined → null 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/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)@tu-lang/runtimeshipshydratealongsidemount. The first render adopts existing DOM children rather than creating new ones — 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. 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 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