//! Swift phon descriptor generation.
//!
//! Emits, for a facet `Shape`, a Swift expression that builds a
//! `PhonIR.Descriptor` — the typed-path codec's input. The tricky witness logic
//! lives in the phon package's generic factories (`OptionWitness.of`,
//! `BytesWitness.string`, `SeqWitness.of`, `MapWitness.stringKeyed`); enum
//! `tag`/`project`/`destroy`/`inject` are emitted per-type here (they switch over
//! concrete cases).
//!
//! First cut covers what the Message envelope + the core Testbed need: scalars,
//! String/bytes, struct/tuple-struct records, Option, list (bulk scalar /
//! per-element struct), string-keyed map, enum (unit + newtype variants), and
//! Result. Unsupported shapes panic at codegen time (loud, not silent).
use facet_core::{ScalarType, Shape};
use phon_ir as ir;
use phon_schema::{SchemaId, SchemaRef};
use vox_phon::{schema_bytes_for_shape, schema_id_for_shape};
use vox_types::{
EnumInfo, ShapeKind, StructInfo, VariantKind, classify_shape, classify_variant, is_bytes,
};
use super::types::{generate_named_types, is_dynamic_value, swift_field_name, swift_type_base};
use crate::render::hex_u64;
/// Assemble the full phon wire module: the type definitions for `types` plus the
/// phon codec for `roots`. Replaces the postcard `generate_wire_types`.
pub fn generate_phon_wire(
types: &[(String, &'static Shape)],
roots: &[(String, &'static Shape)],
) -> String {
let mut out = String::new();
out.push_str("// @generated by vox-codegen — DO NOT EDIT\n");
out.push_str("// Regenerate with `cargo xtask codegen --swift-wire`.\n\n");
out.push_str("import Foundation\nimport PhonSchema\nimport PhonIR\nimport PhonEngine\n\n");
// phon types don't need `Codable` (the descriptor codec encodes/decodes them);
// drop it — it also can't synthesize for every wire type.
let typedefs = generate_named_types(types)
.replace(": Codable, Sendable, Error", ": Sendable, Error")
.replace(": Codable, Sendable", ": Sendable");
out.push_str(&typedefs);
out.push('\n');
out.push_str(&generate_phon_codec(roots));
out
}
/// Emit the phon codec for a set of root types: per root, the schema-closure
/// bytes, a `Registry`, the `Descriptor`, the encode/decode programs (cached), and
/// `encode<Name>` / `decode<Name>` entry points. The engine does the rest.
pub fn generate_phon_codec(roots: &[(String, &'static Shape)]) -> String {
let mut out = String::new();
for (name, shape) in roots {
let ty = swift_type_base(shape);
let id = phon_schema_id(shape);
let closure = schema_bytes_for_shape(shape).expect("phon schema closure bytes");
let bytes = closure
.iter()
.map(u8::to_string)
.collect::<Vec<_>>()
.join(", ");
let id_hex = hex_u64(id);
let (desc, blocks) = descriptor_with_blocks(shape);
let blocks_lit = blocks_literal(&blocks);
// These globals are immutable and initialized once; mark them
// `nonisolated(unsafe)` (the descriptor/registry/program types carry
// closures and aren't `Sendable`, but are never mutated after init).
out.push_str(&format!("// MARK: - {name} codec\n\n"));
out.push_str(&format!(
"public let {name}SchemaClosure: [UInt8] = [{bytes}]\n"
));
out.push_str(&format!(
"nonisolated(unsafe) public let {name}Registry: Registry = Registry((try! parseSchemaClosure({name}SchemaClosure)).schemas)\n"
));
out.push_str(&format!("public let {name}RootId = SchemaId({id_hex})\n"));
out.push_str(&format!(
"nonisolated(unsafe) public let {name}Descriptor: Descriptor = {desc}\n"
));
// Recursion blocks for any cyclic schema in this root (`[:]` when none) — the
// `Access.recurse` stand-ins in `{name}Descriptor` resolve into these via
// `CallBlock`.
out.push_str(&format!(
"nonisolated(unsafe) public let {name}DescriptorBlocks: [SchemaId: Descriptor] = {blocks_lit}\n"
));
// Encode uses the own-schema lowering (`lowerTyped`); there is NO cached
// same-schema decode program/function — every decode goes through the runtime's
// reconciling `lowerDecode(writer → reader)` built from the peer's advertised
// schema (see `buildMessageDecoder` / `decodeHandshakeFrame`).
out.push_str(&format!(
"nonisolated(unsafe) private let {name}EncodeProgram: Lowered = try! lowerTyped({name}Descriptor, {name}Registry, {name}DescriptorBlocks)\n\n"
));
out.push_str(&format!(
"private let {name}Encoder = VoxTypedEncoder({name}EncodeProgram)\n\n"
));
out.push_str(&format!(
"public func encode{name}(_ value: {ty}) -> [UInt8] {{ encodeVoxTyped(value, {name}Encoder) }}\n\n"
));
}
out
}
/// The phon content-derived id (`SchemaId`) of a shape's root — the canonical id
/// peers agree on, matching the closure bytes (NOT the vox-types `SchemaHash`).
fn phon_schema_id(shape: &'static Shape) -> u64 {
schema_id_for_shape(shape).expect("phon schema id").as_u64()
}
/// `MemoryLayout<T>` size/align expression for a shape's Swift type.
fn layout_expr(shape: &'static Shape) -> String {
let ty = swift_type_base(shape);
format!("Layout(size: MemoryLayout<{ty}>.size, align: MemoryLayout<{ty}>.alignment)")
}
fn schema_ref_expr(schema_ref: &SchemaRef) -> String {
match schema_ref {
SchemaRef::Concrete { id, args } if args.is_empty() => {
format!(".concrete(SchemaId({}))", hex_u64(id.as_u64()))
}
SchemaRef::Concrete { id, args } => {
let args = args
.iter()
.map(schema_ref_expr)
.collect::<Vec<_>>()
.join(", ");
format!(
".concrete(id: SchemaId({}), args: [{args}])",
hex_u64(id.as_u64())
)
}
SchemaRef::Var { name } => format!(".variable(name: {name:?})"),
}
}
fn concrete_schema_id(schema_ref: &SchemaRef) -> SchemaId {
match schema_ref {
SchemaRef::Concrete { id, args } if args.is_empty() => *id,
other => panic!("Swift recursion descriptor needs a concrete schema id, got {other:?}"),
}
}
/// Whether a shape is a fixed-width scalar (copies as raw bytes) — bool, the
/// integer and float widths, char/unit. String/Str are NOT fixed (bulk bytes).
fn is_fixed_scalar(shape: &'static Shape) -> bool {
matches!(
classify_shape(shape),
ShapeKind::Scalar(
ScalarType::Unit
| ScalarType::Bool
| ScalarType::U8
| ScalarType::U16
| ScalarType::U32
| ScalarType::U64
| ScalarType::USize
| ScalarType::I8
| ScalarType::I16
| ScalarType::I32
| ScalarType::I64
| ScalarType::ISize
| ScalarType::F32
| ScalarType::F64
)
)
}
/// Recursion context for descriptor emission. The Rust Phon derive is the oracle for
/// schema refs: collection/option wrapper ids are root-context ids, not ids that can
/// be recomputed from each child shape in isolation.
struct RecCtx {
rust_blocks: std::collections::HashMap<SchemaId, ir::Descriptor>,
building: std::collections::HashSet<u64>,
blocks: std::collections::BTreeMap<u64, String>,
}
impl RecCtx {
fn ensure_block(&mut self, shape: &'static Shape, id: SchemaId) {
if self.blocks.contains_key(&id.as_u64()) || self.building.contains(&id.as_u64()) {
return;
}
let rust_body = self
.rust_blocks
.get(&id)
.unwrap_or_else(|| panic!("missing Rust-derived recursion block for {id:?}"))
.clone();
self.building.insert(id.as_u64());
let body = descriptor_node(shape, &rust_body, self);
self.building.remove(&id.as_u64());
self.blocks.insert(id.as_u64(), body);
}
}
/// The full `Descriptor(...)` expression for a (non-recursive) shape. For a possibly
/// recursive shape use [`descriptor_with_blocks`], which also returns the block bodies.
pub fn descriptor_expr(shape: &'static Shape) -> String {
descriptor_with_blocks(shape).0
}
/// The root `Descriptor(...)` expression plus the recursion block bodies (keyed by
/// schema id) for every cyclic schema reachable from `shape`. A cyclic position emits
/// an `Access.recurse` stand-in and its full body is collected as a block, so the
/// emitted descriptor tree stays finite (`r[ir.recursion]`). The blocks map is empty
/// for a non-recursive shape.
pub fn descriptor_with_blocks(
shape: &'static Shape,
) -> (String, std::collections::BTreeMap<u64, String>) {
let derived = phon::derive::of_shape(shape).expect("phon descriptor derive");
let mut ctx = RecCtx {
rust_blocks: derived.descriptor_blocks,
building: std::collections::HashSet::new(),
blocks: std::collections::BTreeMap::new(),
};
let desc = descriptor_node(shape, &derived.descriptor, &mut ctx);
(desc, ctx.blocks)
}
/// A Swift `[SchemaId: Descriptor]` literal from collected block bodies (`[:]` when
/// empty) — what codegen passes as `descriptorBlocks` to `lowerTyped`/`lowerDecode`.
pub fn blocks_literal(blocks: &std::collections::BTreeMap<u64, String>) -> String {
if blocks.is_empty() {
return "[:]".to_string();
}
let entries: Vec<String> = blocks
.iter()
.map(|(id, body)| format!("SchemaId({}): {body}", hex_u64(*id)))
.collect();
format!("[{}]", entries.join(", "))
}
/// One descriptor node. Schema refs come from Rust's derived descriptor; access and
/// witness expressions are Swift-specific and are emitted from the shape.
fn descriptor_node(shape: &'static Shape, rust_desc: &ir::Descriptor, ctx: &mut RecCtx) -> String {
if let ir::Access::Pointer(pointer) = &rust_desc.access {
let ShapeKind::Pointer { pointee } = classify_shape(shape) else {
panic!("Swift pointer descriptor got non-pointer shape {shape:?}");
};
return descriptor_node(pointee, &pointer.pointee, ctx);
}
if matches!(rust_desc.access, ir::Access::Recurse) {
let id = concrete_schema_id(&rust_desc.schema);
ctx.ensure_block(shape, id);
return recurse_descriptor(shape, &rust_desc.schema);
}
format!(
"Descriptor(schema: {}, layout: {}, access: {})",
schema_ref_expr(&rust_desc.schema),
layout_expr(shape),
access_expr(shape, rust_desc, ctx)
)
}
/// The `Access.recurse` stand-in descriptor for a cyclic position: schema + layout,
/// no inlined body (the body lives in the block registry, reached by `CallBlock`).
fn recurse_descriptor(shape: &'static Shape, schema_ref: &SchemaRef) -> String {
format!(
"Descriptor(schema: {}, layout: {}, access: .recurse)",
schema_ref_expr(schema_ref),
layout_expr(shape)
)
}
fn access_expr(shape: &'static Shape, rust_desc: &ir::Descriptor, ctx: &mut RecCtx) -> String {
// A self-describing dynamic `Value` field (e.g. `Metadata`): carried as a phon
// `Dynamic`. In Swift memory it is a `PhonSchema.Value`; `.dynamic` drives the
// self-describing codec at the field offset (NOT opaque `Data`).
if is_dynamic_value(shape) {
return ".dynamic".to_string();
}
// `Vec<u8>`/`&[u8]` map to Swift `Data` (per `swift_type_base`); the `.data`
// witness (a vox-runtime extension) reads/builds it.
if is_bytes(shape) {
return ".bytes(BytesAccess(stride: 1, elemAlign: 1, witness: .data))".to_string();
}
match classify_shape(shape) {
ShapeKind::Scalar(ScalarType::String | ScalarType::Str | ScalarType::CowStr) => {
".bytes(BytesAccess(stride: 1, elemAlign: 1, witness: .string))".to_string()
}
ShapeKind::Scalar(_) => ".scalar".to_string(),
ShapeKind::Option { inner } => {
let inner_ty = swift_type_base(inner);
let ir::Access::Option(option) = &rust_desc.access else {
panic!("Swift option descriptor got {:?}", rust_desc.access);
};
format!(
".option(OptionAccess(witness: .of({inner_ty}.self), some: {}))",
descriptor_node(inner, &option.some, ctx)
)
}
ShapeKind::List { element }
| ShapeKind::Slice { element }
| ShapeKind::Array { element, .. } => {
let ir::Access::Sequence(sequence) = &rust_desc.access else {
panic!("Swift sequence descriptor got {:?}", rust_desc.access);
};
sequence_or_bulk(element, &sequence.element, ctx)
}
ShapeKind::Set { element } => {
let ir::Access::Set(set) = &rust_desc.access else {
panic!("Swift set descriptor got {:?}", rust_desc.access);
};
sequence_set(element, &set.element, ctx)
}
ShapeKind::Map { key, value } => {
assert!(
matches!(
classify_shape(key),
ShapeKind::Scalar(ScalarType::String | ScalarType::Str | ScalarType::CowStr)
),
"phon maps must be string-keyed"
);
let ir::Access::Map(map) = &rust_desc.access else {
panic!("Swift map descriptor got {:?}", rust_desc.access);
};
let v_ty = swift_type_base(value);
format!(
".map(MapAccess(key: {}, value: {}, keyStride: MemoryLayout<String>.stride, keyAlign: MemoryLayout<String>.alignment, valueStride: MemoryLayout<{v_ty}>.stride, valueAlign: MemoryLayout<{v_ty}>.alignment, witness: .stringKeyed({v_ty}.self)))",
descriptor_node(key, &map.key, ctx),
descriptor_node(value, &map.value, ctx)
)
}
ShapeKind::Struct(StructInfo {
fields,
name: Some(type_name),
..
}) => {
let ir::Access::Record(record) = &rust_desc.access else {
panic!("Swift record descriptor got {:?}", rust_desc.access);
};
let entries: Vec<String> = fields
.iter()
.zip(record.fields.iter())
.map(|(f, rust_field)| {
format!(
"FieldAccess(offset: MemoryLayout<{type_name}>.offset(of: \\{type_name}.{})!, descriptor: {})",
swift_field_name(f.name),
descriptor_node(f.shape(), &rust_field.descriptor, ctx)
)
})
.collect();
format!(
".record(RecordAccess(fields: [{}], construct: .inPlace))",
entries.join(", ")
)
}
ShapeKind::Enum(EnumInfo {
name: Some(_),
variants,
}) => {
let ir::Access::Enum(enumeration) = &rust_desc.access else {
panic!("Swift enum descriptor got {:?}", rust_desc.access);
};
enum_access(shape, variants, enumeration, ctx)
}
// A tuple — chiefly a method's args (the phon schema is a positional struct
// `(_,…)`; the record's fields zip with it by index). A 1-tuple collapses to
// its element in Swift (no `.0` keypath), so its single field is at offset 0;
// a 0-tuple is unit (0 bytes); N≥2 use the Swift tuple's element offsets.
ShapeKind::Tuple { elements } => {
let shapes: Vec<&'static Shape> = elements.iter().map(|p| p.shape).collect();
let ir::Access::Record(record) = &rust_desc.access else {
panic!("Swift tuple descriptor got {:?}", rust_desc.access);
};
tuple_access(shape, &shapes, record, ctx)
}
ShapeKind::TupleStruct { fields } => {
let shapes: Vec<&'static Shape> = fields.iter().map(|f| f.shape()).collect();
let ir::Access::Record(record) = &rust_desc.access else {
panic!("Swift tuple-struct descriptor got {:?}", rust_desc.access);
};
tuple_access(shape, &shapes, record, ctx)
}
ShapeKind::Result { ok, err } => {
let ir::Access::Result(result) = &rust_desc.access else {
panic!("Swift result descriptor got {:?}", rust_desc.access);
};
result_access(ok, err, result, ctx)
}
// An opaque field (the already-phon-encoded method payload) rides the wire
// as a length-prefixed byte run — a `Primitive::Bytes`. In memory it is a
// Swift `Data`.
ShapeKind::Opaque => {
".bytes(BytesAccess(stride: 1, elemAlign: 1, witness: .data))".to_string()
}
other => panic!("phon descriptor: unsupported shape {other:?}"),
}
}
/// A tuple as a positional `.record`. The Swift type is `swift_type_base(shape)`:
/// `Void` for 0 elements, the bare element type for 1 (a Swift 1-tuple collapses —
/// its field is at offset 0), or `(A, B, …)` for N≥2 (element offsets via keypath).
fn tuple_access(
shape: &'static Shape,
elements: &[&'static Shape],
record: &ir::RecordAccess,
ctx: &mut RecCtx,
) -> String {
match elements.len() {
0 => ".scalar".to_string(), // unit: 0 bytes
1 => format!(
".record(RecordAccess(fields: [FieldAccess(offset: 0, descriptor: {})], construct: .inPlace))",
descriptor_node(elements[0], &record.fields[0].descriptor, ctx)
),
_ => {
let ty = swift_type_base(shape);
let fields: Vec<String> = elements
.iter()
.zip(record.fields.iter())
.enumerate()
.map(|(i, (e, rust_field))| {
format!(
"FieldAccess(offset: MemoryLayout<{ty}>.offset(of: \\.{i})!, descriptor: {})",
descriptor_node(e, &rust_field.descriptor, ctx)
)
})
.collect();
format!(
".record(RecordAccess(fields: [{}], construct: .inPlace))",
fields.join(", ")
)
}
}
}
/// A `[T]`: bulk byte run when `T` is a fixed scalar, else a per-element sequence.
fn sequence_or_bulk(
element: &'static Shape,
element_desc: &ir::Descriptor,
ctx: &mut RecCtx,
) -> String {
let ty = swift_type_base(element);
if is_fixed_scalar(element) {
format!(
".bytes(BytesAccess(stride: MemoryLayout<{ty}>.stride, elemAlign: MemoryLayout<{ty}>.alignment, witness: .scalarArray({ty}.self)))"
)
} else {
format!(
".sequence(SequenceAccess(element: {}, stride: MemoryLayout<{ty}>.stride, elemAlign: MemoryLayout<{ty}>.alignment, witness: .of({ty}.self)))",
descriptor_node(element, element_desc, ctx)
)
}
}
/// A `Set<T>`: a unique per-element sequence. Sets cannot use the scalar bulk
/// array path because the runtime handle is not `[T]`, and decode must rebuild a
/// `Set<T>`.
fn sequence_set(
element: &'static Shape,
element_desc: &ir::Descriptor,
ctx: &mut RecCtx,
) -> String {
let ty = swift_type_base(element);
format!(
".sequence(SequenceAccess(element: {}, stride: MemoryLayout<{ty}>.stride, elemAlign: MemoryLayout<{ty}>.alignment, witness: .setOf({ty}.self)))",
descriptor_node(element, element_desc, ctx)
)
}
/// A variant's payload fields as `(optional Swift label, shape)`. Unit → empty;
/// newtype/tuple → unlabeled; struct → labeled (Swift constructs `.case(label: …)`).
fn variant_payload_fields(variant: &facet_core::Variant) -> Vec<(Option<String>, &'static Shape)> {
match classify_variant(variant) {
VariantKind::Unit => Vec::new(),
VariantKind::Newtype { .. } => vec![(None, variant.data.fields[0].shape())],
VariantKind::Tuple { .. } => variant
.data
.fields
.iter()
.map(|f| (None, f.shape()))
.collect(),
VariantKind::Struct { .. } => variant
.data
.fields
.iter()
.map(|f| (Some(swift_field_name(f.name)), f.shape()))
.collect(),
}
}
fn is_uninhabited_swift_type(shape: &'static Shape) -> bool {
if swift_type_base(shape) == "Infallible" {
return true;
}
matches!(
classify_shape(shape),
ShapeKind::Enum(EnumInfo { variants, .. }) if variants.is_empty()
)
}
fn enum_access(
shape: &'static Shape,
variants: &[facet_core::Variant],
enumeration: &ir::EnumAccess,
ctx: &mut RecCtx,
) -> String {
let ty = swift_type_base(shape);
let mut tag_cases = String::new();
let mut project_cases = String::new();
let mut destroy_cases = String::new();
let mut inject_cases = String::new();
let mut variant_entries = Vec::new();
for (i, v) in variants.iter().enumerate() {
let rust_variant = &enumeration.variants[i];
let wire_index = rust_variant.index;
let case = swift_field_name(v.name);
tag_cases.push_str(&format!("case .{case}: return {i}\n "));
let fields = variant_payload_fields(v);
if fields.is_empty() {
project_cases.push_str(&format!("case .{case}: break\n "));
inject_cases.push_str(&format!("case {i}: v = .{case}\n "));
variant_entries.push(format!(
"VariantAccess(wireIndex: {wire_index}, payloadFields: [], payloadLayout: Layout(size: 0, align: 1))"
));
continue;
}
// The scratch payload record: a Swift tuple of the field types (or the bare
// type for a single field). Field offsets are this tuple's natural layout.
let tys: Vec<String> = fields.iter().map(|(_, s)| swift_type_base(s)).collect();
let uninhabited_payload = fields.iter().any(|(_, s)| is_uninhabited_swift_type(s));
let scratch_ty = if tys.len() == 1 {
tys[0].clone()
} else {
format!("({})", tys.join(", "))
};
let offset = |k: usize| -> String {
if fields.len() == 1 {
"0".to_string()
} else {
format!("MemoryLayout<{scratch_ty}>.offset(of: \\.{k})!")
}
};
if uninhabited_payload {
project_cases.push_str(&format!(
"case .{case}: fatalError(\"uninhabited variant payload\")\n "
));
destroy_cases.push_str(&format!("case {i}: break\n "));
inject_cases.push_str(&format!(
"case {i}: fatalError(\"uninhabited variant payload\")\n "
));
let pfields: Vec<String> = fields
.iter()
.zip(rust_variant.payload.fields.iter())
.enumerate()
.map(|(k, ((_, s), rust_field))| {
format!(
"FieldAccess(offset: {}, descriptor: {})",
offset(k),
descriptor_node(s, &rust_field.descriptor, ctx)
)
})
.collect();
variant_entries.push(format!(
"VariantAccess(wireIndex: {wire_index}, payloadFields: [{}], payloadLayout: MemoryLayout<{scratch_ty}>.phonLayout)",
pfields.join(", ")
));
continue;
}
// project: bind each associated value, store it into scratch at its offset.
let binds: Vec<String> = (0..fields.len()).map(|k| format!("let f{k}")).collect();
let mut proj = format!("case .{case}({}): ", binds.join(", "));
for (k, ty) in tys.iter().enumerate() {
proj.push_str(&format!(
"scratch.advanced(by: {}).assumingMemoryBound(to: {}.self).initialize(to: f{k});",
offset(k),
ty
));
}
project_cases.push_str(&format!("{proj}\n "));
// destroy: deinit each field at its offset.
let mut des = format!("case {i}: ");
for (k, ty) in tys.iter().enumerate() {
des.push_str(&format!(
"scratch.advanced(by: {}).assumingMemoryBound(to: {}.self).deinitialize(count: 1);",
offset(k),
ty
));
}
destroy_cases.push_str(&format!("{des}\n "));
// inject: move each field out of scratch, construct the case (labels for struct).
let mut inj = format!("case {i}: ");
for (k, ty) in tys.iter().enumerate() {
inj.push_str(&format!(
"let f{k} = scratch.advanced(by: {}).assumingMemoryBound(to: {}.self).move(); ",
offset(k),
ty
));
}
let args: Vec<String> = fields
.iter()
.enumerate()
.map(|(k, (label, _))| match label {
Some(l) => format!("{l}: f{k}"),
None => format!("f{k}"),
})
.collect();
inj.push_str(&format!("v = .{case}({})", args.join(", ")));
inject_cases.push_str(&format!("{inj}\n "));
// variant entry: payloadFields at the scratch offsets; payloadLayout = tuple.
let pfields: Vec<String> = fields
.iter()
.zip(rust_variant.payload.fields.iter())
.enumerate()
.map(|(k, ((_, s), rust_field))| {
format!(
"FieldAccess(offset: {}, descriptor: {})",
offset(k),
descriptor_node(s, &rust_field.descriptor, ctx)
)
})
.collect();
variant_entries.push(format!(
"VariantAccess(wireIndex: {wire_index}, payloadFields: [{}], payloadLayout: MemoryLayout<{scratch_ty}>.phonLayout)",
pfields.join(", ")
));
}
format!(
".enumeration(EnumAccess(\n tag: {{ ptr in switch ptr.assumingMemoryBound(to: {ty}.self).pointee {{\n {tag_cases}}} }},\n projectPayload: {{ value, _, scratch in switch value.assumingMemoryBound(to: {ty}.self).pointee {{\n {project_cases}}} }},\n destroyPayload: {{ scratch, localIndex in switch localIndex {{\n {destroy_cases}default: break\n }} }},\n inject: {{ slot, localIndex, scratch in\n let v: {ty}\n switch localIndex {{\n {inject_cases}default: fatalError(\"bad variant index\")\n }}\n slot.assumingMemoryBound(to: {ty}.self).initialize(to: v) }},\n variants: [{}]))",
variant_entries.join(", ")
)
}
/// `Result<Ok, Err>` as a 2-variant enum (`.success`=0, `.failure`=1).
fn result_access(
ok: &'static Shape,
err: &'static Shape,
result: &ir::ResultAccess,
ctx: &mut RecCtx,
) -> String {
let ok_ty = swift_type_base(ok);
let err_ty = swift_type_base(err);
let res_ty = format!("Result<{ok_ty}, {err_ty}>");
format!(
".enumeration(EnumAccess(\n tag: {{ ptr in switch ptr.assumingMemoryBound(to: {res_ty}.self).pointee {{ case .success: return 0; case .failure: return 1 }} }},\n projectPayload: {{ value, _, scratch in switch value.assumingMemoryBound(to: {res_ty}.self).pointee {{ case .success(let f0): scratch.assumingMemoryBound(to: {ok_ty}.self).initialize(to: f0); case .failure(let f0): scratch.assumingMemoryBound(to: {err_ty}.self).initialize(to: f0) }} }},\n destroyPayload: {{ scratch, localIndex in if localIndex == 0 {{ scratch.assumingMemoryBound(to: {ok_ty}.self).deinitialize(count: 1) }} else {{ scratch.assumingMemoryBound(to: {err_ty}.self).deinitialize(count: 1) }} }},\n inject: {{ slot, localIndex, scratch in\n let v: {res_ty} = localIndex == 0 ? .success(scratch.assumingMemoryBound(to: {ok_ty}.self).move()) : .failure(scratch.assumingMemoryBound(to: {err_ty}.self).move())\n slot.assumingMemoryBound(to: {res_ty}.self).initialize(to: v) }},\n variants: [VariantAccess(wireIndex: 0, payloadFields: [FieldAccess(offset: 0, descriptor: {})], payloadLayout: MemoryLayout<{ok_ty}>.phonLayout), VariantAccess(wireIndex: 1, payloadFields: [FieldAccess(offset: 0, descriptor: {})], payloadLayout: MemoryLayout<{err_ty}>.phonLayout)]))",
descriptor_node(ok, &result.ok, ctx),
descriptor_node(err, &result.err, ctx)
)
}
#[cfg(test)]
mod tests {
use super::*;
use facet::Facet;
use phon_schema::{Primitive, primitive_id};
use std::collections::BTreeSet;
#[test]
fn scalar_and_string() {
// A scalar / string has no recursion, so `descriptor_expr` exercises `access_expr`.
assert!(descriptor_expr(<u32 as Facet>::SHAPE).contains("access: .scalar"));
assert!(descriptor_expr(<String as Facet>::SHAPE).contains("witness: .string"));
}
#[test]
fn boxed_pointer_descriptor_uses_pointee_swift_layout() {
let expr = descriptor_expr(<Box<u32> as Facet>::SHAPE);
assert!(expr.contains("MemoryLayout<UInt32>"), "got: {expr}");
assert!(expr.contains("access: .scalar"), "got: {expr}");
}
#[test]
fn envelope_descriptor_emits() {
// Walk the real Message envelope — panics here name the unsupported shape.
let expr = descriptor_expr(<vox_types::Message<'static> as Facet>::SHAPE);
assert!(expr.contains("Descriptor("), "got: {expr}");
assert!(
expr.contains(".enumeration"),
"envelope should be an enum at the root payload"
);
}
#[test]
fn envelope_codec_module_emits() {
let module = generate_phon_codec(&[(
"Message".to_string(),
<vox_types::Message<'static> as Facet>::SHAPE,
)]);
assert!(module.contains("MessageSchemaClosure: [UInt8] = ["));
assert!(module.contains("public func encodeMessage(_ value: Message)"));
// The Descriptor + Registry are emitted (the runtime builds the reconciling
// decode from them); there is NO cached same-schema decode program/function.
assert!(module.contains("MessageDescriptor: Descriptor ="));
assert!(!module.contains("decodeMessage"));
assert!(!module.contains("MessageDecodeProgram"));
}
#[test]
fn uninhabited_variant_payloads_do_not_bind_impossible_values() {
let expr = descriptor_expr(<vox_types::VoxError<std::convert::Infallible> as Facet>::SHAPE);
assert!(
expr.contains("case .user: fatalError(\"uninhabited variant payload\")"),
"generated descriptor:\n{expr}"
);
assert!(!expr.contains("case .user(let f0)"));
assert!(!expr.contains("Infallible.self).move(); v = .user"));
}
fn collect_emitted_schema_ids(swift: &str) -> BTreeSet<u64> {
let mut ids = BTreeSet::new();
let mut rest = swift;
let marker = "SchemaId(0x";
while let Some(index) = rest.find(marker) {
let after_marker = &rest[index + marker.len()..];
let mut hex = String::new();
for ch in after_marker.chars() {
if ch == '_' {
continue;
}
if ch.is_ascii_hexdigit() {
hex.push(ch);
} else {
break;
}
}
ids.insert(u64::from_str_radix(&hex, 16).expect("emitted schema id hex"));
rest = after_marker;
}
ids
}
#[test]
fn styx_value_descriptor_ids_are_resolvable_from_schema_closure() {
let service = spec_proto::all_services()
.into_iter()
.find(|service| service.service_name == "Testbed")
.expect("testbed service exists");
let method = service
.methods
.iter()
.copied()
.find(|method| method.method_name == "echo_styx_value")
.expect("echo_styx_value method exists");
let (descriptor, blocks) = descriptor_with_blocks(method.args_shape);
let mut descriptor_ids = collect_emitted_schema_ids(&descriptor);
for block in blocks.values() {
descriptor_ids.extend(collect_emitted_schema_ids(block));
}
let closure = vox_phon::schema_bytes_for_shape(method.args_shape).expect("schema closure");
let bundle = vox_phon::parse_schema_bytes(&closure).expect("parse schema closure");
let closure_ids = bundle
.schemas
.iter()
.map(|schema| schema.id.as_u64())
.collect::<BTreeSet<_>>();
let primitive_ids = Primitive::ALL
.iter()
.map(|&primitive| primitive_id(primitive).as_u64())
.collect::<BTreeSet<_>>();
let missing = descriptor_ids
.difference(&closure_ids)
.copied()
.filter(|id| !primitive_ids.contains(id))
.collect::<Vec<_>>();
assert!(
missing.is_empty(),
"Swift descriptor references schemas absent from the method closure: {missing:#x?}"
);
}
}