Skip to main content

submilli_engine/codegen/function_emitter/
json.rs

1//! Codegen for the `JSON.stringify` and `JSON.parse` compiler intrinsics.
2
3use crate::codegen::CodegenCtx;
4use crate::codegen::function_emitter::FunctionEmitter;
5use crate::codegen::function_emitter::expr::{emit_expr, emit_vtable_dispatch_on_object_stack};
6use crate::codegen::intrinsics::IntrinsicTypeIndices;
7use crate::codegen::symbol_table::may_hold_null;
8use crate::{ExprId, Type};
9use wasm_encoder::{BlockType, Function, HeapType, Instruction, RefType, ValType};
10
11/// Thrown when `JSON.stringify` has no JSON text and the call is typed `string`.
12pub(crate) const NO_JSON_TEXT_MESSAGE: &str = "`JSON.stringify` has no JSON text for this value (it is `undefined` or a function); use `JSON.stringify(value) ?? …` or check the value first";
13
14pub(super) fn emit_stringify(
15    emitter: &mut FunctionEmitter,
16    ctx: &CodegenCtx,
17    args: &[ExprId],
18    slot_ty: &Type,
19    source_ty: &Type,
20) -> Result<(), crate::compiler_error::CompilerFailure> {
21    let [arg, rest @ ..] = args else {
22        return Err(crate::codegen::internal_failure(
23            "JSON.stringify requires a value",
24        ));
25    };
26    if rest.len() > 2 {
27        return Err(crate::codegen::internal_failure(
28            "JSON.stringify accepts at most three arguments",
29        ));
30    }
31    let arg = *arg;
32    let arg_ty = ctx
33        .ta
34        .try_expr(arg)
35        .map_err(crate::codegen::arena_failure)?
36        .ty
37        .clone();
38    if args.len() == 1 {
39        emit_expr(emitter, ctx, arg)?;
40        emit_stringify_value(emitter, ctx, &arg_ty);
41    } else {
42        emit_expr(emitter, ctx, arg)?;
43        let arg_local = emitter.add_anonymous_local(ctx.symbols.value_type(&arg_ty)?)?;
44        emitter.instruction(Instruction::LocalSet(arg_local));
45        emit_stringify_optional_args(emitter, ctx, args, arg_local, &arg_ty)?;
46    }
47    // A value with no JSON text is `undefined` only where the typechecker
48    // widened the call. Anywhere else it throws, then the string is pinned
49    // before the slot coercion: erasure can widen that slot to `unknown`
50    // while the call still promises a string.
51    let may_be_undefined = may_stringify_undefined(ctx, &arg_ty);
52    if may_be_undefined && !source_ty.holds_undefined() {
53        emit_reject_missing_json_text(emitter, ctx)?;
54        return super::cast::emit_coerce_to_slot(emitter, ctx, &Type::String, slot_ty);
55    }
56    let emitted_ty = if may_be_undefined {
57        Type::union(vec![Type::String, Type::Undefined])
58    } else {
59        Type::String
60    };
61    super::cast::emit_coerce_to_slot(emitter, ctx, &emitted_ty, slot_ty)
62}
63
64/// With a `string | undefined` JSON text on the stack, throws when it is
65/// `undefined` and leaves the string.
66fn emit_reject_missing_json_text(
67    emitter: &mut FunctionEmitter,
68    ctx: &CodegenCtx,
69) -> Result<(), crate::compiler_error::CompilerFailure> {
70    let intrinsics = ctx
71        .symbols
72        .intrinsic_type_indices()
73        .ok_or_else(|| crate::codegen::internal_failure("intrinsics registered"))?;
74    let value = emitter.add_anonymous_local(ValType::Ref(RefType {
75        nullable: false,
76        heap_type: HeapType::Concrete(intrinsics.object),
77    }))?;
78    emitter.instruction(Instruction::LocalSet(value));
79    emitter.instruction(Instruction::LocalGet(value));
80    emitter.instruction(Instruction::RefTestNonNull(HeapType::Concrete(
81        intrinsics.undefined,
82    )));
83    emitter.emit_if(BlockType::Empty);
84    crate::codegen::throw::emit_missing_value_error_throw(emitter, ctx, NO_JSON_TEXT_MESSAGE);
85    emitter.emit_end();
86    emitter.instruction(Instruction::LocalGet(value));
87    emitter.instruction(Instruction::RefCastNonNull(HeapType::Concrete(
88        intrinsics.string,
89    )));
90    Ok(())
91}
92
93/// Serializes an on-stack value to a string or the undefined singleton when it
94/// has no JSON text.
95fn emit_stringify_value(emitter: &mut FunctionEmitter, ctx: &CodegenCtx, arg_ty: &Type) {
96    if !may_stringify_undefined(ctx, arg_ty) {
97        emit_defined_stringify_value(emitter, ctx, arg_ty);
98        return;
99    }
100    let Some(intrinsics) = ctx.require(
101        ctx.symbols.intrinsic_type_indices(),
102        "intrinsics registered",
103    ) else {
104        return;
105    };
106    let Some(value) = ctx.latch(emitter.add_anonymous_local(ValType::Ref(RefType {
107        nullable: true,
108        heap_type: HeapType::Concrete(intrinsics.object),
109    }))) else {
110        return;
111    };
112    emitter.instruction(Instruction::LocalSet(value));
113    emitter.instructions(crate::codegen::nullish::is_undefined_or_closure(
114        &[Instruction::LocalGet(value)],
115        intrinsics,
116    ));
117    emitter.emit_if(BlockType::Result(ValType::Ref(RefType {
118        nullable: false,
119        heap_type: HeapType::Concrete(intrinsics.object),
120    })));
121    if ctx
122        .latch(super::expr::emit_undefined(emitter, ctx))
123        .is_none()
124    {
125        return;
126    }
127    emitter.emit_else();
128    emitter.instruction(Instruction::LocalGet(value));
129    emit_stringify_nullable(emitter, ctx);
130    emitter.emit_end();
131}
132
133fn may_stringify_undefined(ctx: &CodegenCtx, ty: &Type) -> bool {
134    ty.any_member(&|member| {
135        Type::stringifies_to_undefined(member) || is_constructor_value(ctx, member)
136    })
137}
138
139/// A constructor binding used as a value (`Number`) is a function, which JSON
140/// leaves out. Its tag (`static_value_tag`) is the native-function text the
141/// runtime also builds the closure from, so both read the kind from it.
142fn is_constructor_value(ctx: &CodegenCtx, ty: &Type) -> bool {
143    let Type::InterfaceRef { mangled, .. } = ty else {
144        return false;
145    };
146    ctx.symbols
147        .static_value_tag(mangled)
148        .is_some_and(|tag| tag.starts_with("function "))
149}
150
151fn emit_defined_stringify_value(emitter: &mut FunctionEmitter, ctx: &CodegenCtx, arg_ty: &Type) {
152    // Peel first: the scalar arms below lower unboxed, so an alias reaching the
153    // `_` vtable arm would push an f64/i32 where a ref is expected. A union of
154    // number literals (`1 | 2`) lowers to an unboxed f64 too.
155    let peeled = match arg_ty.lowered() {
156        Type::Union(members) if is_unboxed_number_union(members) => &Type::Number,
157        peeled => peeled,
158    };
159    match peeled {
160        Type::Null => {
161            // The value is a null ref; discard it and emit the literal `null`.
162            emitter.instruction(Instruction::Drop);
163            ctx.latch(super::emit_inline_string_literal(emitter, ctx, "null"));
164        }
165        Type::Boolean | Type::BooleanLiteral(_) => emit_to_json_direct(emitter, ctx, "Boolean"),
166        Type::Number | Type::NumberLiteral(_) => {
167            emit_to_json_direct(emitter, ctx, "Number");
168        }
169        Type::String | Type::StringLiteral(_) => {
170            emit_stringify_string_host(emitter, ctx);
171        }
172        _ if may_hold_null(peeled) => {
173            // The value lowers to `(ref null $Object)`; vtable dispatch on a
174            // null receiver would trap at `struct.get $Object 0`. Branch on
175            // `ref.is_null` first.
176            emit_stringify_nullable(emitter, ctx);
177        }
178        _ => {
179            emit_vtable_dispatch_on_object_stack(emitter, ctx, 1);
180        }
181    }
182}
183
184/// Whether a union holds only numbers and number literals, which lower to an
185/// unboxed f64 like `number` itself.
186fn is_unboxed_number_union(members: &[Type]) -> bool {
187    !members.is_empty()
188        && members
189            .iter()
190            .all(|member| matches!(member.lowered(), Type::Number | Type::NumberLiteral(_)))
191}
192
193fn emit_stringify_nullable(emitter: &mut FunctionEmitter, ctx: &CodegenCtx) {
194    let Some(intrinsics) = ctx.require(
195        ctx.symbols.intrinsic_type_indices(),
196        "intrinsics declared by codegen entry",
197    ) else {
198        return;
199    };
200    let Some(obj_tmp) = ctx.latch(emitter.add_anonymous_local(ValType::Ref(RefType {
201        nullable: true,
202        heap_type: HeapType::Concrete(intrinsics.object),
203    }))) else {
204        return;
205    };
206    emitter.instruction(Instruction::LocalTee(obj_tmp));
207    emitter.instruction(Instruction::RefIsNull);
208    emitter.emit_if(BlockType::Result(ValType::Ref(RefType {
209        nullable: false,
210        heap_type: HeapType::Concrete(intrinsics.string),
211    })));
212    ctx.latch(super::emit_inline_string_literal(emitter, ctx, "null"));
213    emitter.emit_else();
214    emitter.instruction(Instruction::LocalGet(obj_tmp));
215    emitter.instruction(Instruction::RefAsNonNull);
216    emit_vtable_dispatch_on_object_stack(emitter, ctx, 1);
217    emitter.emit_end();
218}
219
220/// Serialize a `$string` to its JSON form (`"…"`, escaped) via the
221/// `submilli:json.stringify` host fn rather than the per-code-unit Wasm escape
222/// loop, which costs ~64 fuel/byte. The host fn takes/returns a `$rawString`, so
223/// unwrap the receiver's raw array, call it, and re-wrap with the string vtable.
224fn emit_stringify_string_host(emitter: &mut FunctionEmitter, ctx: &CodegenCtx) {
225    let Some(intrinsics) = ctx.require(
226        ctx.symbols.intrinsic_type_indices(),
227        "intrinsic type indices registered",
228    ) else {
229        return;
230    };
231    let Some(string_type_idx) = ctx.require(ctx.symbols.string_type_idx(), "$string registered")
232    else {
233        return;
234    };
235    emitter.instruction(Instruction::StructGet {
236        struct_type_index: string_type_idx,
237        field_index: 1,
238    });
239
240    let Some(stringify_idx) = ctx.require(
241        ctx.symbols.func_idx(&crate::mangle::host(
242            crate::runtime::JSON_MODULE_NAME,
243            "stringify",
244        )),
245        "submilli:json.stringify imported during codegen",
246    ) else {
247        return;
248    };
249    emitter.instruction(Instruction::Call(stringify_idx));
250
251    let Some(raw_local) = ctx.latch(emitter.add_anonymous_local(ValType::Ref(RefType {
252        nullable: false,
253        heap_type: HeapType::Concrete(intrinsics.raw_string),
254    }))) else {
255        return;
256    };
257    emitter.instruction(Instruction::LocalSet(raw_local));
258    let Some(string_vtable_idx) = ctx.require(
259        ctx.symbols.prelude_global_idx("string_vtable"),
260        "string_vtable imported",
261    ) else {
262        return;
263    };
264    emitter.instruction(Instruction::GlobalGet(string_vtable_idx));
265    emitter.instruction(Instruction::LocalGet(raw_local));
266    emitter.instruction(Instruction::I64Const(0));
267    emitter.instruction(Instruction::StructNew(intrinsics.string));
268}
269
270fn emit_stringify_optional_args(
271    emitter: &mut FunctionEmitter,
272    ctx: &CodegenCtx,
273    args: &[ExprId],
274    arg_local: u32,
275    arg_ty: &Type,
276) -> Result<(), crate::compiler_error::CompilerFailure> {
277    let [_, replacer, rest @ ..] = args else {
278        return Err(crate::codegen::internal_failure(
279            "JSON.stringify optional arguments are missing",
280        ));
281    };
282    emit_expr(emitter, ctx, *replacer)?;
283    emitter.instruction(Instruction::Drop);
284
285    let space = match rest {
286        [] => StringifySpace::None,
287        [space] => emit_stringify_space_arg(emitter, ctx, *space)?,
288        _ => {
289            return Err(crate::codegen::internal_failure(
290                "JSON.stringify has extra optional arguments",
291            ));
292        }
293    };
294
295    emitter.instruction(Instruction::LocalGet(arg_local));
296    emit_stringify_value(emitter, ctx, arg_ty);
297    let may_be_undefined = may_stringify_undefined(ctx, arg_ty);
298    if may_be_undefined {
299        let intr = ctx
300            .symbols
301            .intrinsic_type_indices()
302            .ok_or_else(|| crate::codegen::internal_failure("intrinsics registered"))?;
303        let result_type = ValType::Ref(RefType {
304            nullable: false,
305            heap_type: HeapType::Concrete(intr.object),
306        });
307        let result = emitter.add_anonymous_local(result_type)?;
308        emitter.instruction(Instruction::LocalTee(result));
309        emitter.instruction(Instruction::RefTestNonNull(HeapType::Concrete(
310            intr.undefined,
311        )));
312        emitter.emit_if(BlockType::Result(result_type));
313        emitter.instruction(Instruction::LocalGet(result));
314        emitter.emit_else();
315        emitter.instruction(Instruction::LocalGet(result));
316        emitter.instruction(Instruction::RefCastNonNull(HeapType::Concrete(intr.string)));
317    }
318    match space {
319        StringifySpace::None => {}
320        StringifySpace::Dynamic(local) => emit_dynamic_space(emitter, ctx, local)?,
321        StringifySpace::Number(local) => {
322            emit_string_on_stack_raw(emitter, ctx);
323            emitter.instruction(Instruction::LocalGet(local));
324            emit_pretty_number_host(emitter, ctx);
325            emit_wrap_raw_string(emitter, ctx);
326        }
327        StringifySpace::String(local) => {
328            emit_string_on_stack_raw(emitter, ctx);
329            emitter.instruction(Instruction::LocalGet(local));
330            emit_string_on_stack_raw(emitter, ctx);
331            emit_pretty_string_host(emitter, ctx);
332            emit_wrap_raw_string(emitter, ctx);
333        }
334    };
335    if may_be_undefined {
336        emitter.emit_end();
337    }
338    Ok(())
339}
340
341enum StringifySpace {
342    None,
343    Number(u32),
344    String(u32),
345    Dynamic(u32),
346}
347
348fn emit_stringify_space_arg(
349    emitter: &mut FunctionEmitter,
350    ctx: &CodegenCtx,
351    space: ExprId,
352) -> Result<StringifySpace, crate::compiler_error::CompilerFailure> {
353    let space_ty = ctx
354        .ta
355        .try_expr(space)
356        .map_err(crate::codegen::arena_failure)?
357        .ty
358        .clone();
359    Ok(match space_ty.lowered() {
360        Type::Number | Type::NumberLiteral(_) => {
361            emit_expr(emitter, ctx, space)?;
362            let local = emitter.add_anonymous_local(ValType::F64)?;
363            emitter.instruction(Instruction::LocalSet(local));
364            StringifySpace::Number(local)
365        }
366        Type::String | Type::StringLiteral(_) => {
367            emit_expr(emitter, ctx, space)?;
368            let local = emitter.add_anonymous_local(ctx.symbols.value_type(&space_ty)?)?;
369            emitter.instruction(Instruction::LocalSet(local));
370            StringifySpace::String(local)
371        }
372        Type::Null | Type::Undefined | Type::Void => {
373            emit_expr(emitter, ctx, space)?;
374            emitter.instruction(Instruction::Drop);
375            StringifySpace::None
376        }
377        Type::Unknown | Type::Union(_) => {
378            emit_expr(emitter, ctx, space)?;
379            let local = emitter.add_anonymous_local(ctx.symbols.value_type(&Type::Unknown)?)?;
380            emitter.instruction(Instruction::LocalSet(local));
381            StringifySpace::Dynamic(local)
382        }
383        other => {
384            return Err(crate::codegen::internal_failure(format!(
385                "JSON.stringify space type was not validated: {other}"
386            )));
387        }
388    })
389}
390
391fn emit_dynamic_space(
392    emitter: &mut FunctionEmitter,
393    ctx: &CodegenCtx,
394    space: u32,
395) -> Result<(), crate::compiler_error::CompilerFailure> {
396    let string_type = ctx.symbols.value_type(&Type::String)?;
397    let json = emitter.add_anonymous_local(string_type)?;
398    emitter.instruction(Instruction::LocalSet(json));
399    let number = ctx
400        .symbols
401        .boxed_number_type_idx()
402        .ok_or_else(|| crate::codegen::internal_failure("boxed number registered"))?;
403    let string = ctx
404        .symbols
405        .string_type_idx()
406        .ok_or_else(|| crate::codegen::internal_failure("string registered"))?;
407    emitter.instruction(Instruction::LocalGet(space));
408    emitter.instruction(Instruction::RefTestNonNull(HeapType::Concrete(number)));
409    emitter.emit_if(BlockType::Result(string_type));
410    emitter.instruction(Instruction::LocalGet(json));
411    emit_string_on_stack_raw(emitter, ctx);
412    emitter.instruction(Instruction::LocalGet(space));
413    super::cast::emit_cast_to(emitter, ctx, &Type::Number)?;
414    emit_pretty_number_host(emitter, ctx);
415    emit_wrap_raw_string(emitter, ctx);
416    emitter.emit_else();
417    emitter.instruction(Instruction::LocalGet(space));
418    emitter.instruction(Instruction::RefTestNonNull(HeapType::Concrete(string)));
419    emitter.emit_if(BlockType::Result(string_type));
420    emitter.instruction(Instruction::LocalGet(json));
421    emit_string_on_stack_raw(emitter, ctx);
422    emitter.instruction(Instruction::LocalGet(space));
423    super::cast::emit_cast_to(emitter, ctx, &Type::String)?;
424    emit_string_on_stack_raw(emitter, ctx);
425    emit_pretty_string_host(emitter, ctx);
426    emit_wrap_raw_string(emitter, ctx);
427    emitter.emit_else();
428    emitter.instruction(Instruction::LocalGet(json));
429    emitter.emit_end();
430    emitter.emit_end();
431
432    Ok(())
433}
434
435fn emit_string_on_stack_raw(emitter: &mut FunctionEmitter, ctx: &CodegenCtx) {
436    let Some(string_type_idx) = ctx.require(ctx.symbols.string_type_idx(), "$string registered")
437    else {
438        return;
439    };
440    emitter.instruction(Instruction::StructGet {
441        struct_type_index: string_type_idx,
442        field_index: 1,
443    });
444}
445
446fn emit_pretty_number_host(emitter: &mut FunctionEmitter, ctx: &CodegenCtx) {
447    let Some(pretty_idx) = ctx.require(
448        ctx.symbols.func_idx(&crate::mangle::host(
449            crate::runtime::JSON_MODULE_NAME,
450            "stringifyPrettyNumber",
451        )),
452        "submilli:json.stringifyPrettyNumber imported during codegen",
453    ) else {
454        return;
455    };
456    emitter.instruction(Instruction::Call(pretty_idx));
457}
458
459fn emit_pretty_string_host(emitter: &mut FunctionEmitter, ctx: &CodegenCtx) {
460    let Some(pretty_idx) = ctx.require(
461        ctx.symbols.func_idx(&crate::mangle::host(
462            crate::runtime::JSON_MODULE_NAME,
463            "stringifyPrettyString",
464        )),
465        "submilli:json.stringifyPrettyString imported during codegen",
466    ) else {
467        return;
468    };
469    emitter.instruction(Instruction::Call(pretty_idx));
470}
471
472fn emit_wrap_raw_string(emitter: &mut FunctionEmitter, ctx: &CodegenCtx) {
473    let Some(intrinsics) = ctx.require(
474        ctx.symbols.intrinsic_type_indices(),
475        "intrinsic type indices registered",
476    ) else {
477        return;
478    };
479    let Some(raw_local) = ctx.latch(emitter.add_anonymous_local(ValType::Ref(RefType {
480        nullable: false,
481        heap_type: HeapType::Concrete(intrinsics.raw_string),
482    }))) else {
483        return;
484    };
485    emitter.instruction(Instruction::LocalSet(raw_local));
486    let Some(string_vtable_idx) = ctx.require(
487        ctx.symbols.prelude_global_idx("string_vtable"),
488        "string_vtable imported",
489    ) else {
490        return;
491    };
492    emitter.instruction(Instruction::GlobalGet(string_vtable_idx));
493    emitter.instruction(Instruction::LocalGet(raw_local));
494    emitter.instruction(Instruction::I64Const(0));
495    emitter.instruction(Instruction::StructNew(intrinsics.string));
496}
497
498fn emit_to_json_direct(emitter: &mut FunctionEmitter, ctx: &CodegenCtx, iface: &str) {
499    let mangled = crate::mangle::extend(&crate::mangle::prelude(iface), "toJson");
500    let Some(func_idx) = ctx.latch(ctx.symbols.func_idx(&mangled).ok_or_else(|| {
501        crate::codegen::internal_failure(format!("{iface}#toJson imported from prelude"))
502    })) else {
503        return;
504    };
505    emitter.instruction(Instruction::Call(func_idx));
506}
507
508/// Body of the exported `__main_output` shim: call `main`, then encode its result
509/// to the nullable string the runtime consumes as program output. Undefined
510/// uses the private ABI's null reference; language null produces the text
511/// `"null"`. The host reads concrete strings verbatim.
512pub(crate) fn emit_main_output_shim(
513    ctx: &CodegenCtx,
514    main_func_idx: u32,
515    return_ty: &Type,
516    source_return_ty: &Type,
517) -> Result<Function, crate::compiler_error::CompilerFailure> {
518    let mut emitter = FunctionEmitter::new(ctx, &[])?;
519    emitter.instruction(Instruction::Call(main_func_idx));
520    let optional_output = may_stringify_undefined(ctx, return_ty);
521    if optional_output {
522        let intr = ctx
523            .symbols
524            .intrinsic_type_indices()
525            .ok_or_else(|| crate::codegen::internal_failure("intrinsics registered"))?;
526        let value = emitter.add_anonymous_local(ValType::Ref(RefType {
527            nullable: true,
528            heap_type: HeapType::Concrete(intr.object),
529        }))?;
530        emitter.instruction(Instruction::LocalSet(value));
531        emitter.instructions(crate::codegen::nullish::is_undefined_or_closure(
532            &[Instruction::LocalGet(value)],
533            intr,
534        ));
535        emitter.emit_if(BlockType::Result(ValType::Ref(RefType {
536            nullable: true,
537            heap_type: HeapType::Concrete(intr.string),
538        })));
539        emitter.instruction(Instruction::RefNull(HeapType::Concrete(intr.string)));
540        emitter.emit_else();
541        emitter.instruction(Instruction::LocalGet(value));
542    }
543    let scalar_output = match source_return_ty.lowered() {
544        Type::String
545        | Type::StringLiteral(_)
546        | Type::Number
547        | Type::NumberLiteral(_)
548        | Type::Boolean
549        | Type::BooleanLiteral(_) => true,
550        Type::Union(members) => is_primitive_union(members),
551        _ => false,
552    };
553    if return_ty == &Type::Unknown && scalar_output {
554        emit_nullable_primitive_to_string(&mut emitter, ctx);
555    } else {
556        emit_main_output_value(&mut emitter, ctx, return_ty.lowered());
557    }
558    if optional_output {
559        emitter.emit_end();
560    }
561    emitter.build()
562}
563
564/// Encodes `main`'s on-stack return value (`arg_ty`, already peeled) into the output
565/// `(ref $string)`. Scalars render via `toString` — a `string` is its own output, so
566/// it passes through verbatim and a large string return never pays a JSON-escape pass.
567/// Everything else (objects, arrays, tuples, interfaces, unions, `null`) routes through
568/// the shared JSON encoder, so structured returns match `JSON.stringify`.
569fn emit_main_output_value(emitter: &mut FunctionEmitter, ctx: &CodegenCtx, arg_ty: &Type) {
570    match arg_ty {
571        Type::String | Type::StringLiteral(_) => {}
572        Type::Number | Type::NumberLiteral(_) => emit_number_to_string_radix10(emitter, ctx),
573        Type::Boolean | Type::BooleanLiteral(_) => emit_to_string_direct(emitter, ctx, "Boolean"),
574        // A union of primitives (`string | number`, `string | undefined`, …)
575        // follows the primitive rule, not JSON: whichever value it holds at run
576        // time prints as that primitive would, so a string flows through verbatim.
577        Type::Union(members) if is_primitive_union(members) => {
578            emit_nullable_primitive_to_string(emitter, ctx);
579        }
580        Type::Undefined
581        | Type::Void
582        | Type::Unknown
583        | Type::TypeVar(_)
584        | Type::GenericParam { .. } => emit_stringify_nullable(emitter, ctx),
585        _ => emit_defined_stringify_value(emitter, ctx, arg_ty),
586    }
587}
588
589/// `true` for a union of bare primitives (`string`/`number`/`boolean` or their
590/// literals), optionally with `null` or `undefined`. Objects and arrays fall
591/// through to JSON.
592fn is_primitive_union(members: &[Type]) -> bool {
593    let mut has_primitive = false;
594    for member in members {
595        match member.lowered() {
596            Type::Null | Type::Undefined => {}
597            Type::String
598            | Type::StringLiteral(_)
599            | Type::Number
600            | Type::NumberLiteral(_)
601            | Type::Boolean
602            | Type::BooleanLiteral(_) => has_primitive = true,
603            _ => return false,
604        }
605    }
606    has_primitive
607}
608
609/// Renders a `(ref null $Object)`-lowered nullable primitive: the literal `null`
610/// when null, else the boxed value's vtable `toString` (slot 0) — identity for a
611/// string (verbatim), canonical `toString` for a boxed number/boolean.
612fn emit_nullable_primitive_to_string(emitter: &mut FunctionEmitter, ctx: &CodegenCtx) {
613    let Some(intrinsics) = ctx.require(
614        ctx.symbols.intrinsic_type_indices(),
615        "intrinsics declared by codegen entry",
616    ) else {
617        return;
618    };
619    let Some(obj_tmp) = ctx.latch(emitter.add_anonymous_local(ValType::Ref(RefType {
620        nullable: true,
621        heap_type: HeapType::Concrete(intrinsics.object),
622    }))) else {
623        return;
624    };
625    emitter.instruction(Instruction::LocalTee(obj_tmp));
626    emitter.instruction(Instruction::RefIsNull);
627    emitter.emit_if(BlockType::Result(ValType::Ref(RefType {
628        nullable: false,
629        heap_type: HeapType::Concrete(intrinsics.string),
630    })));
631    ctx.latch(super::emit_inline_string_literal(emitter, ctx, "null"));
632    emitter.emit_else();
633    emitter.instruction(Instruction::LocalGet(obj_tmp));
634    emitter.instruction(Instruction::RefAsNonNull);
635    emit_vtable_dispatch_on_object_stack(emitter, ctx, 0);
636    emitter.emit_end();
637}
638
639/// Calls `Number#toString` with the default radix 10 — the same import a no-arg
640/// `(n).toString()` lowers to, so NaN/Infinity render as `"NaN"`/`"Infinity"`
641/// (where JSON would emit `null`).
642fn emit_number_to_string_radix10(emitter: &mut FunctionEmitter, ctx: &CodegenCtx) {
643    emitter.instruction(Instruction::F64Const(10.0_f64.into()));
644    emit_to_string_direct(emitter, ctx, "Number");
645}
646
647fn emit_to_string_direct(emitter: &mut FunctionEmitter, ctx: &CodegenCtx, iface: &str) {
648    let mangled = crate::mangle::extend(&crate::mangle::prelude(iface), "toString");
649    let Some(func_idx) = ctx.latch(ctx.symbols.func_idx(&mangled).ok_or_else(|| {
650        crate::codegen::internal_failure(format!("{iface}#toString imported from prelude"))
651    })) else {
652        return;
653    };
654    emitter.instruction(Instruction::Call(func_idx));
655}
656
657pub(super) fn emit_parse(
658    emitter: &mut FunctionEmitter,
659    ctx: &CodegenCtx,
660    args: &[ExprId],
661) -> Result<(), crate::compiler_error::CompilerFailure> {
662    let [arg] = args else {
663        return Err(crate::codegen::internal_failure(
664            "JSON.parse requires exactly one argument",
665        ));
666    };
667
668    // Emit arg (the source `$string`), then extract field 1 — the host fn
669    // signature takes `(ref $rawString)`, not the wrapped struct.
670    emit_expr(emitter, ctx, *arg)?;
671    super::cast::emit_coerce_to_slot(
672        emitter,
673        ctx,
674        &ctx.ta
675            .try_expr(*arg)
676            .map_err(crate::codegen::arena_failure)?
677            .ty,
678        &Type::String,
679    )?;
680    let string_type_idx = ctx
681        .symbols
682        .string_type_idx()
683        .ok_or_else(|| crate::codegen::internal_failure("$string registered"))?;
684    emitter.instruction(Instruction::StructGet {
685        struct_type_index: string_type_idx,
686        field_index: 1,
687    });
688
689    let parse_idx = ctx
690        .symbols
691        .func_idx(&crate::mangle::host(
692            crate::runtime::JSON_MODULE_NAME,
693            "parse",
694        ))
695        .ok_or_else(|| {
696            crate::codegen::internal_failure("submilli:json.parse imported during codegen")
697        })?;
698    // On invalid JSON the host fn raises a catchable Error directly. Valid JSON
699    // returns the language's `unknown` representation: `(ref null $Object)`.
700    emitter.instruction(Instruction::Call(parse_idx));
701    Ok(())
702}
703
704/// Builds a constant `(ref $rawString)` inline via `array.new_fixed` — the packed
705/// UTF-16 data with no `$string` wrapper and no string-pool entry.
706pub(crate) fn emit_inline_const_raw_string(
707    emitter: &mut FunctionEmitter,
708    ctx: &CodegenCtx,
709    text: &str,
710) -> Result<(), crate::compiler_error::CompilerFailure> {
711    let intrinsics = ctx.symbols.intrinsic_type_indices().ok_or_else(|| {
712        crate::codegen::internal_failure("inline string intrinsic types are missing")
713    })?;
714    let units = crate::literal_units::literal_units(text);
715    let array_size = u32::try_from(units.len()).map_err(|_| {
716        crate::codegen::internal_failure("inline string exceeds the array length representation")
717    })?;
718    for unit in units {
719        emitter.instruction(Instruction::I32Const(i32::from(unit)));
720    }
721    emitter.instruction(Instruction::ArrayNewFixed {
722        array_type_index: intrinsics.raw_string,
723        array_size,
724    });
725    Ok(())
726}
727
728pub(crate) fn emit_raw_string_matches_literal(
729    emitter: &mut FunctionEmitter,
730    intrinsics: IntrinsicTypeIndices,
731    key_raw_local: u32,
732    expected: &str,
733) {
734    let expected = crate::literal_units::literal_units(expected);
735    let Some(expected_length) = emitter.ctx.latch(crate::codegen::wasm_u32(expected.len())) else {
736        return;
737    };
738    let Some(result_local) = emitter.ctx.latch(emitter.add_anonymous_local(ValType::I32)) else {
739        return;
740    };
741    emitter.instruction(Instruction::I32Const(0));
742    emitter.instruction(Instruction::LocalSet(result_local));
743
744    emitter.emit_block(BlockType::Empty);
745
746    emitter.instruction(Instruction::LocalGet(key_raw_local));
747    emitter.instruction(Instruction::ArrayLen);
748    emitter.instruction(Instruction::I32Const(expected_length as i32));
749    emitter.instruction(Instruction::I32Ne);
750    emitter.instruction(Instruction::BrIf(0));
751
752    for (&unit, i) in expected.iter().zip(0..expected_length) {
753        emitter.instruction(Instruction::LocalGet(key_raw_local));
754        emitter.instruction(Instruction::I32Const(i as i32));
755        emitter.instruction(Instruction::ArrayGetU(intrinsics.raw_string));
756        emitter.instruction(Instruction::I32Const(i32::from(unit)));
757        emitter.instruction(Instruction::I32Ne);
758        emitter.instruction(Instruction::BrIf(0));
759    }
760
761    emitter.instruction(Instruction::I32Const(1));
762    emitter.instruction(Instruction::LocalSet(result_local));
763
764    emitter.emit_end();
765
766    emitter.instruction(Instruction::LocalGet(result_local));
767}
768
769#[cfg(test)]
770mod tests {
771    use super::*;
772    use crate::codegen::invariant_tests::{assert_internal, with_context};
773    use crate::codegen::{SymbolTable, tests::mock_symbols_with_intrinsics};
774    use crate::{Span, TypedAst, TypedExpr, TypedExprKind};
775
776    #[test]
777    fn invalid_json_arity_is_an_internal_failure_before_reading_arguments() {
778        with_context(&TypedAst::new(), &SymbolTable::default(), |ctx| {
779            let mut emitter = FunctionEmitter::new(ctx, &[]).unwrap();
780            assert_internal(
781                emit_stringify(&mut emitter, ctx, &[], &Type::String, &Type::String).unwrap_err(),
782            );
783            assert_internal(emit_parse(&mut emitter, ctx, &[]).unwrap_err());
784            let invalid = crate::ExprId(u32::MAX);
785            assert_internal(
786                emit_stringify(
787                    &mut emitter,
788                    ctx,
789                    &[invalid; 4],
790                    &Type::String,
791                    &Type::String,
792                )
793                .unwrap_err(),
794            );
795            assert_internal(emit_parse(&mut emitter, ctx, &[invalid; 2]).unwrap_err());
796            ctx.check_failure().unwrap();
797        });
798    }
799
800    #[test]
801    fn json_emitters_latch_missing_registrations() {
802        type Emit = fn(&mut FunctionEmitter<'_>, &CodegenCtx<'_>);
803        let emitters: &[Emit] = &[
804            emit_stringify_nullable,
805            emit_stringify_string_host,
806            emit_string_on_stack_raw,
807            emit_pretty_number_host,
808            emit_pretty_string_host,
809            emit_wrap_raw_string,
810            emit_nullable_primitive_to_string,
811            |emitter, ctx| emit_to_json_direct(emitter, ctx, "Number"),
812            |emitter, ctx| emit_to_string_direct(emitter, ctx, "Boolean"),
813            |emitter, ctx| {
814                ctx.latch(emit_inline_const_raw_string(emitter, ctx, "\u{1f642}"));
815            },
816        ];
817        for emit in emitters {
818            with_context(&TypedAst::new(), &SymbolTable::default(), |ctx| {
819                emit(&mut FunctionEmitter::new(ctx, &[]).unwrap(), ctx);
820                assert_internal(ctx.check_failure().unwrap_err());
821            });
822        }
823        for emit in [emit_stringify_string_host as Emit, emit_wrap_raw_string] {
824            with_context(&TypedAst::new(), &mock_symbols_with_intrinsics(), |ctx| {
825                emit(&mut FunctionEmitter::new(ctx, &[]).unwrap(), ctx);
826                assert_internal(ctx.check_failure().unwrap_err());
827            });
828        }
829    }
830
831    #[test]
832    fn invalid_space_metadata_returns_internal_failure() {
833        for ty in [Type::Boolean, Type::Error] {
834            let mut ta = TypedAst::new();
835            let space = ta
836                .try_push_expr(TypedExpr {
837                    kind: TypedExprKind::Boolean(true),
838                    ty,
839                    span: Span::at(crate::FileId(0)),
840                })
841                .unwrap();
842            with_context(&ta, &mock_symbols_with_intrinsics(), |ctx| {
843                let mut emitter = FunctionEmitter::new(ctx, &[]).unwrap();
844                match emit_stringify_space_arg(&mut emitter, ctx, space) {
845                    Err(error) => assert_internal(error),
846                    Ok(_) => panic!("invalid space metadata was accepted"),
847                }
848            });
849        }
850    }
851
852    #[test]
853    fn inline_raw_literals_keep_utf16_units() {
854        with_context(&TypedAst::new(), &mock_symbols_with_intrinsics(), |ctx| {
855            let mut emitter = FunctionEmitter::new(ctx, &[]).unwrap();
856            emit_inline_const_raw_string(&mut emitter, ctx, "a\u{1f642}").unwrap();
857            ctx.check_failure().unwrap();
858            let units: Vec<_> = emitter
859                .instructions
860                .iter()
861                .filter_map(|inst| match inst {
862                    Instruction::I32Const(unit) => Some(*unit),
863                    _ => None,
864                })
865                .collect();
866            assert_eq!(units, vec![97, 0xd83d, 0xde42]);
867            assert!(matches!(
868                emitter.instructions.last(),
869                Some(Instruction::ArrayNewFixed { array_size: 3, .. })
870            ));
871        });
872    }
873}