Writ reference
This page is the precise reference for the shipped Writ surface. Where it helps, it cites the Writ spec’s permanent rule ids (e.g. writ.value.wany-word) from docs/spec/writ.md in the Logos repository; those ids are stable linkable addresses. For the concepts, start with the introduction; for a guided build-up, the tutorial.
Literal grammar
Writ structured-data literals use a leading @ sigil in expression position (writ.literal.at-brace). Only the outermost literal carries the @; nested values are plain.
| Form | Meaning |
|---|---|
@null |
null |
@true / @false |
bool |
@INT / @-INT |
integer |
@FLOAT |
float |
@"str" |
string |
@[ v, … ] |
array |
@{ k: v, … } |
map |
Value encodings (writ.literal.value-encodings): an integer lowers inline as i24 when in [-2^23, 2^23−1], else boxes to i64; a float boxes to f64; a homogeneous scalar array becomes a typed array, otherwise an object array; a map with integer keys (I32/U32/I64/U64) becomes a typed map, otherwise a string-keyed object map.
Typed collection literals
writ.literal.typed-collections:
@<Elem>[ … ]— a typed dense array,Elemone ofI8 U8 I16 U16 I32 U32 I64 U64 F32 F64, resolving toWArray<Elem>.@<K>{ … }/@<K,V>{ … }— a typed map,Kone ofI32 U32 I64 U64,Vdefaulting toWAny, resolving toWMap<K,V>.
In SDN text the equivalent prefix is <I32> [ … ]. The runtime text parser keeps the type params as hints and returns regular array/map nodes.
Result type: WritStatic vs Rc<Writ>
- A capture-free
@-literal has typeWritStatic(writ.literal.rodata-blob): a compile-time blob in rodata laid out[u64 size][bytes],WritStatic.ptrpointing past the size prefix. The blob is native Writ format (self-relativeWAnyslots,WMap<WString,WAny>,WArray<WAny>,WString) — the same layout the parser builds. Read it withWView2(writ.literal.wview2-reader). - A capture-bearing
@-literal has typeRc<Writ>and is constructed at runtime — requiresuse logos.lang.writ.tmpl;.
Captures
writ.literal.captures. Available only in @-literal syntax — parse_writ / parse do not interpret $:
$ident— splice a Logos variable. Same-name$identcaptures share one value slot (deduplicated).${expr}— splice an arbitrary expression. Never deduplicated (may have side effects).$N— a positional PARAM placeholder for document templates (in the embedding grammar).
Capture is type-checked and coerced when safe, and supports as <T>[…] casts for typed arrays.
The WAny value model
The word
writ.value.wany-word. WAny is the Writ heterogeneous slot: one 8-byte word, impl Copy, defined
#[zoned] #[borrow_carrying] enum WAny { Ref(*const u8), Pod(u64) }
Decoding the raw word: word == 0 is a null reference (Ref(0)); word & 1 == 1 is an inline Pod; word & 1 == 0 && word != 0 is a Ref. Zone objects are ≥2-aligned, so a Ref’s low bit is always 0 and never collides with the Pod tag.
Pod encoding
writ.value.pod-encoding. A Pod word is (value << 8) | ((code & 0x7F) << 1) | 1: bit 0 = 1, bits 1–7 a 7-bit type code (1..127), bits 8–63 a 56-bit signed inline value. Inline integers are i56, not i64 — a value that does not fit 56 signed bits boxes into a Ref. Inline Pod codes (writ.value.pod-codes): WA_I56=1 (generic inline integer), WA_BOOL=2, and exact-width integers WT_I8=20 WT_U8=21 WT_I16=22 WT_I24=23 WT_U16=24 WT_U24=25, each preserving its precise type across a round-trip.
Ref codes
writ.value.ref-codes. A Ref points at a tagged arena object whose in-band tag identifies the pointee: W_STRING=130, W_ARRAY=100, W_MAP=101 (string-keyed), W_TINYMAP=98 (TOM), W_DECIMAL=102, W_TYPEDVALUE=4115, plus boxed wide scalars W_I64=26 W_U64=27 W_F32=30 W_F64=31.
Type code and predicates
writ.value.type-code. WAny::type_code() is the single dispatch point: 0 for null, the inline pod_code() for a Pod, or the pointee’s tag for a Ref. Kind predicates read it: is_null is_pod is_ref is_int is_float is_string is_array is_map is_tinymap. resolve() returns the value-form absolute pointer (safe to obtain; dereferencing it is where unsafe lives). Accessors: as_i56 as_i64 as_u64 as_f32 as_f64 as_bool, and WAny::from(v) / WAny::pod(v, code) / WAny::ref_to(ptr) construct.
Absent is the zero value
writ.value.absent-is-null. A null or absent WAny decodes to the reading type’s zero, never faulting: as_i64/as_u64 → 0, as_f32/as_f64 → 0.0, as_bool → false, a Ref accessor → a null ref. This is the same contract as an absent schema key (writ.schema.absent-key).
At-rest vs value form
writ.value.at-rest-vs-value. A WAny has two forms. The value form (the plain word, movable, in registers) holds a Ref as an absolute pointer. The at-rest form — the same word stored in an arena slot reached through a *zoned WAny (a WArray/WMap buffer) — holds a Ref as a self-relative delta target − &slot. The compiler owns the bridge: *slot materialises at-rest→value, *slot = v lowers value→at-rest; Pod/null are position-independent. WAnyMut (writ.value.wanymut-cursor) is the mutable dual — a fat &mut onto an element slot, with get() / set(v).
Schemas
Declaration grammar
writ.schema.decl-grammar:
schema S <type_param_list>? <code_clause>? { field* }
field := pub? IDENT ':' type ('=' const_expr)? ','?
Sema registers S as a struct flagged is_schema. The schema name itself is the typed view — there is no separate FooView type (writ.schema.view-over-map). A view’s only real struct fields are synthetic (writ.schema.synthetic-view-fields): m: *const WMap<Wu6,WAny> (the backing TOM) and z: *mut u8 (the arena allocator, for boxing on write) — a 16-byte fat view. z is null for a read-only view bound from an erased WAny. Declared fields do not occupy struct offsets; they live in parallel key/field tables surfaced by the desugared get/set.
The code(...) and category(...) clauses use a contextual keyword (a bare IDENT validated in sema), so code / category remain usable as ordinary identifiers.
Field keys
writ.schema.field-key-code. Each field’s key is the = const_expr (CTFE-evaluated) when present, else the running positional index (from 0, advancing to key + 1 after each field). Keys are TOM u8 codes and:
- must lie in
0..51— a key< 0or> 51is a hard error:"key N out of TOM range 0..51". - must be unique — a key equal to an earlier field’s is a hard error:
"duplicate key N".
Type code, category, variant
writ.schema.type-code. An optional code(const_expr) clause sets the schema’s schema_type_code (CTFE u64; absent ⇒ 0). It is stamped into the backing TOM header at construction and is the schema’s global identity — read from the pointee, never stored separately. The code packs category(16 bits) | variant(48 bits) (writ.schema.category-variant-mask): CATEGORY_SHIFT = 48, CATEGORY_MASK = 0xFFFF << 48, VARIANT_MASK = (1 << 48) − 1. Predefined categories include CAT_UNSET=0 CAT_AST=1 CAT_TYPE=2 CAT_LIR_EXPR=3 CAT_LIR_STMT=4 CAT_LIR_PAT=5 CAT_SYMBOLS=6 CAT_DIAG=7.
Field read / write desugaring
- Read
p.f(writ.schema.field-read-sugar) ⇒T::from_wany((&*self.m).get(KEY)): read the syntheticm,get(KEY)yielding aWAny, convert toTvia theWritFieldtrait. An absent key returns a nullWAny→ the type’s zero (writ.schema.absent-key). A name that is not a declared field falls through to the ordinary struct path. - Write
p.f = v(writ.schema.field-write-sugar) ⇒self.m.set(KEY, T::to_wany(v, z)).zis the view-carried allocator, passed for boxing wide values and interning strings; inline conversions ignore it. A type-mismatched write is a hard error:"schema write 'p.f': expected …, got …".
The WritField trait
writ.schema.writfield-trait. Field conversions go through:
trait WritField {
fn from_wany(v: WAny) -> Self;
fn to_wany(self: Self, z: *mut Allocator) -> WAny;
}
Stdlib impls cover bool, str, WAny (identity), all integer widths (i8..i64 / u8..u64, i24 i56 u24 u56 isize usize), and floats (f32 f64). Inline-fitting values ignore z; wide/boxing values (i32 u32 u56 u64 isize usize f32 f64) allocate through it; str interns via wstring_in_alloc(z, …). A user type becomes schema-storable by implementing WritField. Notable field types:
str(writ.schema.str-field) — write interns into the view’s arena; read decodes the internedWStringback to astr(null-safe → empty).WAny(writ.schema.wany-field) — the dynamic/heterogeneous field: identity, no conversion, no boxing; re-tag the same slot with anyWAny; absent reads back null.
Generic schemas
writ.schema.generic. schema Wrap<T: WritField> { val: T = 0 } binds T as a TypeVar; a T-typed field stays symbolic in the generic body and is substituted with the receiver’s concrete type-args at the use site (Wrap<i64> → T becomes i64), with mono retargeting the read/write to the concrete WritField impl. A type param without a WritField impl is a hard error: "does not implement trait 'WritField'". A generic instance derives a per-instance schema_type_code from the base code’s category plus a variant hashed from the canonical concrete name (writ.schema.generic-instance-code); make, view_checked, and schema-enum match share this helper, so a produced node’s code matches what a consumer checks.
impl and traits
writ.schema.impl-and-traits. The schema name is an ordinary type: impl S { … } and impl Trait for S need no special casing; methods take self: &S / &mut S and use both the field sugar and raw self.m.get/set. A schema may be a trait bound. There is no dyn schema — open runtime polymorphism is WAny + an explicit checked bind; closed sets are a schema enum.
Schema enum and dispatch
writ.enum.decl:
schema enum E <type_param_list>? <category(expr)>? { V(S), … }
A closed union whose variants are other concrete schemas; a value is a view {m, z} onto the TOM of one variant. It is not a flat Logos enum: no discriminant is stored — the variant is read from the pointee’s own schema_type_code (single source of truth). The optional category(expr) sets the enum’s category.
match (writ.enum.match-dispatch) desugars to reading (&*e.m).schema_type_code() once, then an if-chain comparing it against each variant’s per-instance code and binding the concrete variant view in the matched arm. Or-patterns in a schema-enum arm are not yet supported ("or-patterns not supported yet"); an arm pattern must be E::Variant(b) or _.
Typed-view binding (shipped surface)
The three shipped bind/construct forms:
| Form | Check | Result | Rule |
|---|---|---|---|
wr.make::<S>() |
— (allocates & stamps) | S (writable view) |
writ.view.make |
.view::<S>() |
none (trusted) | S |
writ.view.trusted-bind |
.view_checked::<S>() |
verifies schema_type_code once |
Option<S> |
writ.view.checked-option |
makeproduces a fresh view over a newly allocated, code-stamped TOM (wr.make_schema_h(cap, S::CODE)reinterpreted asS);capis the schema’s field count (min 1). The view carriesz, so fields are immediately writable..view::<S>()(alias.child) binds without a code check — the producer is trusted. Use where the type is statically known (a concrete child inside an already-bound tree)..view_checked::<S>()is the safe downcast from erased/externalWAny: resolve to the TOM, readschema_type_code(), yieldSome(S)iff it equalsS::CODE, elseNone. Pattern-match theOptionto consume.
Check policy (writ.view.check-policy) follows from the static type, not a flag: erased/external input ⇒ check unavoidable (view_checked, or a match over a schema enum, once); a bound concrete child ⇒ view (check provably redundant, elided).
Typed edges
WRef<S> (writ.edge.wref) is the one graph-edge primitive: pub struct WRef<S> { h: WAny } — a single WAny ref handle whose phantom S names the target schema family (compile-time-only documentation; the stored value is the WAny). WRef::<S>::from_any(h) wraps; .any() unwraps. It is a schema field via an identity WritField (writ.edge.wref-writfield). Consume an edge with r.any().view::<S>() (writ.edge.resolve-view) — the child type is statically the edge target, so no code check.
Serialization
One logical document, three interchangeable representations; any value round-trips losslessly between all three (writ.md §Three Serialization Modes):
- Zero-copy — the native in-memory layout; internal pointers are offsets, so heap/disk/shared-memory bytes are the same bytes, no parse on read. For storage, IPC, and accelerator offload.
- Binary serial — a compact, validated wire format (codec
src/writ/binary_codec.cpp); validated on decode so a compromised peer cannot hand you a malformed document. HRPC frames Writ this way. In the stdlib,wbs_write/wbs_read(HBS). - SDN (String Data Notation) — the human-readable text form; every type prints and parses itself. Produced by
stringify, consumed byparse_writ/parse.
The text parser, binary codec, and zero-copy clone route through the same trait surface, so every registered datatype gets all three for free.
Containers
| Container | Alias / code | Shape |
|---|---|---|
Writ |
— | the owned, never-move root container: owns a segment-arena Allocator, holds the root WAny. writ_new(seg_size); set_root / root. (writ.container.writ-root) |
WMap<Wu6,WAny> |
WTinyValMap, W_TINYMAP=98 |
the bitmap-indexed TinyObjectMap: ≤52 keys (0..51) → WAny, 24-byte header, O(1) lookup via popcount. The default schema backing. (writ.container.tom) |
WMap<WString,WAny> |
WValMap, W_MAP=101 |
the string-keyed object map (JSON object): open-addressing, FNV-1a, grows like a hash map. (writ.container.object-map) |
WMap<K,WAny> (K: WIntKeyTag) |
MapI32AnyVal=3101..MapU64AnyVal=3104 |
the dense integer-keyed map, fixed cap, O(n) linear lookup. (writ.container.dense-int-map) |
WArray<WAny> |
W_ARRAY=100 |
heterogeneous JSON array of at-rest WAny slots. (writ.container.warray) |
WArray<T> (T: WArrTag) |
2101..2110 |
typed, packed, homogeneous primitive array. |
WString |
W_STRING=130 |
a UTF-8 string interned in the arena; as_str borrows it back. |
WTypedValue |
W_TYPEDVALUE=4115 |
an SDN datatype instantiation, @Type(params?) = init. (writ.container.typed-value) |
The TinyObjectMap in detail
writ.container.tom. Header packs bitmap[0:51] | cap[52:57] | size[58:63], plus a separate schema_code: u64, plus a self-relative data: *zoned mut WAny value buffer kept in key order. get(key) returns a null WAny for an absent/out-of-range key; set(key, val) is a thin &mut — fixed cap, so it never allocates, and is a no-op for key ≥ 52 or a full map with a new key. A key’s value-array position is popcount(bitmap & keys-below). Wu6 is a pure type-level label for the 6-bit key. It is byte-identical across C++ and Logos — every logosc AST node is one, which is what makes the heterogeneous-compiler story mechanical.
Walking a document
writ.container.document-walk. A “document” is a Writ container plus its root WAny. Walkers recurse from any WAny, dispatching on type_code() exactly like equal / stringify; container children are reached via is_array/is_map + resolve(). This is the canonical traversal shape (node_count, depth).
Pitfalls / gotchas
-
Absent is null, not an error. A missing map key or an absent schema field reads back as the type’s zero (
0/false/ null ref / emptystr/ nullWAny), never a fault and never anOption(writ.value.absent-is-null,writ.schema.absent-key). Afterparse_writ, a parse error surfaces the same way:doc.root().is_null(). Check for it explicitly where a real value is required. -
.as::<S>()/.as_trusted::<S>()are not shipped. ADR 0011 planned these bind forms, but onlymake/view/view_checkedare implemented — the code wins over the ADR (writ.view.check-policy). Use.view::<S>()for the trusted bind and.view_checked::<S>()for the checked one. -
The GC runs no destructors. Reclamation is an on-demand copying/compacting collection over the reachable set — ZTypes are
!Drop, so no destructors run. Do not attach cleanup semantics to a Writ value’s reclamation; there is no conventional heap free per object, only whole-zone copy-and-drop (writ.md§Memory Management). -
Inline integers are
i56, noti64. APodcarries a 56-bit signed value; anything wider boxes to aRef(W_I64/W_U64). This is transparent toas_i64, but relevant when reasoning about which values allocate (writ.value.pod-encoding). -
Keys must be
0..51and unique. A schema field key outside that range, or duplicated, is a hard compile error (writ.schema.field-key-code). -
Captures are literal-only.
$ident/${expr}are interpreted only in@-literals in source, never by the runtime text parser (writ.literal.captures). -
Schema-enum arms are
Variant(b)or_only — or-patterns are not yet supported (writ.enum.match-dispatch). -
Prefer the free-call/resolve form over
.method()on a&WAny. Calling aWAnyaccessor as a method on a&WAnyenum receiver can return null in mlir-gen; the schema desugarings route through a resolved free call for this reason (writ.pitfall.wany-method-on-enum-ref). This is mostly internal, but relevant if you hand-rollWAnytraversal.
Related
- Writ Introduction — the thesis, the
WAnymental model, and where Writ sits in Logos. - Writ Tutorial — a runnable build-up from a first literal through schemas and schema enums.
- Deem: querying Writ — the query layer over the Writ object graph these types define.