/** * typed.ts — typed-emit の language-neutral フレームワーク(bc#46 / Layer B2)。 * * typed-codegen.md §4.2/§4.3 の骨組み。可搬型記法(`PortableType`, B0/bc#44)を「各言語の型宣言/ * 型注釈へ落とせる language-neutral な型プラン」へ導出する枠と、直線 emit({@link StraightlineDialect})を * typed に拡張する seam({@link TypedStraightlineDialect})を提供する。TS 具体化は * `emit-straightline-typed-typescript.ts`(言語識別子 `typescript-typed`)。go/rust(#47/#48)が * 同じ seam に乗る。 * * 設計の核(§4.2「全ワイヤ型は SCP 構造の決定関数」): * - 型はワイヤの導出プロパティ。consumer(graphddb, B1/#45)が lowering 時に各ノードへ確定型を * `outType`/`outputType` として注記する。generator はレジストリを持たず、注記された型記法だけを * 読む(DSL 非依存)。 * - **language-neutral 部(ここ)**: 型記法 → 「型プラン」(named struct/interface の集合 + 各注記点の * 型参照)の導出。obj は決定的な名前を振って dedup(同形 obj は同一宣言を共有)。arr/opt/scalar は * inline 参照。名前生成・走査順・dedup は言語非依存で一度だけ実装する(go/rust/ts 共通)。 * - **per-language 部(dialect seam)**: 型プランの各ノードを「その言語の型構文(TS interface / * Go struct / Rust struct)」へ綴じる + typed materialization(scope/output を typed 値で組む・ * `ref` を typed field access で行う)を emit する。 * * TS における位置づけ(oracle): TS は型消去のため **runtime 脱box は無い**(生成コードの runtime 挙動は * straight-line と同一)。typed 経路は「`outType` から型付き interface/型注釈を emit → `tsc` clean + * emit された型が `outType` と構造一致(構造 pin)」を権威づける検証用 oracle であり、raw→typed の * 実 perf 脱box は go/rust(#47/#48 の materialization)+ graphddb #60(marshaller 実測)の責務。 */ import type { ComponentGraphIR, PortableType } from "../behavior.js"; /** * TypeRef — 注記点(node.outType / component.outputType)の型を language-neutral に表す。 * - scalar: 5 種のスカラ型(そのまま各言語のスカラ型へ落ちる)。 * - opt: nullable(`T | null` / `Option` / …)。 * - arr: 同種配列(要素型)。 * - map: 動的 string キー・同種値型のマップ(`Record` / `map[string]V` / * `BTreeMap`)。arr の map 版。canonical 直列化はキーを code point 順にソート。 * - named: obj を named struct/interface として宣言し、その名前を参照する(Go/Rust は無名ネスト * struct を跨げないので named 化が必須。TS も可読性・dedup のため named interface を使う)。 */ export type TypeRef = { kind: "scalar"; scalar: "string" | "int" | "float" | "bool" | "null"; } | { kind: "value"; } | { kind: "opt"; inner: TypeRef; } | { kind: "arr"; elem: TypeRef; } | { kind: "map"; value: TypeRef; } | { kind: "named"; name: string; }; /** named struct/interface 宣言(obj 型 1 つ)。fields はキー順保存(型記法の宣言順)。 * `nominal`(#192・build-time plan metadata・生成物には出ない): true = 宣言型(名前 dedup)/ * false|absent = 匿名型(構造 dedup + `T${index}`)。deriveTypeRef の曖昧さ回避に使う * (同形の named と anon が共存するとき、anon 参照が named decl に誤マッチしないよう)。 */ export interface TypeDecl { name: string; fields: { name: string; type: TypeRef; }[]; nominal?: boolean; } /** * TypePlan — IR 全体から導出した型プラン。 * - decls: named 宣言(obj 型)を **決定的順序**(初出順)で並べたもの。同形 obj は 1 宣言に dedup。 * - node/output の各注記点の型参照は {@link deriveTypeRef} が都度返す(プラン共有の decls を参照)。 */ export interface TypePlan { decls: TypeDecl[]; } /** * buildTypePlan — IR 全体を走査し、全注記点(body ノード `outType` / component `outputType`)の obj 型を * named 宣言に dedup 収集した {@link TypePlan} を返す。SCP-only mandatory typing 下では未注記ノードは * codegen 入口(generateModule)の gate で `UNTYPED_NODE` compile エラーになるため、ここに届く IR は * 全ノードが型付きである。decls が空になり得るのは NAMED(obj)型が 1 つも無い良性ケースだけ(スカラは * inline で型付き)で、その場合も untyped fallback は存在しない。language-neutral・決定的。 */ export declare function buildTypePlan(ir: ComponentGraphIR): TypePlan; /** * outputReachableDeclNames — the set of {@link TypeDecl} names REACHABLE from an OUTPUT annotation point * (a body node's `outType` or a component's `outputType`), transitively through named / arr / opt / map * fields. This is the language-neutral definition of "a struct that appears in an OUTPUT type": exactly the * OUTPUT roots {@link buildTypePlan} walks, MINUS the input-port schemas (`portSchemas` / `inputPorts` * `elemType`), which contribute INPUT-ONLY structs. * * It is the set the compile emitters (go/rust) de-box implicitly — their inline de-box is driven from these * same output roots (`buildDeBoxPlan` over each node `outType` / component `outputType` in * emit-typed-native-{go,rust}.ts), so it NEVER visits an input-only struct. A `value`-carrying INPUT-ONLY * struct (e.g. a dynamic-WHERE fragment plan whose `params` field is `value[]`) has no output * materialization at all (bc#156: `value` is input-only). Exposing the reachable set here lets the TS * marshaller emitter make the SAME decision instead of emitting an output de-box for every decl in * `plan.decls` (which mixes input and output structs), where an input-only `value` field fails closed. */ export declare function outputReachableDeclNames(ir: ComponentGraphIR, plan: TypePlan): Set; /** * deriveTypeRef — 単一の注記点の型記法を(buildTypePlan と同じ dedup 規律で)TypeRef へ落とす。 * ただし named 宣言の同定はプランの走査で既に確定しているため、ここでは同じ structuralKey で * プラン内 decl を引く(プランに無い obj 型は渡されない前提 — buildTypePlan が全注記点を網羅する)。 */ export declare function deriveTypeRef(t: PortableType, plan: TypePlan): TypeRef; /** * OutputSlotLowering — how a value of type V is lowered into a typed OUTPUT slot (a component's output, * an output struct/interface field, an output array element) whose DECLARED type is D, and the type the * value HAS ONCE PLACED there. * * - `identity` — V is placed as it stands; `placed` is V. * - `optSome` — D is `{opt:W}` and V is not an opt, so V is placed in the opt's PRESENT form; `placed` * is `{opt:V}`. This is the write side of a declared-nullable slot (`unproducibleNodeIds` makes the * field nullable because the node can be UNPRODUCED; the value here is the PRODUCED one). It is NOT a * default and NOT a coalesce — absence is written by the emitter's own unproduced arm, never here. */ export type OutputSlotLowering = { kind: "identity" | "optSome"; placed: TypeRef; }; /** * lowerIntoOutputSlot — the ONE rule for "what may be placed into a typed output slot, and as what type". * Language-neutral: it decides the SHAPE of the lowering; each emitter spells it in its own syntax * (rust `Some(x)`; go a fresh-pointer IIFE, since `&` on a temporary is not addressable; TS the * EMPTY spelling — `T` is assignable to `T | null`, so a union slot admits its member with nothing to * construct). The two halves must agree, so every site that performs the lowering reads `kind` for the * spelling AND passes `placed` to {@link makeOutputFieldTypeCheck}'s check. * * A site that CANNOT perform the lowering (it emits the value bare into a struct literal) must pass the * value's own type instead, so the check fails closed rather than blessing code the toolchain rejects. */ export declare function lowerIntoOutputSlot(declared: TypeRef | undefined, value: TypeRef): OutputSlotLowering; /** * OutputFieldTypeCheck — the fail-closed gate one emitter binds ONCE ({@link makeOutputFieldTypeCheck}) and * every output slot in that emitter runs. `fieldType` is the slot's DECLARED type, `valueType` the type of * the value ACTUALLY PLACED there (a {@link lowerIntoOutputSlot} `placed`). */ export type OutputFieldTypeCheck = (compName: string, fieldPath: string, fieldType: TypeRef | undefined, valueType: TypeRef | undefined) => void; /** * makeOutputFieldTypeCheck — build the fail-closed OUTPUT_TYPE_INCONSISTENT gate for one language. It * compares the slot's DECLARED type against the type of the value ACTUALLY PLACED in it (the caller's * {@link lowerIntoOutputSlot} `placed`), both rendered by that emitter's OWN `renderTypeRef` — so the * comparison is the target language's type identity, including the struct NAME two structurally identical * rows differ by. * * For go/rust that identity IS the compiler's rule (nominal, invariant), so a mismatch is exactly the * error the toolchain would report. For TS, whose rule is ASSIGNABILITY, rendered equality is STRICTER * than `tsc`: the admitted set is {identity} ∪ {the lowerings `lowerIntoOutputSlot` covers}, and anything * outside it fails closed even where `tsc` would accept it. So the gate guarantees "never emit a module * the toolchain rejects", NOT "fires only when the toolchain would reject" — on TS it is deliberately the * narrower of the two, and a rejection is a LOUD generation failure rather than a silently-wrong module. */ export declare function makeOutputFieldTypeCheck(lang: string, compilerReject: string, render: (ref: TypeRef) => string): OutputFieldTypeCheck; /** * SlotRefLowering — an emitter-supplied lowering for a NODE-RESULT REFERENCE (`ref`/`refOpt`) reached at a * typed output slot. The ONE output recursion consults it at every slot it descends into (the component * output, an output struct field, an output array element, an operator operand) BEFORE its own `ref` * handling, and keeps handling everything it returns `undefined` for (a literal, an input-param read, an * obj/arr assembly, an operator). * * This is the seam that keeps the recursion SINGLE. The produced-aware output lowering (#86 pt1 / * {@link lowerProducedAwareRead}) used to be a SECOND recursion that copied the obj branch per language and * therefore never reached an array element — the position it could not reach was fail-closed rather than * covered (#293/#295). */ export type SlotRefLowering = (node: unknown, declared: TypeRef | undefined, fieldPath: string) => { expr: string; ref: TypeRef | undefined; } | undefined; /** * ProducedAwareSpelling — the per-language SPELLINGS {@link lowerProducedAwareRead} needs. Everything else * about that lowering is language-neutral and lives in the rule: which arm is taken, what the unproduced arm * holds, which type both arms must agree on, and when the slot cannot represent the unproduced read at all. */ export interface ProducedAwareSpelling { /** the boolean expression reading node `head`'s produced flag (go `produced_x`; rust `produced_x.get()`). */ producedFlag(head: string): string; /** the PRESENT form of a value lowered `optSome` into `placed` — the `optSome` half of * {@link lowerIntoOutputSlot} (go a fresh pointer, since `&` on a temporary is not addressable; * rust `Some(..)`). */ optSome(valueExpr: string, placed: TypeRef): string; /** the ZERO value of a type in this language (go `nil` / `T{}` / `""`; rust `None` / `T::default()`). */ zero(ref: TypeRef): string; /** a conditional EXPRESSION whose two arms both have type `placed` (go an IIFE — Go has no expression * `if`; rust `if c { a } else { b }`). */ conditional(cond: string, present: string, absent: string, placed: TypeRef): string; } /** * unproducedInSlot — the ONE spelling of "the UNPRODUCED value of a node, as the slot type `placed` holds * it". `run_behavior`'s writeUnproduced writes `unproducedValue(relationKind)` into a skipped node, so what * a reader sees is decided by `relationKind` (its ABSENCE means null — builder.ts owns that rule): * - relationKind ABSENT → null at every depth (`refOpt` propagates it) ⇒ the slot's zero, which is null * in both languages BECAUSE the slot is an opt (the caller enforces that — see * {@link lowerProducedAwareRead}). * - `relationKind:"connection"` read WHOLE → the EMPTY connection, whose rep is the connection struct * ZERO (its serializer normalizes an empty/nil items to `[]` and a nil cursor to `null`, so the zero * serializes to `unproducedValue("connection")` byte-for-byte). An OPT slot still holds that empty * connection, never null — so the zero is lowered through the SOME spelling. * - `relationKind:"connection"` read INTO a key the empty connection HAS (`.items` → `[]`, `.cursor` → * `null`) → the slot's own zero, which IS that value in both languages (an empty slice/Vec serializes * to `[]`, an opt zero to `null`). WHICH keys those are is not decided here: the caller's floor asks * {@link holdsUnproduced}, so a read of any other declared field never reaches this function (#305). * * Read by BOTH positions that place an unproduced value into a typed slot: the produced-aware output read * and the `map.into` attach point of a guard-skipped element (#177/#182, where the read is always WHOLE). */ export declare function unproducedInSlot(placed: TypeRef, relationKind: "connection" | undefined, whole: boolean, spell: ProducedAwareSpelling): string; /** * lowerProducedAwareRead — the ONE rule for reading a node that can be UNPRODUCED (#86 pt1) at a typed * output slot: ` ? : `. * * The PRESENT arm is an ordinary output-slot lowering, so it goes through the same * {@link lowerIntoOutputSlot} + {@link OutputFieldTypeCheck} pair as any other slot — `placed` is both the * type the check is given AND the type the two arms (and the conditional itself) must have. * * The ABSENT arm is {@link unproducedInSlot}, which can only be spelled when the slot can REPRESENT the * unproduced read AT THE READ PATH. That is not a rule this file owns: it is `behavior.ts`'s * {@link holdsUnproduced}, which walks the SSoT unproduced value ({@link unproducedValue}) by the path and * asks whether the type holds what it lands on — so the set of paths an unproduced node HAS (`{items, * cursor}` for a connection, nothing at all for a null) is decided there and spelled nowhere else. The * floor FAILS CLOSED wherever that walk says no: * - the walk lands on null (a node with no `relationKind`, read WHOLE or crossed by a `refOpt`) and the * slot is not optional — the zero is a real value (go `""`, rust `String::default()`), so emitting it * would return a value where `run_behavior` raises NULL_REF; * - the walk cannot continue (a key the unproduced value does not have — every declared field of a * connection beyond `items`/`cursor`, #305) — `run_behavior` raises MISSING_PROP there on EVERY * surface while the zero would be returned silently; * - a hard `ref` crosses a null intermediate — NULL_REF on the interpreter, and only `refOpt` can spell * the null (`refCore`), which is why `optional` is part of the question. * Either way the divergence is the one #233 forbids. `type-gate.ts`'s resolveOutputType descends an obj * field, an array element and the output itself; it does NOT descend into an operator's operands, so this * floor is the only thing standing at an operand. */ export declare function lowerProducedAwareRead(args: { /** the FULL ref path (`[head, …rest]`) — its head names the node, its tail is the read path. */ path: readonly string[]; relationKind: "connection" | undefined; /** whether the reference is a `refOpt` (a null intermediate propagates instead of raising NULL_REF). */ optional: boolean; /** the emitted read of the PRODUCED value (a typed field access) and its type. */ valueExpr: string; value: TypeRef; /** the slot's declared type. */ declared: TypeRef | undefined; compName: string; fieldPath: string; check: OutputFieldTypeCheck; spell: ProducedAwareSpelling; /** the type plan — the slot type is asked in PortableType terms, which is what holdsUnproduced reads. */ plan: TypePlan; }): { expr: string; ref: TypeRef; }; /** * inputPortIsOptional — その input port が **省略可(値が「無い」= null を取り得る)**か。 * * 可搬宣言面の `{opt: T}` は `PortSchema` へ `required:false` として降りる(authoring.ts * `portSchemaFromPortable`)。`required === false` が optional の**唯一の印**であり、未指定/`true` * は required(type-gate.ts `portableTypeOfInputPort` と同一規則 — 規則を二重定義しない)。 */ export declare function inputPortIsOptional(schema: { required?: boolean; } | undefined): boolean; /** * inputPortTypeRef — input port schema の **language-neutral な型**(go/rust の native emitter が * 共通に lower する SSoT)。optional(`required:false`)なら `{opt: …}` で包む(各言語は renderTypeRef が * `Option` / `*T` へ綴じる)。 * * port 型語彙は読まない — {@link portableTypeOfInputPort}(behavior.ts、可搬型の所有層)が唯一の reader で、 * ここはその PortableType を TypeRef へ写すだけ。以前はここに 2 つ目の switch があり、語彙が型ゲート側と * 食い違っていた(ここは `object`/`arr`/`literal`/`number`/`value` を知り `null` を知らず、型ゲートはその逆)。 * 食い違った側では宣言が黙って「型無し」になる。 * * 確定できない `type`(`unknown`、elemType 無しの array/map 等)は `undefined`(呼び側が fail-closed する * か boxed 経路を保つ)。**optional の内側が確定しない場合も `undefined`** — 呼び側は「optionality を * 表現できない port」として LOUD に fail-closed する(zero 値への黙った縮退を作らない)。 */ export declare function inputPortTypeRef(schema: { type?: string; required?: boolean; elemType?: PortableType; } | undefined, plan?: TypePlan): TypeRef | undefined; /** * TypedMaterializer — per-language の typed 具体化 seam。language-neutral な {@link TypePlan} と * 各注記点の {@link TypeRef} を受け取り、その言語の「型宣言(interface/struct)」と typed * materialization(scope/output を typed 値で組む・`ref` を typed field access で行う)を綴じる。 * * ── decls 部(B2-ts / bc#46 で確立・型テキスト描画)────────────────────────────── * `emitTypeDecls` / `renderTypeRef` は型記法 → その言語の型宣言テキスト(TS interface / Go struct / * Rust struct)を綴じる。TS(`emit-straightline-typed-typescript.ts`)はこれと型消去 oracle だけで * 完結する(runtime 脱box が無いため)。 * * ── runtime-materialization 部(B2-go / bc#47 で co-design・**最初のコンパイル言語で拡張**)───── * TS は型消去のため runtime typed materialization を持たない → decls 部だけでは **コンパイル言語の * 実 de-box(raw Value → 具体 struct)を綴じられない**。そこで **最初のコンパイル言語(go)で seam を * 拡張**し、以下の hook を足す(rust #48 が同じ形で乗る)。すべて **language-neutral な入力** * ({@link TypePlan} / {@link TypeRef} / node id / field path / 生の raw 式テキスト)を受け、per-language が * その言語の materialization 構文へ綴じる(型プラン導出=共有、materialization=per-lang)。 * * - {@link emitMarshallers}: 型プランの各 named 宣言に対し「raw(動的 Value)→ その struct」の * **monomorphized marshaller**(§4.4)を emit する。struct field を 1 つずつ typed に読み出して * 具体 struct を組む(generic な Value 走査を exec 経路から除去する de-box の実体)。 * - {@link materializeExpr}: 「raw な Value 式(handler 結果 / scope 読み)」を、注記型 {@link TypeRef} * の **具体 typed 値**(struct / scalar / slice)へ変換する式を emit する(marshaller 呼び / 直キャスト)。 * - {@link typedFieldAccess}: 既に typed な base 式(materialize 済みローカル)への **静的 field access** * (struct field 直参照 — map lookup でない)を emit する。ネスト field path を辿る。 * - {@link serializeTyped}: 具体 typed 値を **canonical 直列化のための動的 Value へ戻す**式を emit する * (観測等価 pin のため。exec は typed のまま進み、境界でだけ Value に戻して golden と突き合わせる)。 * * decls-only の materializer(TS)は runtime hook 群を実装しなくてよい(optional)。コンパイル言語 * (go/rust)は実装する。呼び側(各 emitter)が hook の有無で typed exec を出すか oracle に留めるかを決める。 * * ── struct-native exec の規律(bc#47 監査 rework・rust #48 も従う)────────────────── * これらの hook を使う typed exec は **struct 空間で実行する**こと(LAYERED な Value-then-struct は不可): * - handler 結果は **その場で**(境界で)`materializeExpr`/marshaller により outType の具体 struct へ * de-box し、**typed scope(struct)** に格納する。動的 Value の node-result scope を組んではならない。 * - downstream の `ref`/field access は `typedFieldAccess`(struct field 直参照)で typed scope を読む。 * - component output は typed 値(struct/slice)として組み、`serializeTyped` で **最終 return 境界のみ** * Value へ戻す(動的 Value output tree を組んではならない)。 * - plan 駆動・handler dispatch・operator 意味論(overflow/短絡)は既存 SSoT(runPlan/handler/A0 primitive) * を再利用する(二重実装しない)。struct 空間へ移すのは **結果 materialization(handler 結果・scope・ * output)だけ**。localized な operand の Value 化(operator 被演算子など)は許容。 */ export interface TypedMaterializer { /** * 型プランの named 宣言群(obj 型)を、その言語の型宣言テキストへ綴じる(decls 順=決定的)。 * decls が空なら空文字(typed 化するものが無い)。 */ emitTypeDecls(plan: TypePlan): string; /** * 単一 TypeRef を、その言語の型注釈式(TS の型注釈・Go の型名・Rust の型名)へ綴じる。 */ renderTypeRef(ref: TypeRef): string; /** * emitMarshallers — 型プランの各 named 宣言に対する raw→typed marshaller 群を emit する(§4.4)。 * decls 順=決定的。decls が空 or 未実装なら空文字。marshaller は「動的 Value を受け、宣言の各 * field を typed に読み出して具体 struct を返す」monomorphized 関数(generic Value 走査を exec から除去)。 */ emitMarshallers?(plan: TypePlan): string; /** * materializeExpr — raw な Value 式(`rawExpr`)を、注記型 `ref` の具体 typed 値へ変換する式を返す。 * obj → marshaller 呼び、scalar → 型付きキャスト、arr → 要素 materialize、opt → null 分岐。 * これが exec 経路で「Value を struct に de-box する」実体(呼び側はこの式を typed ローカルへ束ねる)。 */ materializeExpr?(rawExpr: string, ref: TypeRef, plan: TypePlan): string; /** * typedFieldAccess — 既に typed な base 式(`baseExpr`、型 `baseRef`)に対し、field path(`fields`)を * **静的 struct field access** で辿る式と、辿り着いた型 {@link TypeRef} を返す。map lookup でなく * struct field 直参照であること(=de-box されている)が per-lang 実装の責務。 */ typedFieldAccess?(baseExpr: string, baseRef: TypeRef, fields: string[], plan: TypePlan): { expr: string; ref: TypeRef; }; /** * serializeTyped — 具体 typed 値式(`typedExpr`、型 `ref`)を、canonical 直列化のための動的 Value へ * 戻す式を返す(観測等価 pin 用。exec は typed のまま進み、境界でだけ Value 化して golden と突き合わせる)。 */ serializeTyped?(typedExpr: string, ref: TypeRef, plan: TypePlan): string; } /** 型プランから named 宣言を名前で引く(per-language materializer の共有ヘルパ)。 */ export declare function findDecl(plan: TypePlan, name: string): TypeDecl | undefined; //# sourceMappingURL=typed.d.ts.map