pub mod phon_client;
pub mod phon_descriptor;
pub mod phon_server;
pub mod phon_service;
pub mod types;
use facet_core::Shape;
use vox_types::ServiceDescriptor;
pub use types::{collect_named_types, generate_named_types};
pub struct WireType {
pub swift_name: String,
pub shape: &'static Shape,
}
pub fn generate_service(service: &ServiceDescriptor) -> String {
validate_channel_rules(service);
super::validate_no_fd_shapes(service, "Swift");
let mut out = String::new();
out.push_str("// @generated by vox-codegen\n");
out.push_str("// DO NOT EDIT - regenerate with `cargo xtask codegen --swift`\n\n");
out.push_str("import Foundation\nimport PhonEngine\nimport PhonIR\nimport PhonSchema\nimport VoxRuntime\n\n");
let named = collect_named_types(service);
let typedefs = generate_named_types(&named)
.replace(": Codable, Sendable, Error", ": Sendable, Error")
.replace(": Codable, Sendable", ": Sendable");
out.push_str(&typedefs);
out.push('\n');
out.push_str(&phon_service::generate_phon_service(service));
out.push_str(&phon_client::generate_phon_client(service));
out.push_str(&phon_server::generate_phon_server(service));
out
}
fn validate_channel_rules(service: &ServiceDescriptor) {
for method in service.methods {
assert!(
!vox_types::shape_contains_channel(method.return_shape),
"channels are not allowed in return types: {}.{}",
method.service_name,
method.method_name
);
assert!(
!vox_types::shape_contains_channel_in_collection(method.args_shape),
"channels are not allowed inside collections: {}.{}",
method.service_name,
method.method_name
);
assert!(
!vox_types::shape_contains_indirect_channel_arg(method.args_shape),
"channels are only allowed as direct method arguments: {}.{}",
method.service_name,
method.method_name
);
}
}
#[cfg(test)]
mod tests {
use std::collections::BTreeSet;
use base64::{Engine, engine::general_purpose::STANDARD};
use facet::{Facet, Shape};
use vox_types::{
MethodDescriptor, MethodDescriptorOptions, MethodId, Rx, ServiceDescriptor, Tx,
method_descriptor,
};
use crate::render::hex_u64;
use super::generate_service;
fn schema_closure_expr(bytes: &[u8]) -> String {
let encoded = STANDARD.encode(bytes);
let mut out = String::from("voxSchemaClosure(\"\"\"\n");
for chunk in encoded.as_bytes().chunks(100) {
out.push_str(" ");
out.push_str(std::str::from_utf8(chunk).expect("base64 is utf-8"));
out.push('\n');
}
out.push_str(" \"\"\")");
out
}
fn parse_self_describing_unique_bundle(
bytes: &[u8],
expected_root: u64,
) -> vox_phon::SchemaBundle {
let bundle = vox_phon::parse_schema_bytes(bytes).expect("schema closure parses");
assert_eq!(bundle.root.as_u64(), expected_root);
let mut seen = BTreeSet::new();
for schema in &bundle.schemas {
assert!(
seen.insert(schema.id.as_u64()),
"schema closure must not repeat type ID {:#x}",
schema.id.as_u64()
);
}
bundle
}
#[derive(Facet)]
struct NestedInner {
value: u32,
}
#[derive(Facet)]
struct NestedOuter {
inner: NestedInner,
}
#[derive(Facet)]
struct NestedChannels {
stream: Tx<u32>,
}
#[derive(Facet)]
struct OuterChannels {
inner: NestedChannels,
}
#[cfg(unix)]
#[derive(Facet)]
struct FdBundle {
descriptors: Vec<vox_types::Fd>,
}
fn service_with_shapes(
args_shape: &'static Shape,
return_shape: &'static Shape,
) -> ServiceDescriptor {
let method = Box::leak(Box::new(MethodDescriptor {
id: MethodId(1),
service_name: "BadSvc",
method_name: "bad",
args_shape,
args: &[],
return_shape,
response_wire_shape: <Result<(), vox_types::VoxError> as Facet>::SHAPE,
args_have_channels: vox_types::shape_contains_channel(args_shape),
doc: None,
}));
let methods = Box::leak(vec![method as &'static MethodDescriptor].into_boxed_slice());
ServiceDescriptor {
service_name: "BadSvc",
methods,
doc: None,
}
}
fn panic_message(panic: Box<dyn std::any::Any + Send>) -> String {
panic
.downcast_ref::<String>()
.cloned()
.or_else(|| panic.downcast_ref::<&'static str>().map(|s| s.to_string()))
.expect("panic payload should be a string")
}
#[test]
fn generated_swift_uses_phon_schema_closures_and_canonical_method_ids() {
let echo = method_descriptor::<(NestedOuter,), NestedOuter>(
"SchemaSvc",
"EchoNested",
&["value"],
&[Some(<NestedOuter as Facet>::SHAPE)],
MethodDescriptorOptions {
response_wire_shape: <Result<NestedOuter, vox_types::VoxError> as Facet>::SHAPE,
doc: None,
},
);
let methods = Box::leak(vec![echo].into_boxed_slice());
let service = ServiceDescriptor {
service_name: "SchemaSvc",
methods,
doc: None,
};
let generated = generate_service(&service);
let args_root = vox_phon::schema_id_for_shape(echo.args_shape).expect("args schema id");
let response_root =
vox_phon::schema_id_for_shape(echo.response_wire_shape).expect("response schema id");
let args_closure =
vox_phon::schema_bytes_for_shape(echo.args_shape).expect("args schema closure");
let response_closure = vox_phon::schema_bytes_for_shape(echo.response_wire_shape)
.expect("response schema closure");
let args_bundle = parse_self_describing_unique_bundle(&args_closure, args_root.as_u64());
let _response_bundle =
parse_self_describing_unique_bundle(&response_closure, response_root.as_u64());
let nested_inner_id =
vox_phon::schema_id_for_shape(<NestedInner as Facet>::SHAPE).expect("inner schema id");
assert_eq!(args_bundle.root, args_root);
assert!(
args_bundle
.schemas
.iter()
.any(|schema| schema.id == nested_inner_id),
"args closure must carry transitively referenced NestedInner schema"
);
assert!(
generated.contains(&format!(
" {}: PhonMethodSchemas(",
hex_u64(crate::method_id(echo))
)),
"generated Swift method table must use the canonical Vox method ID:\n{generated}"
);
assert!(
generated.contains(&format!(
"argsRoot: SchemaId({})",
hex_u64(args_root.as_u64())
)),
"generated Swift must use the phon args schema ID:\n{generated}"
);
assert!(
generated.contains(&format!(
"responseRoot: SchemaId({})",
hex_u64(response_root.as_u64())
)),
"generated Swift must use the phon response schema ID:\n{generated}"
);
assert!(
generated.contains(&format!(
"argsSchemaClosure: {}",
schema_closure_expr(&args_closure)
)),
"generated Swift must embed the self-contained phon args closure:\n{generated}"
);
assert!(
generated.contains(&format!(
"responseSchemaClosure: {}",
schema_closure_expr(&response_closure)
)),
"generated Swift must embed the self-contained phon response closure:\n{generated}"
);
}
#[test]
fn generated_swift_method_ids_include_service_name() {
let alpha = method_descriptor::<(), ()>(
"AlphaSvc",
"echo",
&[],
&[],
MethodDescriptorOptions {
response_wire_shape: <Result<(), vox_types::VoxError> as Facet>::SHAPE,
doc: None,
},
);
let beta = method_descriptor::<(), ()>(
"BetaSvc",
"echo",
&[],
&[],
MethodDescriptorOptions {
response_wire_shape: <Result<(), vox_types::VoxError> as Facet>::SHAPE,
doc: None,
},
);
assert_ne!(crate::method_id(alpha), crate::method_id(beta));
let alpha_methods = Box::leak(vec![alpha].into_boxed_slice());
let beta_methods = Box::leak(vec![beta].into_boxed_slice());
let alpha_service = ServiceDescriptor {
service_name: "AlphaSvc",
methods: alpha_methods,
doc: None,
};
let beta_service = ServiceDescriptor {
service_name: "BetaSvc",
methods: beta_methods,
doc: None,
};
let alpha_generated = generate_service(&alpha_service);
let beta_generated = generate_service(&beta_service);
let alpha_entry = format!(
" {}: PhonMethodSchemas(",
hex_u64(crate::method_id(alpha))
);
let beta_entry = format!(
" {}: PhonMethodSchemas(",
hex_u64(crate::method_id(beta))
);
assert!(
alpha_generated.contains(&alpha_entry),
"Alpha service must emit its service-qualified method ID:\n{alpha_generated}"
);
assert!(
!alpha_generated.contains(&beta_entry),
"Alpha service must not reuse Beta service's same-method-name ID:\n{alpha_generated}"
);
assert!(
beta_generated.contains(&beta_entry),
"Beta service must emit its service-qualified method ID:\n{beta_generated}"
);
}
#[test]
fn generated_swift_emits_rpc_caller_handler_and_dispatch_shapes() {
let echo = method_descriptor::<(String,), String>(
"TestSvc",
"echo",
&["message"],
&[None],
MethodDescriptorOptions {
response_wire_shape: <Result<String, vox_types::VoxError> as Facet>::SHAPE,
doc: None,
},
);
let divide = method_descriptor::<(u64, u64), Result<u64, String>>(
"TestSvc",
"divide",
&["lhs", "rhs"],
&[None, None],
MethodDescriptorOptions {
response_wire_shape: <Result<u64, vox_types::VoxError<String>> as Facet>::SHAPE,
doc: None,
},
);
let methods = Box::leak(vec![echo, divide].into_boxed_slice());
let service = ServiceDescriptor {
service_name: "TestSvc",
methods,
doc: None,
};
let generated = generate_service(&service);
let echo_id = hex_u64(crate::method_id(echo));
let divide_id = hex_u64(crate::method_id(divide));
assert!(
generated.contains("public protocol TestSvcCaller: Sendable {")
&& generated.contains(" func echo(message: String) async throws -> String")
&& generated.contains(
" func divide(lhs: UInt64, rhs: UInt64) async throws -> Result<UInt64, String>",
),
"generated Swift must expose async caller methods matching the source service:\n{generated}"
);
assert!(
generated.contains("public final class TestSvcClient: TestSvcCaller, Sendable {")
&& generated.contains(" private let connection: VoxLane")
&& generated
.contains(" public init(connection: VoxLane, timeout: TimeInterval? = 30.0)")
&& generated.contains("let payload = encodeVoxTyped(message, testSvc_echo_ArgsEncoder)")
&& generated.contains(&format!(
"methodId: {echo_id}, metadata: .null, payload: payload, timeout: timeout,"
))
&& generated.contains(&format!(
"schemaInfo: ClientSchemaInfo(methodSchemas: testSvcMethods[{echo_id}]!, registry: testSvcRegistry)"
))
&& generated.contains("let result: Result<String, VoxError<Infallible>> = try decodeVoxTyped(respDecoder, response)"),
"generated Swift client must delegate request serialization and response handling to VoxLane:\n{generated}"
);
assert!(
generated.contains(" case connectionShutdown")
&& !generated.contains(" case sessionShutdown"),
"generated Swift wire errors must use connection-era vocabulary:\n{generated}"
);
assert!(
generated.contains("public protocol TestSvcHandler: Sendable {")
&& generated.contains(" func echo(message: String) async throws -> String")
&& generated.contains(
" func divide(lhs: UInt64, rhs: UInt64) async throws -> Result<UInt64, String>",
),
"generated Swift handler must expose service methods with server-side argument types:\n{generated}"
);
assert!(
generated.contains("public final class TestSvcDispatcher: ServiceDispatcher {")
&& generated.contains(" private let handler: TestSvcHandler")
&& generated.contains(" public init(handler: TestSvcHandler) { self.handler = handler }")
&& generated.contains("public func dispatch(methodId: UInt64, payload: [UInt8], requestId: UInt64, channels: [UInt64], registry: ChannelRegistry, schemaSendTracker: SchemaSendTracker, schemaReceiveTracker: SchemaTracker, context: RequestContext, taskTx: @escaping @Sendable (TaskMessage) -> Void) async")
&& generated.contains(&format!(
"case {echo_id}: await dispatch_echo(payload: payload, requestId: requestId, schemaSendTracker: schemaSendTracker, schemaReceiveTracker: schemaReceiveTracker, taskTx: taskTx)"
))
&& generated.contains(&format!(
"case {divide_id}: await dispatch_divide(payload: payload, requestId: requestId, schemaSendTracker: schemaSendTracker, schemaReceiveTracker: schemaReceiveTracker, taskTx: taskTx)"
))
&& generated.contains("let voxValue = try await handler.echo(message: args)")
&& generated.contains("let voxValue = try await handler.divide(lhs: args.0, rhs: args.1)")
&& generated.contains("case .failure(let e): voxResult = .failure(.user(e))"),
"generated Swift dispatcher must route decoded args to the handler and reply through TaskMessage:\n{generated}"
);
assert!(
generated.contains(
"default: taskTx(.response(requestId: requestId, payload: encodeVoxError(.unknownMethod), methodId: methodId))"
),
"generated Swift dispatcher must return a call-level UnknownMethod response for unrecognized method ids:\n{generated}"
);
}
#[test]
fn generated_swift_has_no_trailing_line_whitespace() {
let divide = method_descriptor::<(u64, u64), Result<u64, String>>(
"WhitespaceSvc",
"divide",
&["lhs", "rhs"],
&[None, None],
MethodDescriptorOptions {
response_wire_shape: <Result<u64, vox_types::VoxError<String>> as Facet>::SHAPE,
doc: None,
},
);
let methods = Box::leak(vec![divide].into_boxed_slice());
let service = ServiceDescriptor {
service_name: "WhitespaceSvc",
methods,
doc: None,
};
let generated = generate_service(&service);
for (line_idx, line) in generated.lines().enumerate() {
assert_eq!(
line.trim_end_matches([' ', '\t']),
line,
"generated Swift line {} has trailing whitespace: {line:?}",
line_idx + 1
);
}
}
#[test]
fn generated_swift_maps_schema_decode_failures_to_call_errors() {
let echo = method_descriptor::<(NestedOuter,), NestedOuter>(
"SchemaSvc",
"EchoNested",
&["value"],
&[Some(<NestedOuter as Facet>::SHAPE)],
MethodDescriptorOptions {
response_wire_shape: <Result<NestedOuter, vox_types::VoxError> as Facet>::SHAPE,
doc: None,
},
);
let methods = Box::leak(vec![echo].into_boxed_slice());
let service = ServiceDescriptor {
service_name: "SchemaSvc",
methods,
doc: None,
};
let generated = generate_service(&service);
assert!(
generated.contains("guard let argsDecoder = schemaReceiveTracker.buildDecodeFn"),
"generated Swift server must build a caller->callee args decode plan:\n{generated}"
);
assert!(
generated.contains("encodeVoxError(.invalidPayload(\"no args schema advertised\"))"),
"generated Swift server must reject missing args decode plans as call errors:\n{generated}"
);
assert!(
generated.contains("do { args = try decodeVoxTyped(argsDecoder, payload) } catch {")
&& generated.contains("encodeVoxError(.invalidPayload(\"decode args\"))"),
"generated Swift server must turn args decode failures into InvalidPayload responses:\n{generated}"
);
assert!(
generated
.contains("guard let respDecoder = connection.schemaReceiveTracker.buildDecodeFn"),
"generated Swift client must build a callee->caller response decode plan:\n{generated}"
);
assert!(
generated.contains(
"throw VoxError<Infallible>.invalidPayload(\"no response schema advertised\")",
),
"generated Swift client must surface response decode-plan failure on the call:\n{generated}"
);
assert_eq!(
generated.matches("connection.call(").count(),
1,
"generated Swift client must issue one request attempt and not replay after local decode failure:\n{generated}"
);
}
#[test]
fn generated_swift_emits_channel_schemas() {
let subscribe = method_descriptor::<(Tx<NestedInner>, Rx<NestedInner>), ()>(
"StreamSvc",
"subscribe",
&["output", "input"],
&[
Some(<NestedInner as facet::Facet>::SHAPE),
Some(<NestedInner as facet::Facet>::SHAPE),
],
MethodDescriptorOptions {
response_wire_shape: <Result<(), vox_types::VoxError> as Facet>::SHAPE,
doc: None,
},
);
let methods = Box::leak(vec![subscribe].into_boxed_slice());
let service = ServiceDescriptor {
service_name: "StreamSvc",
methods,
doc: None,
};
let generated = generate_service(&service);
let args_closure = vox_phon::schema_bytes_for_shape_with_auxiliary_roots(
subscribe.args_shape,
&[
(
"channel.arg.0.tx.element",
<NestedInner as facet::Facet>::SHAPE,
),
(
"channel.arg.1.rx.element",
<NestedInner as facet::Facet>::SHAPE,
),
],
)
.expect("args schema closure with channel auxiliary roots");
assert!(
generated.contains("channel.arg.0.tx.element"),
"Tx<T> channel receive decode must use the named auxiliary root:\n{generated}"
);
assert!(
generated.contains("channel.arg.1.rx.element"),
"Rx<T> channel receive decode must use the named auxiliary root:\n{generated}"
);
assert!(
generated.contains(&format!(
"argsSchemaClosure: {}",
schema_closure_expr(&args_closure)
)),
"channel element roots must be carried in argsSchemaClosure auxiliary roots:\n{generated}"
);
assert!(
generated.contains("buildAuxiliaryDecodeFn"),
"generated Swift must build channel item decode programs from advertised schemas:\n{generated}"
);
assert!(
!generated.contains("ElementDecodeProgram"),
"generated Swift must not emit local same-schema channel item decode programs:\n{generated}"
);
assert!(
generated.contains("public struct NestedInner"),
"named channel element types must be emitted as normal generated types:\n{generated}"
);
assert!(
generated.contains("var channelIds: [UInt64] = []")
&& generated.contains("let outputWireIndex = channelIds.count")
&& generated.contains("channelIds.append(await connection.bindClientTxArg(output")
&& generated.contains("let inputWireIndex = channelIds.count")
&& generated.contains("channelIds.append(await connection.bindClientRxArg(input")
&& generated.contains("channels: channelIds,"),
"generated Swift caller must discover direct channel args left-to-right and send that channel id list:\n{generated}"
);
assert!(
generated.contains("let outputWireIndex = channelWireIndex(args.0)")
&& generated.contains("channels[outputWireIndex]")
&& generated.contains("let inputWireIndex = channelWireIndex(args.1)")
&& generated.contains("channels[inputWireIndex]"),
"generated Swift dispatcher must resolve decoded channel wire indexes through Request.channels:\n{generated}"
);
}
#[test]
fn generated_swift_rejects_channels_in_return_shapes() {
let service = service_with_shapes(<() as Facet>::SHAPE, <Tx<u32> as Facet>::SHAPE);
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
generate_service(&service);
}));
let message = panic_message(result.expect_err("generator should reject channel returns"));
assert!(
message.contains("channels are not allowed in return types: BadSvc.bad"),
"unexpected panic message: {message}"
);
}
#[test]
fn generated_swift_rejects_nested_channel_args() {
let service = service_with_shapes(<(OuterChannels,) as Facet>::SHAPE, <() as Facet>::SHAPE);
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
generate_service(&service);
}));
let message = panic_message(result.expect_err("generator should reject nested channels"));
assert!(
message.contains("channels are only allowed as direct method arguments: BadSvc.bad"),
"unexpected panic message: {message}"
);
}
#[test]
fn generated_swift_rejects_channels_inside_collections() {
let service = service_with_shapes(<(Vec<Tx<u32>>,) as Facet>::SHAPE, <() as Facet>::SHAPE);
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
generate_service(&service);
}));
let message =
panic_message(result.expect_err("generator should reject collection channels"));
assert!(
message.contains("channels are not allowed inside collections: BadSvc.bad"),
"unexpected panic message: {message}"
);
}
#[cfg(unix)]
#[test]
fn generated_swift_rejects_fd_capability_surfaces() {
let service = service_with_shapes(<() as Facet>::SHAPE, <FdBundle as Facet>::SHAPE);
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
generate_service(&service);
}));
let message =
panic_message(result.expect_err("generator should reject fd capability surfaces"));
assert!(
message.contains(
"Swift codegen does not support vox::Fd capabilities: BadSvc.bad return type"
),
"unexpected panic message: {message}"
);
}
}