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