Skip to main content

vexil_codegen_ts/
message.rs

1use vexil_lang::ast::{PrimitiveType, SemanticType};
2use vexil_lang::ir::{
3    ConfigDef, Encoding, FieldEncoding, MessageDef, ResolvedType, TypeDef, TypeRegistry,
4};
5
6use crate::emit::CodeWriter;
7use crate::types::ts_type;
8
9// ---------------------------------------------------------------------------
10// Byte-alignment helper
11// ---------------------------------------------------------------------------
12
13/// Returns true if the type is byte-aligned (i.e., not sub-byte).
14pub fn is_byte_aligned(ty: &ResolvedType, registry: &TypeRegistry) -> bool {
15    match ty {
16        ResolvedType::Primitive(PrimitiveType::Bool) => false,
17        ResolvedType::SubByte(_) => false,
18        ResolvedType::Named(id) => {
19            if let Some(TypeDef::Enum(e)) = registry.get(*id) {
20                e.wire_bits >= 8
21            } else {
22                true
23            }
24        }
25        ResolvedType::Optional(inner) => is_byte_aligned(inner, registry),
26        _ => true,
27    }
28}
29
30// ---------------------------------------------------------------------------
31// Primitive bits helper
32// ---------------------------------------------------------------------------
33
34fn primitive_bits(p: &PrimitiveType) -> u8 {
35    match p {
36        PrimitiveType::I8 | PrimitiveType::U8 => 8,
37        PrimitiveType::I16 | PrimitiveType::U16 => 16,
38        PrimitiveType::I32 | PrimitiveType::U32 | PrimitiveType::F32 => 32,
39        PrimitiveType::I64 | PrimitiveType::U64 | PrimitiveType::F64 => 64,
40        _ => 0,
41    }
42}
43
44// ---------------------------------------------------------------------------
45// emit_write
46// ---------------------------------------------------------------------------
47
48/// Emit code to write a value to a BitWriter.
49///
50/// `access` is the TypeScript expression for the value.
51/// `writer` is the variable name of the BitWriter (e.g. "w" or "payloadW").
52pub fn emit_write(
53    w: &mut CodeWriter,
54    access: &str,
55    ty: &ResolvedType,
56    enc: &FieldEncoding,
57    registry: &TypeRegistry,
58    writer: &str,
59) {
60    // Check non-default encoding first
61    match &enc.encoding {
62        Encoding::Varint => {
63            let is_64 = matches!(
64                ty,
65                ResolvedType::Primitive(PrimitiveType::U64 | PrimitiveType::I64)
66            );
67            if is_64 {
68                w.line(&format!("{writer}.writeLeb12864({access});"));
69            } else {
70                w.line(&format!("{writer}.writeLeb128({access});"));
71            }
72            return;
73        }
74        Encoding::ZigZag => {
75            let type_bits = match ty {
76                ResolvedType::Primitive(p) => primitive_bits(p),
77                _ => 64,
78            };
79            if type_bits == 64 {
80                w.line(&format!("{writer}.writeZigZag64({access});"));
81            } else {
82                w.line(&format!("{writer}.writeZigZag({access}, {type_bits});",));
83            }
84            return;
85        }
86        Encoding::Delta(inner) => {
87            let base_enc = FieldEncoding {
88                encoding: *inner.clone(),
89                limit: enc.limit,
90            };
91            emit_write(w, access, ty, &base_enc, registry, writer);
92            return;
93        }
94        Encoding::Default => {} // fall through to type dispatch
95        _ => {}                 // non_exhaustive guard
96    }
97
98    emit_write_type(w, access, ty, registry, writer);
99}
100
101fn emit_write_type(
102    w: &mut CodeWriter,
103    access: &str,
104    ty: &ResolvedType,
105    registry: &TypeRegistry,
106    writer: &str,
107) {
108    match ty {
109        ResolvedType::Primitive(p) => match p {
110            PrimitiveType::Bool => w.line(&format!("{writer}.writeBool({access});")),
111            PrimitiveType::U8 => w.line(&format!("{writer}.writeU8({access});")),
112            PrimitiveType::U16 => w.line(&format!("{writer}.writeU16({access});")),
113            PrimitiveType::U32 => w.line(&format!("{writer}.writeU32({access});")),
114            PrimitiveType::U64 => w.line(&format!("{writer}.writeU64({access});")),
115            PrimitiveType::I8 => w.line(&format!("{writer}.writeI8({access});")),
116            PrimitiveType::I16 => w.line(&format!("{writer}.writeI16({access});")),
117            PrimitiveType::I32 => w.line(&format!("{writer}.writeI32({access});")),
118            PrimitiveType::I64 => w.line(&format!("{writer}.writeI64({access});")),
119            PrimitiveType::F32 => w.line(&format!("{writer}.writeF32({access});")),
120            PrimitiveType::F64 => w.line(&format!("{writer}.writeF64({access});")),
121            PrimitiveType::Void => {} // 0 bits — nothing to write
122        },
123        ResolvedType::SubByte(s) => {
124            let bits = s.bits;
125            w.line(&format!("{writer}.writeBits({access}, {bits});"));
126        }
127        ResolvedType::Semantic(s) => match s {
128            SemanticType::String => w.line(&format!("{writer}.writeString({access});")),
129            SemanticType::Bytes => w.line(&format!("{writer}.writeBytes({access});")),
130            SemanticType::Rgb => {
131                w.line(&format!("{writer}.writeU8({access}[0]);"));
132                w.line(&format!("{writer}.writeU8({access}[1]);"));
133                w.line(&format!("{writer}.writeU8({access}[2]);"));
134            }
135            SemanticType::Uuid => w.line(&format!("{writer}.writeRawBytes({access});")),
136            SemanticType::Timestamp => w.line(&format!("{writer}.writeI64({access});")),
137            SemanticType::Hash => w.line(&format!("{writer}.writeRawBytes({access});")),
138        },
139        ResolvedType::Named(id) => {
140            let type_name = match registry.get(*id) {
141                Some(def) => match def {
142                    TypeDef::Message(m) => m.name.to_string(),
143                    TypeDef::Enum(e) => e.name.to_string(),
144                    TypeDef::Flags(f) => f.name.to_string(),
145                    TypeDef::Union(u) => u.name.to_string(),
146                    TypeDef::Newtype(n) => n.name.to_string(),
147                    _ => "Unknown".to_string(),
148                },
149                None => "Unknown".to_string(),
150            };
151            w.line(&format!("{writer}.enterNested();"));
152            w.line(&format!("encode{type_name}({access}, {writer});"));
153            w.line(&format!("{writer}.leaveNested();"));
154        }
155        ResolvedType::Optional(inner) => {
156            // Presence bit
157            w.line(&format!("{writer}.writeBool({access} !== null);"));
158            if is_byte_aligned(inner, registry) {
159                w.line(&format!("{writer}.flushToByteBoundary();"));
160            }
161            w.open_block(&format!("if ({access} !== null)"));
162            emit_write_type(w, access, inner, registry, writer);
163            w.close_block();
164        }
165        ResolvedType::Array(inner) => {
166            w.line(&format!("{writer}.writeLeb128({access}.length);"));
167            w.open_block(&format!("for (const item of {access})"));
168            emit_write_type(w, "item", inner, registry, writer);
169            w.close_block();
170        }
171        ResolvedType::Map(k, v) => {
172            w.line(&format!("{writer}.writeLeb128({access}.size);"));
173            w.open_block(&format!("for (const [mapK, mapV] of {access})"));
174            emit_write_type(w, "mapK", k, registry, writer);
175            emit_write_type(w, "mapV", v, registry, writer);
176            w.close_block();
177        }
178        ResolvedType::Result(ok, err) => {
179            w.open_block(&format!("if ('ok' in {access})"));
180            w.line(&format!("{writer}.writeBool(true);"));
181            emit_write_type(w, &format!("{access}.ok"), ok, registry, writer);
182            w.dedent();
183            w.line("} else {");
184            w.indent();
185            w.line(&format!("{writer}.writeBool(false);"));
186            emit_write_type(w, &format!("{access}.err"), err, registry, writer);
187            w.close_block();
188        }
189        _ => {} // non_exhaustive guard
190    }
191}
192
193// ---------------------------------------------------------------------------
194// emit_read
195// ---------------------------------------------------------------------------
196
197/// Emit code to read a value from a BitReader and bind to `var_name`.
198///
199/// `reader` is the variable name of the BitReader (e.g. "r" or "pr").
200pub fn emit_read(
201    w: &mut CodeWriter,
202    var_name: &str,
203    ty: &ResolvedType,
204    enc: &FieldEncoding,
205    registry: &TypeRegistry,
206    reader: &str,
207) {
208    match &enc.encoding {
209        Encoding::Varint => {
210            let is_64 = matches!(
211                ty,
212                ResolvedType::Primitive(PrimitiveType::U64 | PrimitiveType::I64)
213            );
214            if is_64 {
215                w.line(&format!("const {var_name} = {reader}.readLeb12864();"));
216            } else {
217                w.line(&format!("const {var_name} = {reader}.readLeb128();"));
218            }
219            return;
220        }
221        Encoding::ZigZag => {
222            let type_bits = match ty {
223                ResolvedType::Primitive(p) => primitive_bits(p),
224                _ => 64,
225            };
226            if type_bits == 64 {
227                w.line(&format!("const {var_name} = {reader}.readZigZag64();",));
228            } else {
229                w.line(&format!(
230                    "const {var_name} = {reader}.readZigZag({type_bits});",
231                ));
232            }
233            return;
234        }
235        Encoding::Delta(inner) => {
236            let base_enc = FieldEncoding {
237                encoding: *inner.clone(),
238                limit: enc.limit,
239            };
240            emit_read(w, var_name, ty, &base_enc, registry, reader);
241            return;
242        }
243        Encoding::Default => {}
244        _ => {} // non_exhaustive guard
245    }
246
247    emit_read_type(w, var_name, ty, registry, reader);
248}
249
250fn emit_read_type(
251    w: &mut CodeWriter,
252    var_name: &str,
253    ty: &ResolvedType,
254    registry: &TypeRegistry,
255    reader: &str,
256) {
257    match ty {
258        ResolvedType::Primitive(p) => match p {
259            PrimitiveType::Bool => {
260                w.line(&format!("const {var_name} = {reader}.readBool();"));
261            }
262            PrimitiveType::U8 => {
263                w.line(&format!("const {var_name} = {reader}.readU8();"));
264            }
265            PrimitiveType::U16 => {
266                w.line(&format!("const {var_name} = {reader}.readU16();"));
267            }
268            PrimitiveType::U32 => {
269                w.line(&format!("const {var_name} = {reader}.readU32();"));
270            }
271            PrimitiveType::U64 => {
272                w.line(&format!("const {var_name} = {reader}.readU64();"));
273            }
274            PrimitiveType::I8 => {
275                w.line(&format!("const {var_name} = {reader}.readI8();"));
276            }
277            PrimitiveType::I16 => {
278                w.line(&format!("const {var_name} = {reader}.readI16();"));
279            }
280            PrimitiveType::I32 => {
281                w.line(&format!("const {var_name} = {reader}.readI32();"));
282            }
283            PrimitiveType::I64 => {
284                w.line(&format!("const {var_name} = {reader}.readI64();"));
285            }
286            PrimitiveType::F32 => {
287                w.line(&format!("const {var_name} = {reader}.readF32();"));
288            }
289            PrimitiveType::F64 => {
290                w.line(&format!("const {var_name} = {reader}.readF64();"));
291            }
292            PrimitiveType::Void => {
293                w.line(&format!("const {var_name} = undefined;"));
294            }
295        },
296        ResolvedType::SubByte(s) => {
297            let bits = s.bits;
298            if s.signed {
299                // Sign-extend sub-byte value
300                let shift = 8 - bits;
301                w.line(&format!(
302                    "const {var_name} = ({reader}.readBits({bits}) << {shift}) >> {shift};",
303                ));
304            } else {
305                w.line(&format!("const {var_name} = {reader}.readBits({bits});",));
306            }
307        }
308        ResolvedType::Semantic(s) => match s {
309            SemanticType::String => {
310                w.line(&format!("const {var_name} = {reader}.readString();"));
311            }
312            SemanticType::Bytes => {
313                w.line(&format!("const {var_name} = {reader}.readBytes();"));
314            }
315            SemanticType::Rgb => {
316                w.line(&format!("const {var_name}_0 = {reader}.readU8();"));
317                w.line(&format!("const {var_name}_1 = {reader}.readU8();"));
318                w.line(&format!("const {var_name}_2 = {reader}.readU8();"));
319                w.line(&format!(
320                    "const {var_name}: [number, number, number] = [{var_name}_0, {var_name}_1, {var_name}_2];"
321                ));
322            }
323            SemanticType::Uuid => {
324                w.line(&format!("const {var_name} = {reader}.readRawBytes(16);"));
325            }
326            SemanticType::Timestamp => {
327                w.line(&format!("const {var_name} = {reader}.readI64();"));
328            }
329            SemanticType::Hash => {
330                w.line(&format!("const {var_name} = {reader}.readRawBytes(32);"));
331            }
332        },
333        ResolvedType::Named(id) => {
334            let type_name = match registry.get(*id) {
335                Some(def) => match def {
336                    TypeDef::Message(m) => m.name.to_string(),
337                    TypeDef::Enum(e) => e.name.to_string(),
338                    TypeDef::Flags(f) => f.name.to_string(),
339                    TypeDef::Union(u) => u.name.to_string(),
340                    TypeDef::Newtype(n) => n.name.to_string(),
341                    _ => "Unknown".to_string(),
342                },
343                None => "Unknown".to_string(),
344            };
345            w.line(&format!("{reader}.enterNested();"));
346            w.line(&format!("const {var_name} = decode{type_name}({reader});"));
347            w.line(&format!("{reader}.leaveNested();"));
348        }
349        ResolvedType::Optional(inner) => {
350            w.line(&format!("const {var_name}_present = {reader}.readBool();"));
351            if is_byte_aligned(inner, registry) {
352                w.line(&format!("{reader}.flushToByteBoundary();"));
353            }
354            let inner_ts = ts_type(inner, registry);
355            w.line(&format!("let {var_name}: {inner_ts} | null;",));
356            w.open_block(&format!("if ({var_name}_present)"));
357            emit_read_type(w, &format!("{var_name}_inner"), inner, registry, reader);
358            w.line(&format!("{var_name} = {var_name}_inner;"));
359            w.dedent();
360            w.line("} else {");
361            w.indent();
362            w.line(&format!("{var_name} = null;"));
363            w.close_block();
364        }
365        ResolvedType::Array(inner) => {
366            w.line(&format!("const {var_name}_len = {reader}.readLeb128();"));
367            let inner_ts = ts_type(inner, registry);
368            w.line(&format!("const {var_name}: {inner_ts}[] = [];"));
369            w.open_block(&format!("for (let i = 0; i < {var_name}_len; i++)"));
370            emit_read_type(w, &format!("{var_name}_item"), inner, registry, reader);
371            w.line(&format!("{var_name}.push({var_name}_item);"));
372            w.close_block();
373        }
374        ResolvedType::Map(k, v) => {
375            w.line(&format!("const {var_name}_len = {reader}.readLeb128();"));
376            let k_ts = ts_type(k, registry);
377            let v_ts = ts_type(v, registry);
378            w.line(&format!("const {var_name} = new Map<{k_ts}, {v_ts}>();"));
379            w.open_block(&format!("for (let i = 0; i < {var_name}_len; i++)"));
380            emit_read_type(w, &format!("{var_name}_k"), k, registry, reader);
381            emit_read_type(w, &format!("{var_name}_v"), v, registry, reader);
382            w.line(&format!("{var_name}.set({var_name}_k, {var_name}_v);"));
383            w.close_block();
384        }
385        ResolvedType::Result(ok, err) => {
386            let ok_ts = ts_type(ok, registry);
387            let err_ts = ts_type(err, registry);
388            w.line(&format!("const {var_name}_isOk = {reader}.readBool();"));
389            w.line(&format!(
390                "let {var_name}: {{ ok: {ok_ts} }} | {{ err: {err_ts} }};"
391            ));
392            w.open_block(&format!("if ({var_name}_isOk)"));
393            emit_read_type(w, &format!("{var_name}_ok"), ok, registry, reader);
394            w.line(&format!("{var_name} = {{ ok: {var_name}_ok }};"));
395            w.dedent();
396            w.line("} else {");
397            w.indent();
398            emit_read_type(w, &format!("{var_name}_err"), err, registry, reader);
399            w.line(&format!("{var_name} = {{ err: {var_name}_err }};"));
400            w.close_block();
401        }
402        _ => {} // non_exhaustive guard
403    }
404}
405
406// ---------------------------------------------------------------------------
407// emit_message
408// ---------------------------------------------------------------------------
409
410/// Emit a complete message: interface + encode function + decode function.
411pub fn emit_message(w: &mut CodeWriter, msg: &MessageDef, registry: &TypeRegistry) {
412    let name = msg.name.as_str();
413
414    // Interface
415    w.open_block(&format!("export interface {name}"));
416    for field in &msg.fields {
417        let field_ts = ts_type(&field.resolved_type, registry);
418        w.line(&format!("{}: {};", field.name, field_ts));
419    }
420    w.close_block();
421    w.blank();
422
423    // Encode function
424    w.open_block(&format!(
425        "export function encode{name}(v: {name}, w: BitWriter): void"
426    ));
427    for field in &msg.fields {
428        let access = format!("v.{}", field.name);
429        emit_write(
430            w,
431            &access,
432            &field.resolved_type,
433            &field.encoding,
434            registry,
435            "w",
436        );
437    }
438    w.line("w.flushToByteBoundary();");
439    w.close_block();
440    w.blank();
441
442    // Decode function
443    w.open_block(&format!(
444        "export function decode{name}(r: BitReader): {name}"
445    ));
446    for field in &msg.fields {
447        emit_read(
448            w,
449            field.name.as_str(),
450            &field.resolved_type,
451            &field.encoding,
452            registry,
453            "r",
454        );
455    }
456    w.line("r.flushToByteBoundary();");
457    let field_names: Vec<&str> = msg.fields.iter().map(|f| f.name.as_str()).collect();
458    w.line(&format!("return {{ {} }};", field_names.join(", ")));
459    w.close_block();
460    w.blank();
461}
462
463// ---------------------------------------------------------------------------
464// emit_config
465// ---------------------------------------------------------------------------
466
467/// Emit a config type: interface only (no codec).
468pub fn emit_config(w: &mut CodeWriter, cfg: &ConfigDef, registry: &TypeRegistry) {
469    let name = cfg.name.as_str();
470
471    w.open_block(&format!("export interface {name}"));
472    for field in &cfg.fields {
473        let field_ts = ts_type(&field.resolved_type, registry);
474        w.line(&format!("{}: {};", field.name, field_ts));
475    }
476    w.close_block();
477    w.blank();
478}