use std::collections::HashMap;
use std::fmt::Write;
use phon_schema::{
Primitive, Schema, SchemaKind, SchemaRef, VariantPayload, primitive_id, schema_bundle_to_bytes,
};
use crate::source::Module;
const PRIMITIVES: [Primitive; 21] = [
Primitive::Bool,
Primitive::U8,
Primitive::U16,
Primitive::U32,
Primitive::U64,
Primitive::U128,
Primitive::I8,
Primitive::I16,
Primitive::I32,
Primitive::I64,
Primitive::I128,
Primitive::F32,
Primitive::F64,
Primitive::Char,
Primitive::String,
Primitive::Bytes,
Primitive::DateTime,
Primitive::Uuid,
Primitive::QName,
Primitive::Unit,
Primitive::Never,
];
#[must_use]
pub fn render(module: &Module) -> String {
let r = Renderer::new(module);
r.render()
}
pub fn schema_bundle_bytes(module: &Module) -> Result<Vec<u8>, phon_schema::DecodeError> {
schema_bundle_to_bytes(&module.schemas)
}
#[must_use]
pub fn render_registry(module: &Module, name: &str) -> String {
let bytes = schema_bundle_bytes(module).expect("codegen module schemas form a valid bundle");
let mut out = String::new();
let _ = writeln!(
out,
"export const {name}SchemaBundle = hexToBytes(\"{}\");",
hex(&bytes)
);
let _ = writeln!(
out,
"export const {name} = new Registry(parseSchemaBundle({name}SchemaBundle));"
);
out
}
struct Renderer<'m> {
module: &'m Module,
prims: HashMap<u64, Primitive>,
}
impl<'m> Renderer<'m> {
fn new(module: &'m Module) -> Self {
let prims = PRIMITIVES
.iter()
.map(|&p| (primitive_id(p).as_u64(), p))
.collect();
Renderer { module, prims }
}
fn render(&self) -> String {
let mut out = String::new();
out.push_str("// @generated by phon-codegen — do not edit.\n");
out.push_str(
"// Type declarations + self-describing schema-bytes for the phon engine.\n\n",
);
out.push_str(
"import { Registry, hexToBytes, parseSchemaBundle } from \"@bearcove/phon-schema\";\n\n",
);
for schema in &self.module.schemas {
if let Some(decl) = self.render_decl(schema) {
out.push_str(&decl);
out.push('\n');
}
}
let bytes =
schema_bundle_bytes(self.module).expect("codegen module schemas form a valid bundle");
let _ = writeln!(
out,
"export const schemaBundle = hexToBytes(\"{}\");",
hex(&bytes)
);
out.push_str("export const registry = new Registry(parseSchemaBundle(schemaBundle));\n\n");
out.push_str("export const schemaId = {\n");
for root in &self.module.roots {
let _ = writeln!(
out,
" {}: 0x{:016x}n,",
ident(&root.name),
root.id.as_u64()
);
}
out.push_str("} as const;\n");
out
}
fn render_decl(&self, schema: &Schema) -> Option<String> {
match &schema.kind {
SchemaKind::Struct { name, fields } => {
let mut s = format!("export interface {} {{\n", type_name(name));
for f in fields {
let _ = writeln!(
s,
" {}: {};",
field_key(&f.name),
self.render_ref(&f.schema)
);
}
s.push_str("}\n");
Some(s)
}
SchemaKind::Enum { name, variants } => {
let mut s = format!("export type {} =", type_name(name));
for v in variants {
s.push_str("\n | ");
s.push_str(&self.render_variant(&v.name, &v.payload));
}
s.push_str(";\n");
Some(s)
}
_ => None,
}
}
fn render_variant(&self, name: &str, payload: &VariantPayload) -> String {
let tag = format!("tag: {}", json_str(name));
match payload {
VariantPayload::Unit => format!("{{ {tag} }}"),
VariantPayload::Newtype(r) => format!("{{ {tag}; value: {} }}", self.render_ref(r)),
VariantPayload::Tuple(refs) => {
let inner: Vec<String> = refs.iter().map(|r| self.render_ref(r)).collect();
format!("{{ {tag}; value: [{}] }}", inner.join(", "))
}
VariantPayload::Struct(fields) => {
let mut s = format!("{{ {tag}");
for f in fields {
let _ = write!(
s,
"; {}: {}",
field_key(&f.name),
self.render_ref(&f.schema)
);
}
s.push_str(" }");
s
}
}
}
fn render_ref(&self, r: &SchemaRef) -> String {
let id = match r {
SchemaRef::Concrete { id, .. } => *id,
SchemaRef::Var { name } => return format!("/* type var {name} */ unknown"),
};
if let Some(p) = self.prims.get(&id.as_u64()) {
return primitive_ts(*p).to_string();
}
let Some(schema) = self.module.schema(id) else {
return "unknown".to_string();
};
match &schema.kind {
SchemaKind::Struct { name, .. } | SchemaKind::Enum { name, .. } => type_name(name),
SchemaKind::List { element } | SchemaKind::Set { element } => {
format!("{}[]", self.render_ref_parenthesized(element))
}
SchemaKind::Array { element, .. } => {
format!("{}[]", self.render_ref_parenthesized(element))
}
SchemaKind::Option { element } => format!("{} | null", self.render_ref(element)),
SchemaKind::Map { key, value } => {
format!("Map<{}, {}>", self.render_ref(key), self.render_ref(value))
}
SchemaKind::Tuple { elements } => {
let inner: Vec<String> = elements.iter().map(|e| self.render_ref(e)).collect();
format!("[{}]", inner.join(", "))
}
SchemaKind::Semantic { representation, .. } => self.render_ref(representation),
SchemaKind::Dynamic => "unknown".to_string(),
SchemaKind::Primitive(p) => primitive_ts(*p).to_string(),
SchemaKind::Tensor { .. }
| SchemaKind::Channel { .. }
| SchemaKind::External { .. } => "unknown".to_string(),
}
}
fn render_ref_parenthesized(&self, r: &SchemaRef) -> String {
let t = self.render_ref(r);
if t.contains(" | ") {
format!("({t})")
} else {
t
}
}
}
fn primitive_ts(p: Primitive) -> &'static str {
match p {
Primitive::Bool => "boolean",
Primitive::U8
| Primitive::U16
| Primitive::U32
| Primitive::I8
| Primitive::I16
| Primitive::I32
| Primitive::F32
| Primitive::F64 => "number",
Primitive::U64 | Primitive::U128 | Primitive::I64 | Primitive::I128 => "bigint",
Primitive::Char
| Primitive::String
| Primitive::DateTime
| Primitive::Uuid
| Primitive::QName => "string",
Primitive::Bytes => "Uint8Array",
Primitive::Unit => "null",
Primitive::Never => "never",
}
}
fn type_name(name: &str) -> String {
name.to_string()
}
fn hex(bytes: &[u8]) -> String {
let mut s = String::with_capacity(bytes.len() * 2);
for b in bytes {
let _ = write!(s, "{b:02x}");
}
s
}
fn json_str(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 2);
out.push('"');
for c in s.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
_ => out.push(c),
}
}
out.push('"');
out
}
fn field_key(name: &str) -> String {
let valid = !name.is_empty()
&& name
.chars()
.next()
.is_some_and(|c| c.is_ascii_alphabetic() || c == '_' || c == '$')
&& name
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$');
if valid {
name.to_string()
} else {
json_str(name)
}
}
fn ident(name: &str) -> String {
field_key(name)
}