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