Expand description
The TypeScript tree and printer (phase 7 of the compiler trajectory,
design/bynk-compiler-trajectory.md). Emission produces nodes
(TsProgram/TsStmt); printer::print is the only code in the
compiler that writes a character (R7.3).
Invariant: this crate depends on bynk-syntax only (for
bynk_syntax::span::Span, reused unchanged rather than redefined —
bynk-emit’s own TypeRef/Ir
machinery is not visible here, and never will be: a function taking one
would not compile, the dependency isn’t present, by design). cargo tree -p bynk-ts shows bynk-syntax and nothing else. Matches the same
load-bearing shape bynk-render’s own module doc states for itself
(bynk-render/src/lib.rs).
What exists here today (P7.5, #1307 → P7.8, #1313). The reference’s
own §7.1 sketch names four tree enums (TsStmt/TsExpr/TsType/
TsDecl) with real variants (Const, Let, Binary, Named, Class,
…), but only as a variant-name list, not a field-level design. P7.5
built only TsStmt’s Verbatim escape hatch (VerbatimOrigin-
tagged text, Q2, design/tracks/the-typescript-tree.md §3.2). P7.8
(#1313) adds the rest — TsExpr/TsType/TsDecl, plus real
TsStmt kinds — but not the sketch’s full variant list: only what
bynk-emit/src/emitter/events_fanout.rs (Arc C’s real next file —
P7.8’s own accepted proposal corrected the track doc’s stale schedule,
design/tracks/the-typescript-tree.md §6/§9) concretely needs, grounded
against that file’s own real shape. bynk-emit still builds no
TsProgram beyond Verbatim — Arc C’s own first slice is what starts
converting real emission into real nodes, file by file; this crate
exists so that conversion has somewhere to land, following R10.3’s own
“carve prospectively, at the moment the dependency appears” precedent
(bynk-strip’s own carve is the control case cited for this).
Structs§
- Printed
- The result of printing a
TsProgram: the emitted text, and its source map —Nonewhen no checkpoint resolved, either because no statement carried a span or every span fell outsidesource_text(SourceMapBuilder::to_v3’s own “nothing resolves” case). - Source
MapBuilder - Accumulates source-map checkpoints during emission. Lives behind a
RefCellonbynk-emit’sLowerCtxso the deep lowering chain and the declaration loop can both record without fighting the borrow checker. A sub-builder (one per spliced body) records against its local buffer, then ismerged into the module builder at the splice offset. - TsClass
Ctor - A class’s own constructor.
- TsClass
Field - One
classfield. - TsClass
Method - One class method.
- TsParam
- One function/method/constructor parameter.
- TsProgram
- A whole generated TypeScript module, as an ordered sequence of top-level
statements.
Vec<TsStmt>, plain — no richer container yet (P7.6’s ownArtefacts { docs: BTreeMap<PathBuf, Document> }is where a project’s documents get keyed; this is one document’s own tree). - TsStmt
- One statement — a
Verbatim-tagged escape hatch (still constructible only viaTsStmt::verbatim, per #1307’s Decision D — theverbatim_sitesprobe needs exactly one string to line-scan for), or, from this slice, a real structured kind. The real kinds have no such sealing: they’re normal typed constructors, not a “wrap opaque text” escape hatch, so theverbatim_sitesconcern that motivatesverbatim’s own single-constructor discipline doesn’t apply to them. - TsSwitch
Case - One
case/defaultarm of aTsStmtKind::Switch.test: Noneis thedefault:case — every realdefaultinworkers_entry.rsprints its body directly underdefault:with no{ }block, while every real non-defaultcase(regardless oftest) always prints a{ }-blocked body. - Violation
- One construct
verbatim_violationsfound, and the line it was on.
Enums§
- TsArrow
Body TsExpr::Arrow’s ownbodyshape (#1435, Arc E slice 1) — see that field’s own doc for why this is a widened field rather than a secondTsExprvariant.Expris every real site before this slice (and the overwhelming majority after it): the arrow’s body is one expression, printed with no surrounding braces.Blockisserialisation.rs’s ownFloatnon-finite guard, the first real statement-bodied arrow anywhere in this tree — printed as a real braced block, reusing this crate’s own printer’s existing compact-statement-list renderer (render_compact_stmts, the same oneTsStmtKind::InlineBlockalready shares withrender_branch’s own same-lineif/else) rather than a third copy of that “one physical line, semicolon-separated” logic. Every realBlocksite today is exactly this one-line IIFE shape (an arrow with no other real use of a genuinely multi-line block body has been found); a future multi-line block-bodied arrow is a real, separate gap this variant does not yet cover.- TsBinary
Op ??(events_fanout.rs’senv ?? {}), plus three more real operators #1321 (workers.rs) grounds:||/&&(the Bearer-header presence checks,__authz === null || !__authz.startsWith(...)/__authz !== null && __authz.startsWith(...)) and===/!==(pervasive throughout the file’s own tagged-result and header checks). #1323 (workers_entry.rs) grounds one more:>(the request-body- ceiling guard’s ownNumber(__contentLength) > <cap>— the one real site anywhere inbynk-emitthat needs a relational, not equality, comparison). Not the full JS/TS operator table (Decision B’s own “extend narrowly” posture) — see the printer’s ownbinary_precedence(bynk-ts/src/printer.rs, private) for why a nestedBinaryoperand’s parenthesisation needed to become precedence-aware once more than one operator existed. Arc C, step (11) (#1388) grounds one more:+(string concatenation) — the ICU-formatting cluster’s own dominant structural pattern, every literal/placeholder segment in a message template joins this way. Real JS/TS precedence (binds tighter than every comparison/logical operator this table already has) and real left-associativity ("a" + "b" + "c"needs no parens, the same way a same-operator||/&&chain already prints flat) both matter here, not just the operator symbol itself.- TsBinding
Name - A binding’s own name, in either of the two shapes
events_fanout.rsitself uses: a plain identifier (const subs = ...), or an object-destructuring pattern (const { events } = ...) — naming only the destructured properties themselves ({ a, b }), not the renamed ({ a: renamed }) or nested ({ a: { b } }) forms, since nothing in the grounding file needs either. - TsDecl
- A top-level declaration.
Import,Export,Interface,ConstDecl, andClasswereevents_fanout.rs’s own grounding (P7.8’s own note: “not the sketch’sFunction/TypeAlias— unused in the grounding file”). #1321 (workers.rs) needed both after all — a real gap the accepted proposal’s own Framing didn’t name (its own “no other gap surfaced” checkedTsStmt/TsExpr/TsTypeshapes, notTsDeclones):compose.ts’s ownexport function compose(env: Env, …) { … }is a top-level function declaration, and (when the Worker has agents or publishes events) atype DurableObjectNamespace = { … };fallback alias sits alongside it. Named explicitly as a deviation from the accepted proposal’s own catalogue, not silently added — both are mechanical, directTsDeclsiblings ofConstDecl/Classalready here, the same “small, grounded, same class of gap” this track’s own history repeatedly found and closed (P7.8’sAssign/Continue/TryCatch, Arc C slice 1’sComment). - TsExpr
- An expression. Only the shapes
events_fanout.rsconcretely uses (Decision B) — not the reference sketch’s fullTsExprlist (Arrow,Cond,TemplateLit,Spreadare all unused in the grounding file and deliberately not built here). Arc C slice 3 (#1321,workers.rs) addsArrow,OptionalMember/OptionalIndex(Decision A, gaps 3/4) — this slice’s own real, grounded needs. - TsLit
- A literal — the three kinds
events_fanout.rsuses (a string, a number,null), plusBool(#1323’s own real gap:workers_entry.rs’sCorsPolicy.credentials/SecurityPolicy.nosniffobject-literal fields are real booleans, e.g.credentials: true,nosniff: false— nothing before this slice’s own grounding ever built one). - TsObject
Entry - One entry of a
TsExpr::Objectliteral. OnlyPropexisted before #1321 (events_fanout.rs’s own grounding never needed the other three) —workers.rs’s own dominant shape (Decision A, gap 1) is a literal object whose entries are shorthand async methods (everyon call/on http/… wrapper attaches this way), its local capability-depsobject mixes bare shorthand names with explicitkey: valuepairs, and three real sites spread another object into one (gap 2, folded in here rather than as its own top-levelTsExprvariant — a spread only ever appears as an object- or array-literal entry in this file, never as a standalone expression, so scoping it to entry position is the narrower correct change). - TsType
- A type-position node.
Named(extended with type arguments — a real gap the reference sketch left unaddressed:Record<string, Array<{...}>>/Promise<Response>both need one, and a bareNamedwith no type-argument slot cannot represent either),Array(extended with areadonlymodifier — P7.9’s own real gap: everyList/Queryelement typebynk-emit’sts_type_ref*/ts_tyfamilies build isreadonly T[], not plainT[]),Object, andFn(P7.9’s own second real gap — the query-thunk wrapper(() => readonly T[])and a real parametered function type(a0: T0, …) => Retboth need one) — not the sketch’sUnion/Intersection/Literal/TypeParam/Readonly(still unused;readonlyhere is a modifier onArray, not the sketch’s own separateReadonlywrapper variant). - TsType
Member - One member of a
TsType::Objectstructural type — a property (Prop) or a method signature (Method, #1323’s own real gap:ack(): void,retry(): void,waitUntil(promise: Promise<unknown>): void— no body, a type-position sibling toTsObjectEntry::Method, which does carry one).Method’s own parameters reuseTsParamdirectly (not a bareVec<TsType>the wayTsType::Fn’s anonymous, positionally-numbered parameters do) —waitUntil’s own real parameter has a real name (promise) the printed text must show, unlikeFn’s callers, none of which have one to show. - TsUnary
Op !x(events_fanout.rs’s!Array.isArray(subs),!binding) andtypeof x(#1321,workers.rs’s own secret-probe idiom:typeof __secret !== "string") — the two unary operators real content uses, not the full JS/TS table.- Verbatim
Origin - Which family of residual, not-yet-converted emission a
TsStmt::verbatimstatement came from. A closed enum, deliberately — “makes the ratchet a compile-time construct, not a grep” (Q2’s own settling text). Named file-by-file as Arc C actually needs them (ast_importers’s own five-file floor is the precedent for how this track names residue), not pre-populated for the whole ~19-slice Arc C schedule up front.
Functions§
- Print
programto TypeScript text.source_name/source_textregister the.bynksource every statement’s own span is measured against — today always exactly one, since nothing spans two files yet;output_filenames the generated file in the source map’s ownfilefield. Only a top-level statement’s own span is recorded as a checkpoint (R7.4’s existing scope, unchanged by P7.8 — seeTsStmt::span’s own doc for why a nested statement’s span isn’t recorded yet). - print_
class_ method - Print a single
TsClassMethodon its own, atdepth— the class-method sibling ofprint_object_entry’s own “one fragment, not a whole document” entry point.depthmeans the class’s own depth (matchingprint_object_entry’s own convention exactly), so the method itself lands atdepth + 1, its own body atdepth + 2. #1359’s own real need:emit_provider’s own class wrapper stays hand-written text (each method’s own body needs a per-method source-map sub-builder/merge, and the wrapper’s own real spacing — no blank line between methods — genuinely differs fromTsDecl::Class’s own “one blank line before each method” policy,events_fanout.rs’s real convention, #1317), so each real method prints through here directly into that still-hand-written wrapper. Deliberately no automatic blank-line insertion of its own — the same “caller controls spacing” contractprint_object_entryalready established. - print_
class_ method_ and_ merge print_class_method’s own sibling (#1477): identical rendering, but merges any ofmethod.body’s own direct-childnested_maps intomapas they print, at the real print-time offset — no reverse-engineering the offset from the returned text afterward, the waybynk-emit’s ownemit_class_method_and_merge_source_map(emitter/emit.rs) has to today. Kept as a separate function, not an added parameter onprint_class_methoditself, so every existing caller’s own call sites are untouched — this is a strict addition.- print_
expr - Print a single
TsExpron its own — the expression-level sibling ofprint_type’s own “one fragment, not a whole document” entry point. Arc C, step (11) (#1388) need:emit_icu_placeholder’s ownSelectarm stays one opaque, hand-built block-bodied IIFE (at the time,TsExpr:: Arrowhad no block-body variant at all, and extending it for that one site was rejected as disproportionate) — its own arm VALUES,emit_sub_message’s now-realTsExprresults, still need stringifying back into that opaque host text. #1435 (Arc E slice 1) later addedTsArrowBody::Blockfor a genuinely different real site (serialisation.rs’sFloatguard);emit_icu_placeholder’s ownSelectarm was not reconverted along with it — out of that slice’s own scope — so this call site’s opaque text stays exactly as it was. No source-map/buffer machinery, matchingprint_type/print_stmt’s own scope exactly. - print_
object_ entry - Print a single
TsObjectEntryon its own, atdepth— the object-entry sibling ofprint_stmt’s own “one fragment, not a whole document” entry point,depthmeaning the SAME thing it does for this crate’s own internal multi-line-object renderer: the object’s own depth, so the entry itself lands one level deeper, matching an object built by that renderer exactly. #1337’s own real need:emit_attached_methods(a shared helper spliced intoemit_refined_type/emit_record_type/emit_sum_type’s own still-unconverted&mut Stringbuffers) now returnsVec<TsObjectEntry>instead of writing text directly — each caller renders the returned entries one at a time through this, the same P7.9/#1333 “keep the caller’s own signature, print just the fragment” pattern applied to an object-entry-shaped fragment instead of a whole statement or type. - print_
object_ entry_ and_ merge print_object_entry’s own sibling (#1477), theTsObjectEntry::Methodcounterpart toprint_class_method_and_merge/print_stmt_and_merge: identical rendering, but merges aMethodentry’s own bodynested_mapintomapas it prints, at the real print-time offset. Kept separate fromprint_object_entryitself so every existing caller’s own call sites are untouched.- print_
stmt - Print a single
TsStmton its own, atdepth— the statement-level sibling ofprint_type’s own “one fragment, not a whole document” entry point.bynk-emit‘s own #1333 need (emit_doc_block, a shared helper spliced into ~14 still-unconverted callers’ own buffers) wants one statement’s own printed text at a caller-supplied depth, not a wholeTsProgram— no source-map/buffer machinery, matchingprint_type’s own scope exactly, and reusingrender_stmt’s own exhaustive per-kind dispatch (this module’s own private renderer) rather than a second copy. Review of #1402: aTsStmtKind::Rawnested inside aSwitchcase’s own body (emit_stub_rhs’s ownReturnsEachdispatch, Arc C slice 33,tests_emit.rsslice C) carries the identical “no indent of its own, pre-indented at a fixed absolute depth” hazardrender_class_method’s andrender_multiline_object_entry’s owndebug_assert!s already guard —stmt_contains_rawalready recurses intoSwitchcases, so the same check applies here, this fragment entry point’s own firstRaw-bearingSwitchcaller. A bareRawpassed directly asstmtitself is exempt (not a false negative —render_stmt’s ownRawarm never readsdepthat all, so callingprint_stmton a bareRawis safe at any depth, the establishedprint_stmt_renders_raw_text_verbatim_with_no_ added_indent_or_punctuationcontract below): only a Raw nested inside a depth-using wrapper (like thisSwitchcase) is the real hazard. - print_
stmt_ and_ merge print_stmt’s own sibling (#1477), theTsDecl::Function-body counterpart toprint_class_method_and_merge: identical rendering, but merges anynested_mapstmtcarries — onstmtitself, or on anything nested inside it (a function’s body, a class’s constructor/ methods, …) — intomapas it prints, at the real print-time offset — the same offsetbynk-emit’s ownemit_free_fn(emitter/emit.rs) used to recover by exact arithmetic over the returned text, guarded by adebug_assert!, before #1480 converted it to setnested_mapdirectly instead. Kept separate fromprint_stmtitself so every existing caller’s own call sites are untouched.source_idismap’s own registered source this statement’s content belongs to (see this crate’s own privateMergeTarget’s own doc for why this can’t just be hardcoded).- print_
type - Print a single
TsTypeon its own — the real callers this closes R7.2 for (bynk-emit’sts_type_ref*/ts_tyfamilies, P7.9, #1315) each want one type fragment to interpolate into a larger, still-hand-built line (a field’s own type annotation, a parameter list, …), not a wholeTsProgram. No source-map/buffer machinery —print()owns that for a whole document; this is the printer’s other, narrower entry point, sharing the same internal recursion rather than a second copy. - verbatim_
violations - Scan
text(aVerbatimstatement’s own wrapped TypeScript) for every line matching one of the six banned constructs. Order of the checks within a line matters only for whichconstructlabel a line already matching two patterns gets — real emitted lines don’t do that in practice, so the first match wins and the rest of that line isn’t checked further.