SeaBreeze Principal Join
SeaBreeze is published as typesea/seabreeze. It is deliberately not re-exported
from the root entry point, so ordinary validator users do not pay import or
bundle cost for arena-backed inference.
SeaBreeze is a low-level TypeSea inference algorithm that combines two ideas:
- Hindley-Milner style variables and union-find representatives.
- Best-common-type recovery when concrete constructors cannot be unified.
The result is not a general-purpose TypeScript type checker. It is a validation type solver for TypeSea’s runtime domain. Its job is to compute the smallest useful validator shape for multiple candidates without allocating one JavaScript object per intermediate type.
Core Rule
principalJoin(a, b) returns a node id in a fixed arena.
Var(x) join T -> bind x to T
Never join T -> T
Unknown join T -> Unknown
Number join Number -> Number
Array<A> join Array<B> -> Array<principalJoin(A, B)>
Object<A> join Object<B>
-> object whose common keys are required,
drift keys are optional,
and shared field types are joined recursively
T join U -> Union<T, U> Hindley-Milner alone would fail at number vs string. TypeSea cannot stop
there because validator inference often needs a useful supertype. Best common
type alone would widen too early. SeaBreeze tries HM binding first, keeps exact
constructors exact, and only emits a union when the validation lattice actually
requires it.
Object Join
Object fields are stored sorted by caller-owned numeric key ids. The join is a linear two-pointer merge:
{ a: number, b: string }
join
{ a: string, c: boolean }
=
{ a: number | string, b?: string, c?: boolean } This is a better TypeSea common type than unknown and a more precise runtime
contract than a raw union of the two object shapes.
Allocation Discipline
The implementation lives in src/seabreeze/sea-breeze.ts.
- Nodes are dense ids.
- Parent, rank, kind, child, and field tables are typed arrays.
- Hot operations return numbers, not wrapper objects.
- Capacity is caller-owned and checked explicitly.
- Field keys are already interned by the caller.
- Materialization is intentionally outside the core solver.
The solver still lives in JavaScript, so object reflection around it can allocate. The algorithm core is the part designed for TypeSea’s zero-cost abstraction style.
Complexity
For sorted object shapes:
- Scalar join:
O(alpha(n))through union-find. - Array join: recursive element join.
- Object join:
O(leftFieldCount + rightFieldCount). - Field storage: one fixed arena slot per emitted output field.
The important property is that common object inference does not need a Map per
join and does not allocate transient field records.
Public Surface
Import it from the dedicated subpath:
import {
createSeaBreeze,
SeaBreezeArena,
SeaBreezePresence,
emitSeaBreezeBooleanSourceBundle,
seaBreezeReader
} from "typesea/seabreeze"; For normal code, prefer the builder API:
const s = createSeaBreeze({ maxNodes: 64, maxFields: 16 });
const User = s.object({
id: s.string(),
age: s.optional(s.number()),
tags: s.array(s.string())
});
const FastUser = s.compile(User, {
objectMode: "strict",
mode: "safe",
name: "isUser"
}); The builder is deliberately thin. Primitive methods return canonical numeric
node ids, object() interns keys and appends sorted arena fields, and compile() calls the direct reader emitter. Once FastUser.is() runs, no
builder object participates in validation.
The subpath also exports the arena, typed reader, snapshot helpers, schema
lowering, graph lowering, and direct predicate source emitter. It does not add
exports to typesea, which keeps the root validator API zero-cost with respect
to SeaBreeze.
Current tests cover:
- HM variable binding.
- Scalar conflict fallback to union.
- Optimal object common-shape join.
- Array element variable propagation.
- Fixed-capacity failure behavior.
- Schema lowering into the existing TypeSea JIT path.
- Direct graph lowering and predicate source emission from a lowered graph.
- Direct reader-to-source emission without materializing
Schema,Graph, orGraphNodeobjects.
Bridge To JIT
src/seabreeze/lower-schema.ts implements the compatibility bridge:
SeaBreeze arena node
-> lowerSeaBreezeToSchema()
-> BaseGuard(schema)
-> compileBoolean() / compile()
-> existing Sea-of-Nodes lower/optimize/JIT emitter This intentionally materializes a TypeSea Schema before JIT emission. That
keeps diagnostics, AOT, optimizer parity, and existing hostile-input modes on
the normal TypeSea path.
The lowering accepts a caller-owned key table:
const schema = lowerSeaBreezeToSchema(arena, root, {
keyTable: ["", "id", "name", "flag"],
objectMode: "strict"
}); Field key ids stay numeric inside the arena. The key table is copied and validated at the bridge boundary so later caller mutation cannot change the schema meaning.
Lowering policy:
objectMode:"strict" | "passthrough" | "strip", default"strict".unboundVar:"unknown" | "error", default"unknown".cycle:"unknown" | "error", default"error".unionMode:"flatten" | "binary", default"flatten".
src/seabreeze/lower-graph.ts implements the direct predicate bridge:
SeaBreeze arena node
-> lowerSeaBreezeToGraph()
-> optimizeGraph()
-> emitCompiledGraphBooleanSourceBundle()
-> V8 predicate function This skips root Schema materialization for the predicate graph while still
using schema payloads where TypeSea’s existing object, array, and union nodes
need them for parity with diagnostics and hostile-input modes.
src/seabreeze/emit.ts implements the direct reader emitter:
SeaBreezeReader
-> emitSeaBreezeBooleanSourceBundle()
-> V8 predicate function This path reads arena metadata through SeaBreezeReader and emits predicate
source directly. It still reuses TypeSea’s side-table ABI, function naming, debug
source formatter, and helper prelude, but it does not allocate intermediate Schema, Graph, GraphBuilder, or GraphNode records.
The direct emitter preserves the same safety tier semantics as the normal JIT emitter:
safe: own data descriptors only; accessors and prototype reads fail closed.unsafe: direct property reads for V8 hot paths; hostile-input defenses are intentionally reduced.unchecked: unsafe reads plus skipped strict excess-key checks.