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