1use std::collections::HashMap;
15use std::fmt::Write;
16
17use phon_schema::{
18 Primitive, Schema, SchemaKind, SchemaRef, VariantPayload, primitive_id, schema_bundle_to_bytes,
19};
20
21use crate::source::Module;
22
23const PRIMITIVES: [Primitive; 21] = [
26 Primitive::Bool,
27 Primitive::U8,
28 Primitive::U16,
29 Primitive::U32,
30 Primitive::U64,
31 Primitive::U128,
32 Primitive::I8,
33 Primitive::I16,
34 Primitive::I32,
35 Primitive::I64,
36 Primitive::I128,
37 Primitive::F32,
38 Primitive::F64,
39 Primitive::Char,
40 Primitive::String,
41 Primitive::Bytes,
42 Primitive::DateTime,
43 Primitive::Uuid,
44 Primitive::QName,
45 Primitive::Unit,
46 Primitive::Never,
47];
48
49#[must_use]
53pub fn render(module: &Module) -> String {
54 let r = Renderer::new(module);
55 r.render()
56}
57
58pub fn schema_bundle_bytes(module: &Module) -> Result<Vec<u8>, phon_schema::DecodeError> {
64 schema_bundle_to_bytes(&module.schemas)
65}
66
67#[must_use]
73pub fn render_registry(module: &Module, name: &str) -> String {
74 let bytes = schema_bundle_bytes(module).expect("codegen module schemas form a valid bundle");
75 let mut out = String::new();
76 let _ = writeln!(
77 out,
78 "export const {name}SchemaBundle = hexToBytes(\"{}\");",
79 hex(&bytes)
80 );
81 let _ = writeln!(
82 out,
83 "export const {name} = new Registry(parseSchemaBundle({name}SchemaBundle));"
84 );
85 out
86}
87
88struct Renderer<'m> {
89 module: &'m Module,
90 prims: HashMap<u64, Primitive>,
91}
92
93impl<'m> Renderer<'m> {
94 fn new(module: &'m Module) -> Self {
95 let prims = PRIMITIVES
96 .iter()
97 .map(|&p| (primitive_id(p).as_u64(), p))
98 .collect();
99 Renderer { module, prims }
100 }
101
102 fn render(&self) -> String {
103 let mut out = String::new();
104 out.push_str("// @generated by phon-codegen — do not edit.\n");
105 out.push_str(
106 "// Type declarations + self-describing schema-bytes for the phon engine.\n\n",
107 );
108 out.push_str(
109 "import { Registry, hexToBytes, parseSchemaBundle } from \"@bearcove/phon-schema\";\n\n",
110 );
111
112 for schema in &self.module.schemas {
114 if let Some(decl) = self.render_decl(schema) {
115 out.push_str(&decl);
116 out.push('\n');
117 }
118 }
119
120 let bytes =
122 schema_bundle_bytes(self.module).expect("codegen module schemas form a valid bundle");
123 let _ = writeln!(
124 out,
125 "export const schemaBundle = hexToBytes(\"{}\");",
126 hex(&bytes)
127 );
128 out.push_str("export const registry = new Registry(parseSchemaBundle(schemaBundle));\n\n");
129
130 out.push_str("export const schemaId = {\n");
132 for root in &self.module.roots {
133 let _ = writeln!(
134 out,
135 " {}: 0x{:016x}n,",
136 ident(&root.name),
137 root.id.as_u64()
138 );
139 }
140 out.push_str("} as const;\n");
141
142 out
143 }
144
145 fn render_decl(&self, schema: &Schema) -> Option<String> {
148 match &schema.kind {
149 SchemaKind::Struct { name, fields } => {
150 let mut s = format!("export interface {} {{\n", type_name(name));
151 for f in fields {
152 let _ = writeln!(
153 s,
154 " {}: {};",
155 field_key(&f.name),
156 self.render_ref(&f.schema)
157 );
158 }
159 s.push_str("}\n");
160 Some(s)
161 }
162 SchemaKind::Enum { name, variants } => {
163 let mut s = format!("export type {} =", type_name(name));
164 for v in variants {
165 s.push_str("\n | ");
166 s.push_str(&self.render_variant(&v.name, &v.payload));
167 }
168 s.push_str(";\n");
169 Some(s)
170 }
171 _ => None,
172 }
173 }
174
175 fn render_variant(&self, name: &str, payload: &VariantPayload) -> String {
178 let tag = format!("tag: {}", json_str(name));
179 match payload {
180 VariantPayload::Unit => format!("{{ {tag} }}"),
181 VariantPayload::Newtype(r) => format!("{{ {tag}; value: {} }}", self.render_ref(r)),
182 VariantPayload::Tuple(refs) => {
183 let inner: Vec<String> = refs.iter().map(|r| self.render_ref(r)).collect();
184 format!("{{ {tag}; value: [{}] }}", inner.join(", "))
185 }
186 VariantPayload::Struct(fields) => {
187 let mut s = format!("{{ {tag}");
188 for f in fields {
189 let _ = write!(
190 s,
191 "; {}: {}",
192 field_key(&f.name),
193 self.render_ref(&f.schema)
194 );
195 }
196 s.push_str(" }");
197 s
198 }
199 }
200 }
201
202 fn render_ref(&self, r: &SchemaRef) -> String {
205 let id = match r {
206 SchemaRef::Concrete { id, .. } => *id,
207 SchemaRef::Var { name } => return format!("/* type var {name} */ unknown"),
208 };
209 if let Some(p) = self.prims.get(&id.as_u64()) {
210 return primitive_ts(*p).to_string();
211 }
212 let Some(schema) = self.module.schema(id) else {
213 return "unknown".to_string();
214 };
215 match &schema.kind {
216 SchemaKind::Struct { name, .. } | SchemaKind::Enum { name, .. } => type_name(name),
217 SchemaKind::List { element } | SchemaKind::Set { element } => {
218 format!("{}[]", self.render_ref_parenthesized(element))
219 }
220 SchemaKind::Array { element, .. } => {
221 format!("{}[]", self.render_ref_parenthesized(element))
222 }
223 SchemaKind::Option { element } => format!("{} | null", self.render_ref(element)),
224 SchemaKind::Map { key, value } => {
225 format!("Map<{}, {}>", self.render_ref(key), self.render_ref(value))
226 }
227 SchemaKind::Tuple { elements } => {
228 let inner: Vec<String> = elements.iter().map(|e| self.render_ref(e)).collect();
229 format!("[{}]", inner.join(", "))
230 }
231 SchemaKind::Semantic { representation, .. } => self.render_ref(representation),
232 SchemaKind::Dynamic => "unknown".to_string(),
233 SchemaKind::Primitive(p) => primitive_ts(*p).to_string(),
234 SchemaKind::Tensor { .. }
235 | SchemaKind::Channel { .. }
236 | SchemaKind::External { .. } => "unknown".to_string(),
237 }
238 }
239
240 fn render_ref_parenthesized(&self, r: &SchemaRef) -> String {
243 let t = self.render_ref(r);
244 if t.contains(" | ") {
245 format!("({t})")
246 } else {
247 t
248 }
249 }
250}
251
252fn primitive_ts(p: Primitive) -> &'static str {
253 match p {
254 Primitive::Bool => "boolean",
255 Primitive::U8
256 | Primitive::U16
257 | Primitive::U32
258 | Primitive::I8
259 | Primitive::I16
260 | Primitive::I32
261 | Primitive::F32
262 | Primitive::F64 => "number",
263 Primitive::U64 | Primitive::U128 | Primitive::I64 | Primitive::I128 => "bigint",
264 Primitive::Char
265 | Primitive::String
266 | Primitive::DateTime
267 | Primitive::Uuid
268 | Primitive::QName => "string",
269 Primitive::Bytes => "Uint8Array",
270 Primitive::Unit => "null",
271 Primitive::Never => "never",
272 }
273}
274
275fn type_name(name: &str) -> String {
277 name.to_string()
278}
279
280fn hex(bytes: &[u8]) -> String {
281 let mut s = String::with_capacity(bytes.len() * 2);
282 for b in bytes {
283 let _ = write!(s, "{b:02x}");
284 }
285 s
286}
287
288fn json_str(s: &str) -> String {
291 let mut out = String::with_capacity(s.len() + 2);
292 out.push('"');
293 for c in s.chars() {
294 match c {
295 '"' => out.push_str("\\\""),
296 '\\' => out.push_str("\\\\"),
297 '\n' => out.push_str("\\n"),
298 '\r' => out.push_str("\\r"),
299 '\t' => out.push_str("\\t"),
300 _ => out.push(c),
301 }
302 }
303 out.push('"');
304 out
305}
306
307fn field_key(name: &str) -> String {
309 let valid = !name.is_empty()
310 && name
311 .chars()
312 .next()
313 .is_some_and(|c| c.is_ascii_alphabetic() || c == '_' || c == '$')
314 && name
315 .chars()
316 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$');
317 if valid {
318 name.to_string()
319 } else {
320 json_str(name)
321 }
322}
323
324fn ident(name: &str) -> String {
326 field_key(name)
327}