Kina.ai

Specification

Phase 1, Section 13: Tuple projection

StoryLM-0 wiring slice. Not a Phase 1 exit-criteria replacement. Not a tensor operator. Names the components of an already-typed tuple.


1. What this section commits to

Section 12 returns (primal, g₀, …) from reverseMode. Section 11 updates w with an explicit g. This section lets a program name those components:

let (y, g) = handle f(w) with reverseMode in
w - lr.broadcast(shape(w)) * g

The form is a destructuring let. It is not fst / snd, not numeric field access, and not an MLIR tuple.get. The right-hand side is already a flat tensor tuple (Section 12). The left-hand side binds one identifier per component, left to right.

There is no new tensor primitive and no new FFI.


2. What this section does not commit to


3. Typing

Γ ⊢ e : (τ₀, …, τₙ₋₁)
n ≥ 2
names = [x₀, …, xₙ₋₁]
the xᵢ are pairwise distinct
Γ, x₀:τ₀, …, xₙ₋₁:τₙ₋₁ ⊢ e' : τ
─────────────────────────────────────────
Γ ⊢ let (x₀, …, xₙ₋₁) = e in e' : τ

Arity must match exactly. A non-tuple right-hand side is a type error. Duplicate binders are a type error.

Each τᵢ may be a tensor (the StoryLM-0 case) or any other value type the elaborator already produces. This slice does not add new component types.

Unused binders are allowed. The right-hand side is still evaluated.


4. Operational meaning / lowering

A destructuring let does not compute. After the right-hand side is emitted as m SSA values (Section 12 flatten), the binder xᵢ is that i-th SSA name in the remainder.

%y, %g = …
; xᵢ ↦ that SSA name

No tuple.get, no extra copies, no new linalg op.


5. Surface

S-expression Bootstrap .kina
(not required for this slice) let (y, g) = e in e'

At least two binders, comma-separated. Same in terminator as an ordinary let.

The StoryLM-0 numeric instance (LM-head layout from Section 11; neg VJP from Section 10; seed ones):

let f = fn(w: tensor<F32, [4, 8], @host>) -> tensor<F32, [4, 8], @host> {
  let y = -w in
  y
} in
let step = fn(
  w: tensor<F32, [4, 8], @host>,
  lr: tensor<F32, [1, 1], @host>
) -> tensor<F32, [4, 8], @host> {
  let (y, g) = handle f(w) with reverseMode in
  let lr_b = lr.broadcast([4, 8]) in
  let update = lr_b * g in
  w - update
} in
let w = tensor<F32, [4, 8], @host> in
let lr = tensor<F32, [1, 1], @host> in
step(w, lr)

g = -ones, so w' = w + broadcast(lr). y is bound and unused. f stays a pure tensor let-chain; projection is only in step.


6. IR

Typed IR constructor

LetTuple of string list * program * program * typ

in order (names, rhs, body, result_ty). The parser produces the surface form; the elaborator constructs LetTuple when the let pattern has two or more names. Ordinary Let is unchanged.

The AD pass rewrites children of LetTuple if that node appears outside a differentiated body. A LetTuple inside a reverseMode function body is unsupported (same failure as any non-Let/Var spine).


7. Tests


8. Open issues

  1. Nested patterns / wildcards. Not needed while reverseMode is flat.
  2. AD through projection. Would need VJPs for Tuple / LetTuple. Not required for StoryLM-0’s “handle, then update” shape.
  3. AdamW as a later composition using the same naming.
  4. Decoder-graph VJPs so the named gradient can be a gather / reshape / rank-4 matmul cotangent. Specified in Section 14.