Skip to main content

submilli_engine/runtime/prelude/temporal/
shared.rs

1//! Shared Temporal ABI and object construction; the per-type method surfaces
2//! live in the sibling type modules.
3
4use crate::runtime::host::{abi_arg, abi_result};
5use std::collections::BTreeMap;
6
7use jiff::{
8    RoundMode, Span, SpanRelativeTo, SpanRound, Timestamp, TimestampDifference, Unit, Zoned,
9    ZonedDifference, civil, tz::TimeZone,
10};
11use wasmtime::{
12    Caller, FieldType, Func, FuncType, Global, GlobalType, HeapType, Linker, Mutability, RefType,
13    Rooted, StorageType, Store, StructRef, StructRefPre, StructType, Val, ValType,
14};
15
16use crate::runtime::StoreData;
17use crate::runtime::fuel::host_func_async;
18use crate::runtime::host::{read_string_arg, register_host_fn, write_submilli_string_struct};
19use crate::runtime::intrinsic_types::IntrinsicTypes;
20use crate::runtime::prelude::undefined::is_undefined;
21use crate::{
22    MangledName, NamespaceSymbol, ObjectField, Package, PackageDeclaration, Param,
23    Span as DiagSpan, Type, ValueKind, ValueSymbol,
24};
25
26pub const TEMPORAL_MODULE_NAME: &str = "submilli:temporal";
27const I64_BOUND_EXCLUSIVE: f64 = 9_223_372_036_854_775_808.0;
28const PLAIN_MONTH_DAY_MARKER: i64 = 0x4d4f_4e54_4844_4159;
29
30#[derive(Clone)]
31pub(crate) struct TemporalAbi {
32    pub(crate) instant: StructType,
33    pub(crate) duration: StructType,
34    pub(crate) zoned_date_time: StructType,
35    pub(crate) plain_date: StructType,
36    pub(crate) plain_time: StructType,
37    pub(crate) plain_date_time: StructType,
38    pub(crate) plain_year_month: StructType,
39    pub(crate) plain_month_day: StructType,
40    pub(crate) instant_vtable: Global,
41    pub(crate) duration_vtable: Global,
42    pub(crate) zoned_date_time_vtable: Global,
43    pub(crate) plain_date_vtable: Global,
44    pub(crate) plain_time_vtable: Global,
45    pub(crate) plain_date_time_vtable: Global,
46    pub(crate) plain_year_month_vtable: Global,
47    pub(crate) plain_month_day_vtable: Global,
48}
49
50pub(crate) fn install_abi(
51    linker: &mut Linker<StoreData>,
52    store: &mut Store<StoreData>,
53    intr: &IntrinsicTypes,
54    module: &str,
55) -> wasmtime::Result<TemporalAbi> {
56    use crate::runtime::gc_singleton::singleton_struct;
57    use wasmtime::Finality;
58
59    let imm = Mutability::Const;
60    let vtable_field = FieldType::new(
61        imm,
62        StorageType::ValType(ValType::Ref(RefType::new(
63            false,
64            intr.vtable.clone().into(),
65        ))),
66    );
67    let i32_field = FieldType::new(imm, StorageType::ValType(ValType::I32));
68    let i64_field = FieldType::new(imm, StorageType::ValType(ValType::I64));
69
70    let plain_date = singleton_struct(
71        store.engine(),
72        Finality::NonFinal,
73        Some(intr.object.clone()),
74        vec![
75            vtable_field.clone(),
76            i32_field.clone(),
77            i32_field.clone(),
78            i32_field.clone(),
79        ],
80    )?;
81    let plain_time = singleton_struct(
82        store.engine(),
83        Finality::NonFinal,
84        Some(intr.object.clone()),
85        vec![
86            vtable_field.clone(),
87            i32_field.clone(),
88            i32_field.clone(),
89            i32_field.clone(),
90            i32_field.clone(),
91        ],
92    )?;
93    let plain_date_time = singleton_struct(
94        store.engine(),
95        Finality::NonFinal,
96        Some(intr.object.clone()),
97        vec![
98            vtable_field.clone(),
99            i32_field.clone(),
100            i32_field.clone(),
101            i32_field.clone(),
102            i32_field.clone(),
103            i32_field.clone(),
104            i32_field.clone(),
105            i32_field.clone(),
106        ],
107    )?;
108    let plain_year_month = singleton_struct(
109        store.engine(),
110        Finality::NonFinal,
111        Some(intr.object.clone()),
112        vec![vtable_field.clone(), i32_field.clone(), i32_field.clone()],
113    )?;
114    let plain_month_day = singleton_struct(
115        store.engine(),
116        Finality::NonFinal,
117        Some(intr.object.clone()),
118        vec![vtable_field, i32_field.clone(), i32_field, i64_field],
119    )?;
120
121    let instant_slots = plain_vtable_slots(
122        store,
123        intr,
124        intr.temporal_instant.clone(),
125        instant_to_string_host,
126        instant_equals_host,
127    )?;
128    let instant_vtable = define_plain_vtable(
129        linker,
130        store,
131        intr,
132        module,
133        "temporal_instant_host_vtable",
134        instant_slots,
135    )?;
136    let duration_slots = plain_vtable_slots(
137        store,
138        intr,
139        intr.temporal_duration.clone(),
140        duration_to_string_host,
141        duration_equals_host,
142    )?;
143    let duration_vtable = define_plain_vtable(
144        linker,
145        store,
146        intr,
147        module,
148        "temporal_duration_host_vtable",
149        duration_slots,
150    )?;
151    let zdt_slots = plain_vtable_slots(
152        store,
153        intr,
154        intr.temporal_zdt.clone(),
155        zoned_date_time_to_string_host,
156        zoned_date_time_equals_host,
157    )?;
158    let zoned_date_time_vtable = define_plain_vtable(
159        linker,
160        store,
161        intr,
162        module,
163        "temporal_zoned_date_time_host_vtable",
164        zdt_slots,
165    )?;
166    let plain_date_slots = plain_vtable_slots(
167        store,
168        intr,
169        plain_date.clone(),
170        plain_to_string_date,
171        plain_equals_date,
172    )?;
173    let plain_date_vtable = define_plain_vtable(
174        linker,
175        store,
176        intr,
177        module,
178        "temporal_plain_date_vtable",
179        plain_date_slots,
180    )?;
181    let plain_time_slots = plain_vtable_slots(
182        store,
183        intr,
184        plain_time.clone(),
185        plain_to_string_time,
186        plain_equals_time,
187    )?;
188    let plain_time_vtable = define_plain_vtable(
189        linker,
190        store,
191        intr,
192        module,
193        "temporal_plain_time_vtable",
194        plain_time_slots,
195    )?;
196    let plain_date_time_slots = plain_vtable_slots(
197        store,
198        intr,
199        plain_date_time.clone(),
200        plain_to_string_date_time,
201        plain_equals_date_time,
202    )?;
203    let plain_date_time_vtable = define_plain_vtable(
204        linker,
205        store,
206        intr,
207        module,
208        "temporal_plain_date_time_vtable",
209        plain_date_time_slots,
210    )?;
211    let plain_year_month_slots = plain_vtable_slots(
212        store,
213        intr,
214        plain_year_month.clone(),
215        plain_to_string_year_month,
216        plain_equals_year_month,
217    )?;
218    let plain_year_month_vtable = define_plain_vtable(
219        linker,
220        store,
221        intr,
222        module,
223        "temporal_plain_year_month_vtable",
224        plain_year_month_slots,
225    )?;
226    let plain_month_day_slots = plain_vtable_slots(
227        store,
228        intr,
229        plain_month_day.clone(),
230        plain_to_string_month_day,
231        plain_equals_month_day,
232    )?;
233    let plain_month_day_vtable = define_plain_vtable(
234        linker,
235        store,
236        intr,
237        module,
238        "temporal_plain_month_day_vtable",
239        plain_month_day_slots,
240    )?;
241
242    Ok(TemporalAbi {
243        instant: intr.temporal_instant.clone(),
244        duration: intr.temporal_duration.clone(),
245        zoned_date_time: intr.temporal_zdt.clone(),
246        plain_date,
247        plain_time,
248        plain_date_time,
249        plain_year_month,
250        plain_month_day,
251        instant_vtable,
252        duration_vtable,
253        zoned_date_time_vtable,
254        plain_date_vtable,
255        plain_time_vtable,
256        plain_date_time_vtable,
257        plain_year_month_vtable,
258        plain_month_day_vtable,
259    })
260}
261
262fn define_plain_vtable(
263    linker: &mut Linker<StoreData>,
264    store: &mut Store<StoreData>,
265    intr: &IntrinsicTypes,
266    module: &str,
267    name: &str,
268    slots: [Func; 4],
269) -> wasmtime::Result<Global> {
270    let pre = StructRefPre::new(&mut *store, intr.vtable.clone());
271    let vtable = StructRef::new(
272        &mut *store,
273        &pre,
274        &[
275            Val::FuncRef(Some(slots[0])),
276            Val::FuncRef(Some(slots[1])),
277            Val::FuncRef(Some(slots[2])),
278            Val::FuncRef(Some(slots[3])),
279        ],
280    )?;
281    let gty = GlobalType::new(
282        ValType::Ref(RefType::new(
283            false,
284            HeapType::ConcreteStruct(intr.vtable.clone()),
285        )),
286        Mutability::Const,
287    );
288    let global = Global::new(&mut *store, gty, Val::AnyRef(Some(vtable.to_anyref())))?;
289    let field = crate::mangle::prelude(name);
290    linker.define(&mut *store, module, field.as_str(), global)?;
291    Ok(global)
292}
293
294pub(super) type PlainStringer =
295    fn(&mut Caller<'_, StoreData>, Rooted<StructRef>) -> wasmtime::Result<String>;
296pub(super) type PlainEquals =
297    fn(&mut Caller<'_, StoreData>, Rooted<StructRef>, Rooted<StructRef>) -> wasmtime::Result<bool>;
298
299fn plain_vtable_slots(
300    store: &mut Store<StoreData>,
301    intr: &IntrinsicTypes,
302    ty: StructType,
303    stringer: PlainStringer,
304    equals_op: PlainEquals,
305) -> wasmtime::Result<[Func; 4]> {
306    let to_string_ty = intr.to_string_fn.clone();
307    let to_json_ty = intr.to_json_fn.clone();
308    let equals_ty = intr.equals_fn.clone();
309    let hash_ty = intr.hash_fn.clone();
310
311    let st_ty = ty.clone();
312    let to_string = host_func_async(
313        &mut *store,
314        to_string_ty,
315        move |mut caller, params, results| {
316            let st_ty = st_ty.clone();
317            Box::new(async move {
318                let st = cast_struct(
319                    &mut caller,
320                    abi_arg(params, 0)?,
321                    &st_ty,
322                    "Temporal.Plain.toString",
323                )?;
324                let out = stringer(&mut caller, st)?;
325                let s = write_submilli_string_struct(&mut caller, &out)?;
326                *abi_result(results, 0)? = Val::AnyRef(Some(s.to_anyref()));
327                Ok(())
328            })
329        },
330    );
331
332    let st_ty = ty.clone();
333    let to_json = host_func_async(
334        &mut *store,
335        to_json_ty,
336        move |mut caller, params, results| {
337            let st_ty = st_ty.clone();
338            Box::new(async move {
339                let st = cast_struct(
340                    &mut caller,
341                    abi_arg(params, 0)?,
342                    &st_ty,
343                    "Temporal.Plain.toJSON",
344                )?;
345                let out = stringer(&mut caller, st)?;
346                let json = serde_json::to_string(&out).map_err(|_| {
347                    wasmtime::Error::msg(
348                        "Temporal.Plain.toJSON: could not encode the ISO value as JSON; use a value within the supported date and time range",
349                    )
350                })?;
351                let s = write_submilli_string_struct(&mut caller, &json)?;
352                *abi_result(results, 0)? = Val::AnyRef(Some(s.to_anyref()));
353                Ok(())
354            })
355        },
356    );
357
358    let st_ty = ty.clone();
359    let equals = host_func_async(
360        &mut *store,
361        equals_ty,
362        move |mut caller, params, results| {
363            let st_ty = st_ty.clone();
364            Box::new(async move {
365                let a = cast_struct(
366                    &mut caller,
367                    abi_arg(params, 0)?,
368                    &st_ty,
369                    "Temporal.Plain.equals",
370                )?;
371                let Some(b) = try_cast_struct(&mut caller, abi_arg(params, 1)?, &st_ty)? else {
372                    *abi_result(results, 0)? = Val::I32(0);
373                    return Ok(());
374                };
375                // WasmGC types are structural. The vtable distinguishes nominal
376                // Temporal kinds even when their backing layouts coincide.
377                let (Val::AnyRef(Some(a_vtable)), Val::AnyRef(Some(b_vtable))) =
378                    (a.field(&mut caller, 0)?, b.field(&mut caller, 0)?)
379                else {
380                    return Err(crate::runtime::host::fatal_host_error(
381                        "Temporal.equals: invalid vtable",
382                    ));
383                };
384                *abi_result(results, 0)? = Val::I32(
385                    (Rooted::ref_eq(&caller, &a_vtable, &b_vtable)?
386                        && equals_op(&mut caller, a, b)?) as i32,
387                );
388                Ok(())
389            })
390        },
391    );
392
393    let hash = host_func_async(&mut *store, hash_ty, move |mut caller, params, results| {
394        let ty = ty.clone();
395        Box::new(async move {
396            let value = cast_struct(&mut caller, abi_arg(params, 0)?, &ty, "Temporal.hash")?;
397            let fields = ty.fields().count();
398            crate::runtime::fuel::charge(
399                &mut caller,
400                crate::runtime::fuel::ELEM,
401                fields.saturating_sub(1) as u64,
402            )?;
403            let mut hash = 0_u32;
404            for field in 1..fields {
405                let bits = match value.field(&mut caller, field)? {
406                    Val::I32(n) => n as u64,
407                    Val::I64(n) => n as u64,
408                    // Zone aliases may compare equal. The instant alone is a
409                    // valid hash for zoned values, independent of zone spelling.
410                    Val::AnyRef(_) => continue,
411                    // The resolved ZonedDateTime attachment is not semantic data.
412                    Val::ExternRef(_) if fields == 5 && field == 4 => continue,
413                    _ => {
414                        return Err(crate::runtime::host::fatal_host_error(
415                            "Temporal: invalid hash field",
416                        ));
417                    }
418                };
419                hash = hash.rotate_left(5) ^ super::super::vtable::mix_hash_bits(bits);
420            }
421            *abi_result(results, 0)? = Val::I32(hash as i32);
422            Ok(())
423        })
424    });
425
426    Ok([to_string, to_json, equals, hash])
427}
428
429pub(super) fn as_struct_val(
430    caller: &mut Caller<'_, StoreData>,
431    val: &Val,
432    label: &str,
433) -> wasmtime::Result<Rooted<StructRef>> {
434    match val {
435        Val::AnyRef(Some(any)) => any
436            .as_struct(&mut *caller)?
437            .ok_or_else(|| wasmtime::Error::msg(format!("{label}: expected object"))),
438        other => Err(wasmtime::Error::msg(format!(
439            "{label}: expected object, got {other:?}"
440        ))),
441    }
442}
443
444fn cast_struct(
445    caller: &mut Caller<'_, StoreData>,
446    val: &Val,
447    _ty: &StructType,
448    label: &str,
449) -> wasmtime::Result<Rooted<StructRef>> {
450    as_struct_val(caller, val, label)
451}
452
453fn try_cast_struct(
454    caller: &mut Caller<'_, StoreData>,
455    val: &Val,
456    ty: &StructType,
457) -> wasmtime::Result<Option<Rooted<StructRef>>> {
458    let Val::AnyRef(Some(any)) = val else {
459        return Ok(None);
460    };
461    let Some(value) = any.as_struct(&mut *caller)? else {
462        return Ok(None);
463    };
464    Ok(value.matches_ty(&*caller, ty)?.then_some(value))
465}
466
467pub(super) fn st_i32(
468    caller: &mut Caller<'_, StoreData>,
469    st: Rooted<StructRef>,
470    field: u32,
471    label: &str,
472) -> wasmtime::Result<i32> {
473    match st.field(&mut *caller, field as usize)? {
474        Val::I32(v) => Ok(v),
475        other => Err(wasmtime::Error::msg(format!(
476            "{label}: field {field} is {other:?}"
477        ))),
478    }
479}
480
481pub(super) fn plain_to_string_date(
482    caller: &mut Caller<'_, StoreData>,
483    st: Rooted<StructRef>,
484) -> wasmtime::Result<String> {
485    let y = st_i32(caller, st, 1, "PlainDate")?;
486    let m = st_i32(caller, st, 2, "PlainDate")?;
487    let d = st_i32(caller, st, 3, "PlainDate")?;
488    let date = y_m_d_date(
489        i64::from(y),
490        i64::from(m),
491        i64::from(d),
492        "PlainDate.toString",
493    )?;
494    Ok(super::plain_date::to_string(date))
495}
496
497pub(super) fn plain_to_string_time(
498    caller: &mut Caller<'_, StoreData>,
499    st: Rooted<StructRef>,
500) -> wasmtime::Result<String> {
501    let h = st_i32(caller, st, 1, "PlainTime")?;
502    let m = st_i32(caller, st, 2, "PlainTime")?;
503    let s = st_i32(caller, st, 3, "PlainTime")?;
504    let ns = st_i32(caller, st, 4, "PlainTime")?;
505    let t = h_m_s_ns_time(
506        i64::from(h),
507        i64::from(m),
508        i64::from(s),
509        i64::from(ns),
510        "PlainTime.toString",
511    )?;
512    Ok(super::plain_time::to_string(t))
513}
514
515pub(super) fn plain_to_string_date_time(
516    caller: &mut Caller<'_, StoreData>,
517    st: Rooted<StructRef>,
518) -> wasmtime::Result<String> {
519    let date = y_m_d_date(
520        i64::from(st_i32(caller, st, 1, "PlainDateTime")?),
521        i64::from(st_i32(caller, st, 2, "PlainDateTime")?),
522        i64::from(st_i32(caller, st, 3, "PlainDateTime")?),
523        "PlainDateTime.toString",
524    )?;
525    let time = h_m_s_ns_time(
526        i64::from(st_i32(caller, st, 4, "PlainDateTime")?),
527        i64::from(st_i32(caller, st, 5, "PlainDateTime")?),
528        i64::from(st_i32(caller, st, 6, "PlainDateTime")?),
529        i64::from(st_i32(caller, st, 7, "PlainDateTime")?),
530        "PlainDateTime.toString",
531    )?;
532    Ok(super::plain_date_time::to_string(date.to_datetime(time)))
533}
534
535pub(super) fn plain_to_string_year_month(
536    caller: &mut Caller<'_, StoreData>,
537    st: Rooted<StructRef>,
538) -> wasmtime::Result<String> {
539    Ok(super::plain_year_month::to_string(
540        st_i32(caller, st, 1, "PlainYearMonth")?,
541        st_i32(caller, st, 2, "PlainYearMonth")?,
542    ))
543}
544
545pub(super) fn plain_to_string_month_day(
546    caller: &mut Caller<'_, StoreData>,
547    st: Rooted<StructRef>,
548) -> wasmtime::Result<String> {
549    Ok(super::plain_month_day::to_string(
550        st_i32(caller, st, 1, "PlainMonthDay")?,
551        st_i32(caller, st, 2, "PlainMonthDay")?,
552    ))
553}
554
555fn fields_equal(
556    caller: &mut Caller<'_, StoreData>,
557    a: Rooted<StructRef>,
558    b: Rooted<StructRef>,
559    fields: &[u32],
560    label: &str,
561) -> wasmtime::Result<bool> {
562    for &field in fields {
563        if st_i32(caller, a, field, label)? != st_i32(caller, b, field, label)? {
564            return Ok(false);
565        }
566    }
567    Ok(true)
568}
569
570pub(super) fn plain_equals_date(
571    caller: &mut Caller<'_, StoreData>,
572    a: Rooted<StructRef>,
573    b: Rooted<StructRef>,
574) -> wasmtime::Result<bool> {
575    Ok(super::plain_date::equals(
576        plain_date_from_struct(caller, a, "PlainDate.equals")?,
577        plain_date_from_struct(caller, b, "PlainDate.equals")?,
578    ))
579}
580
581pub(super) fn plain_equals_time(
582    caller: &mut Caller<'_, StoreData>,
583    a: Rooted<StructRef>,
584    b: Rooted<StructRef>,
585) -> wasmtime::Result<bool> {
586    Ok(super::plain_time::equals(
587        plain_time_from_struct(caller, a, 0, "PlainTime.equals")?,
588        plain_time_from_struct(caller, b, 0, "PlainTime.equals")?,
589    ))
590}
591
592pub(super) fn plain_equals_date_time(
593    caller: &mut Caller<'_, StoreData>,
594    a: Rooted<StructRef>,
595    b: Rooted<StructRef>,
596) -> wasmtime::Result<bool> {
597    let a = plain_date_from_struct(caller, a, "PlainDateTime.equals")?.to_datetime(
598        plain_time_from_struct(caller, a, 3, "PlainDateTime.equals")?,
599    );
600    let b = plain_date_from_struct(caller, b, "PlainDateTime.equals")?.to_datetime(
601        plain_time_from_struct(caller, b, 3, "PlainDateTime.equals")?,
602    );
603    Ok(super::plain_date_time::equals(a, b))
604}
605
606pub(super) fn plain_equals_year_month(
607    caller: &mut Caller<'_, StoreData>,
608    a: Rooted<StructRef>,
609    b: Rooted<StructRef>,
610) -> wasmtime::Result<bool> {
611    Ok(super::plain_year_month::equals(
612        (
613            st_i32(caller, a, 1, "PlainYearMonth.equals")?,
614            st_i32(caller, a, 2, "PlainYearMonth.equals")?,
615        ),
616        (
617            st_i32(caller, b, 1, "PlainYearMonth.equals")?,
618            st_i32(caller, b, 2, "PlainYearMonth.equals")?,
619        ),
620    ))
621}
622
623pub(super) fn plain_equals_month_day(
624    caller: &mut Caller<'_, StoreData>,
625    a: Rooted<StructRef>,
626    b: Rooted<StructRef>,
627) -> wasmtime::Result<bool> {
628    Ok(super::plain_month_day::equals(
629        (
630            st_i32(caller, a, 1, "PlainMonthDay.equals")?,
631            st_i32(caller, a, 2, "PlainMonthDay.equals")?,
632        ),
633        (
634            st_i32(caller, b, 1, "PlainMonthDay.equals")?,
635            st_i32(caller, b, 2, "PlainMonthDay.equals")?,
636        ),
637    ))
638}
639
640fn instant_to_string_host(
641    caller: &mut Caller<'_, StoreData>,
642    st: Rooted<StructRef>,
643) -> wasmtime::Result<String> {
644    Ok(instant_from_struct(caller, st, "Instant.toString")?.to_string())
645}
646
647pub(super) fn instant_equals_host(
648    caller: &mut Caller<'_, StoreData>,
649    a: Rooted<StructRef>,
650    b: Rooted<StructRef>,
651) -> wasmtime::Result<bool> {
652    Ok(
653        st_i64(caller, a, 1, "Instant.equals")? == st_i64(caller, b, 1, "Instant.equals")?
654            && st_i32(caller, a, 2, "Instant.equals")? == st_i32(caller, b, 2, "Instant.equals")?,
655    )
656}
657
658fn duration_to_string_host(
659    caller: &mut Caller<'_, StoreData>,
660    st: Rooted<StructRef>,
661) -> wasmtime::Result<String> {
662    let span = span_from_duration_struct(caller, st, "Duration.toString")?;
663    duration_to_string_with_options(&span, None, None, None)
664}
665
666fn duration_equals_host(
667    caller: &mut Caller<'_, StoreData>,
668    a: Rooted<StructRef>,
669    b: Rooted<StructRef>,
670) -> wasmtime::Result<bool> {
671    fields_equal(
672        caller,
673        a,
674        b,
675        &[1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
676        "Duration.equals",
677    )
678}
679
680fn zoned_date_time_to_string_host(
681    caller: &mut Caller<'_, StoreData>,
682    st: Rooted<StructRef>,
683) -> wasmtime::Result<String> {
684    let z = zoned_date_time_from_struct(caller, st, "ZonedDateTime.toString")?;
685    Ok(z.to_string())
686}
687
688fn zoned_date_time_equals_host(
689    caller: &mut Caller<'_, StoreData>,
690    a: Rooted<StructRef>,
691    b: Rooted<StructRef>,
692) -> wasmtime::Result<bool> {
693    let a_secs = st_i64(caller, a, 1, "ZonedDateTime.equals")?;
694    let a_nanos = st_i32(caller, a, 2, "ZonedDateTime.equals")?;
695    let a_tz = st_string(caller, a, 3, "ZonedDateTime.equals")?;
696    let b_secs = st_i64(caller, b, 1, "ZonedDateTime.equals")?;
697    let b_nanos = st_i32(caller, b, 2, "ZonedDateTime.equals")?;
698    let b_tz = st_string(caller, b, 3, "ZonedDateTime.equals")?;
699    Ok(a_secs == b_secs
700        && a_nanos == b_nanos
701        && super::zoned_date_time::time_zone_ids_equal(caller, &a_tz, &b_tz)?)
702}
703
704pub(super) fn reg_plain_date_to_year_month(
705    linker: &mut Linker<StoreData>,
706    iface: &'static str,
707    ty: FuncType,
708) -> wasmtime::Result<()> {
709    register_host_fn(
710        linker,
711        crate::runtime::prelude::MODULE_NAME,
712        prelude_key(iface, "toPlainYearMonth"),
713        ty,
714        true,
715        move |caller, params, results| {
716            let st = as_struct_val(caller, abi_arg(params, 0)?, "toPlainYearMonth")?;
717            let y = st_i32(caller, st, 1, "toPlainYearMonth")?;
718            let m = st_i32(caller, st, 2, "toPlainYearMonth")?;
719            *abi_result(results, 0)? = make_plain_year_month(caller, y, m)?;
720            Ok(())
721        },
722    )
723}
724
725pub(super) fn reg_plain_date_to_month_day(
726    linker: &mut Linker<StoreData>,
727    iface: &'static str,
728    ty: FuncType,
729) -> wasmtime::Result<()> {
730    register_host_fn(
731        linker,
732        crate::runtime::prelude::MODULE_NAME,
733        prelude_key(iface, "toPlainMonthDay"),
734        ty,
735        true,
736        move |caller, params, results| {
737            let st = as_struct_val(caller, abi_arg(params, 0)?, "toPlainMonthDay")?;
738            let m = st_i32(caller, st, 2, "toPlainMonthDay")?;
739            let d = st_i32(caller, st, 3, "toPlainMonthDay")?;
740            *abi_result(results, 0)? = make_plain_month_day(caller, m, d)?;
741            Ok(())
742        },
743    )
744}
745
746pub(super) fn prelude_key(iface: &str, method: &str) -> MangledName {
747    crate::mangle::extend(
748        &crate::mangle::extend(&crate::mangle::prelude("Temporal"), iface),
749        method,
750    )
751}
752
753pub(super) fn reg_plain_from(
754    linker: &mut Linker<StoreData>,
755    engine: &wasmtime::Engine,
756    string: &ValType,
757    obj: &ValType,
758    ctor_iface: &str,
759    label: &'static str,
760    op: fn(&str, &mut Caller<'_, StoreData>) -> wasmtime::Result<Val>,
761) -> wasmtime::Result<()> {
762    register_host_fn(
763        linker,
764        crate::runtime::prelude::MODULE_NAME,
765        prelude_key(ctor_iface, "from"),
766        FuncType::new(engine, [string.clone()], [obj.clone()]),
767        true,
768        move |caller, params, results| {
769            let s = read_string_arg(caller, abi_arg(params, 0)?, label)?;
770            *abi_result(results, 0)? = op(&s, caller)?;
771            Ok(())
772        },
773    )
774}
775
776pub(super) fn reg_plain_string(
777    linker: &mut Linker<StoreData>,
778    engine: &wasmtime::Engine,
779    obj: &ValType,
780    string: &ValType,
781    iface: &str,
782    method: &str,
783    op: PlainStringer,
784) -> wasmtime::Result<()> {
785    let label = format!("{iface}.{method}");
786    register_host_fn(
787        linker,
788        crate::runtime::prelude::MODULE_NAME,
789        prelude_key(iface, method),
790        FuncType::new(engine, [obj.clone()], [string.clone()]),
791        true,
792        move |caller, params, results| {
793            let st = as_struct_val(caller, abi_arg(params, 0)?, &label)?;
794            let out = op(caller, st)?;
795            let s = write_submilli_string_struct(caller, &out)?;
796            *abi_result(results, 0)? = Val::AnyRef(Some(s.to_anyref()));
797            Ok(())
798        },
799    )
800}
801
802pub(super) fn reg_plain_equals(
803    linker: &mut Linker<StoreData>,
804    engine: &wasmtime::Engine,
805    obj: &ValType,
806    iface: &str,
807    op: PlainEquals,
808) -> wasmtime::Result<()> {
809    let label = format!("{iface}.equals");
810    register_host_fn(
811        linker,
812        crate::runtime::prelude::MODULE_NAME,
813        prelude_key(iface, "equals"),
814        FuncType::new(engine, [obj.clone(), obj.clone()], [ValType::I32]),
815        true,
816        move |caller, params, results| {
817            let a = as_struct_val(caller, abi_arg(params, 0)?, &label)?;
818            let b = as_struct_val(caller, abi_arg(params, 1)?, &label)?;
819            *abi_result(results, 0)? = Val::I32(op(caller, a, b)? as i32);
820            Ok(())
821        },
822    )
823}
824
825pub(super) fn reg_plain_compare(
826    linker: &mut Linker<StoreData>,
827    engine: &wasmtime::Engine,
828    obj: &ValType,
829    ctor_iface: &str,
830    fields: &'static [u32],
831) -> wasmtime::Result<()> {
832    let label = format!("{ctor_iface}.compare");
833    register_host_fn(
834        linker,
835        crate::runtime::prelude::MODULE_NAME,
836        prelude_key(ctor_iface, "compare"),
837        FuncType::new(engine, [obj.clone(), obj.clone()], [ValType::F64]),
838        true,
839        move |caller, params, results| {
840            let a = as_struct_val(caller, abi_arg(params, 0)?, &label)?;
841            let b = as_struct_val(caller, abi_arg(params, 1)?, &label)?;
842            let mut ord = 0;
843            for &field in fields {
844                let av = st_i32(caller, a, field, &label)?;
845                let bv = st_i32(caller, b, field, &label)?;
846                if av != bv {
847                    ord = if av < bv { -1 } else { 1 };
848                    break;
849                }
850            }
851            *abi_result(results, 0)? = Val::F64((ord as f64).to_bits());
852            Ok(())
853        },
854    )
855}
856
857pub(super) fn reg_plain_i32_getter(
858    linker: &mut Linker<StoreData>,
859    engine: &wasmtime::Engine,
860    obj: &ValType,
861    iface: &str,
862    name: &str,
863    field: u32,
864) -> wasmtime::Result<()> {
865    let label = format!("{iface}.{name}");
866    register_host_fn(
867        linker,
868        crate::runtime::prelude::MODULE_NAME,
869        prelude_key(iface, name),
870        FuncType::new(engine, [obj.clone()], [ValType::F64]),
871        true,
872        move |caller, params, results| {
873            let st = as_struct_val(caller, abi_arg(params, 0)?, &label)?;
874            *abi_result(results, 0)? =
875                Val::F64((st_i32(caller, st, field, &label)? as f64).to_bits());
876            Ok(())
877        },
878    )
879}
880
881pub(super) fn reg_plain_time_getters(
882    linker: &mut Linker<StoreData>,
883    engine: &wasmtime::Engine,
884    obj: &ValType,
885    iface: &str,
886) -> wasmtime::Result<()> {
887    let ns_field = if iface == "PlainTime" { 4 } else { 7 };
888    for (name, div) in [
889        ("millisecond", 1_000_000),
890        ("microsecond", 1_000),
891        ("nanosecond", 1),
892    ] {
893        let label = format!("{iface}.{name}");
894        register_host_fn(
895            linker,
896            crate::runtime::prelude::MODULE_NAME,
897            prelude_key(iface, name),
898            FuncType::new(engine, [obj.clone()], [ValType::F64]),
899            true,
900            move |caller, params, results| {
901                let st = as_struct_val(caller, abi_arg(params, 0)?, &label)?;
902                let ns = st_i32(caller, st, ns_field, &label)?;
903                let out = if div == 1 { ns } else { (ns / div) % 1000 };
904                *abi_result(results, 0)? = Val::F64((out as f64).to_bits());
905                Ok(())
906            },
907        )?;
908    }
909    Ok(())
910}
911
912pub(super) fn reg_plain_date_derived_getters(
913    linker: &mut Linker<StoreData>,
914    engine: &wasmtime::Engine,
915    obj: &ValType,
916    iface: &str,
917    year_month_only: i32,
918) -> wasmtime::Result<()> {
919    let specs = [
920        ("dayOfWeek", 0),
921        ("monthCode", 0),
922        ("dayOfYear", 1),
923        ("weekOfYear", 2),
924        ("yearOfWeek", 3),
925        ("daysInWeek", 4),
926        ("daysInMonth", 5),
927        ("daysInYear", 6),
928        ("monthsInYear", 7),
929        ("inLeapYear", 8),
930    ];
931    for (name, selector) in specs {
932        if year_month_only == 1
933            && matches!(
934                name,
935                "dayOfWeek" | "dayOfYear" | "weekOfYear" | "yearOfWeek" | "daysInWeek"
936            )
937        {
938            continue;
939        }
940        if name == "monthCode" {
941            continue;
942        }
943        let label = format!("{iface}.{name}");
944        let ret = if name == "inLeapYear" {
945            ValType::I32
946        } else {
947            ValType::F64
948        };
949        register_host_fn(
950            linker,
951            crate::runtime::prelude::MODULE_NAME,
952            prelude_key(iface, name),
953            FuncType::new(engine, [obj.clone()], [ret]),
954            true,
955            move |caller, params, results| {
956                let st = as_struct_val(caller, abi_arg(params, 0)?, &label)?;
957                let y = i64::from(st_i32(caller, st, 1, &label)?);
958                let m = i64::from(st_i32(caller, st, 2, &label)?);
959                let d = if year_month_only == 1 {
960                    1
961                } else {
962                    i64::from(st_i32(caller, st, 3, &label)?)
963                };
964                let date = y_m_d_date(y, m, d, "Temporal.Plain.derivedField")?;
965                let val = date_derived_field(date, selector);
966                if selector == 8 {
967                    *abi_result(results, 0)? = Val::I32(val);
968                } else {
969                    *abi_result(results, 0)? = Val::F64((val as f64).to_bits());
970                }
971                Ok(())
972            },
973        )?;
974    }
975    reg_plain_month_code_getter(
976        linker,
977        engine,
978        obj,
979        &ValType::Ref(RefType::new(
980            false,
981            HeapType::ConcreteStruct(
982                crate::runtime::intrinsic_types::build_intrinsic_types(engine)?.string,
983            ),
984        )),
985        iface,
986        2,
987    )
988}
989
990pub(super) fn reg_plain_month_code_getter(
991    linker: &mut Linker<StoreData>,
992    engine: &wasmtime::Engine,
993    obj: &ValType,
994    string: &ValType,
995    iface: &str,
996    month_field: u32,
997) -> wasmtime::Result<()> {
998    let label = format!("{iface}.monthCode");
999    register_host_fn(
1000        linker,
1001        crate::runtime::prelude::MODULE_NAME,
1002        prelude_key(iface, "monthCode"),
1003        FuncType::new(engine, [obj.clone()], [string.clone()]),
1004        true,
1005        move |caller, params, results| {
1006            let st = as_struct_val(caller, abi_arg(params, 0)?, &label)?;
1007            let month = st_i32(caller, st, month_field, &label)?;
1008            let s = write_submilli_string_struct(caller, &format!("M{month:02}"))?;
1009            *abi_result(results, 0)? = Val::AnyRef(Some(s.to_anyref()));
1010            Ok(())
1011        },
1012    )
1013}
1014
1015fn temporal_abi<'a>(caller: &'a Caller<'_, StoreData>) -> wasmtime::Result<&'a TemporalAbi> {
1016    caller
1017        .data()
1018        .host_abi
1019        .as_ref()
1020        .map(|abi| &abi.temporal)
1021        .ok_or_else(|| wasmtime::Error::msg("host_abi unset (prelude not instantiated)"))
1022}
1023
1024fn make_plain_struct(
1025    caller: &mut Caller<'_, StoreData>,
1026    ty: StructType,
1027    vtable: Global,
1028    fields: &[i32],
1029) -> wasmtime::Result<Val> {
1030    let mut vals = Vec::with_capacity(fields.len() + 1);
1031    vals.push(vtable.get(&mut *caller));
1032    vals.extend(fields.iter().copied().map(Val::I32));
1033    let pre = StructRefPre::new(&mut *caller, ty);
1034    let st = StructRef::new(&mut *caller, &pre, &vals)?;
1035    Ok(Val::AnyRef(Some(st.to_anyref())))
1036}
1037
1038fn make_temporal_struct(
1039    caller: &mut Caller<'_, StoreData>,
1040    ty: StructType,
1041    vtable: Global,
1042    fields: &[Val],
1043) -> wasmtime::Result<Val> {
1044    let mut vals = Vec::with_capacity(fields.len() + 1);
1045    vals.push(vtable.get(&mut *caller));
1046    vals.extend_from_slice(fields);
1047    let pre = StructRefPre::new(&mut *caller, ty);
1048    let st = StructRef::new(&mut *caller, &pre, &vals)?;
1049    Ok(Val::AnyRef(Some(st.to_anyref())))
1050}
1051
1052pub(super) fn make_duration(
1053    caller: &mut Caller<'_, StoreData>,
1054    span: &Span,
1055) -> wasmtime::Result<Val> {
1056    let (ty, vtable) = {
1057        let abi = temporal_abi(caller)?;
1058        (abi.duration.clone(), abi.duration_vtable)
1059    };
1060    let mut fields = vec![Val::I32(0); 10];
1061    write_span(&mut fields, span)?;
1062    make_temporal_struct(caller, ty, vtable, &fields)
1063}
1064
1065pub(super) fn make_instant(
1066    caller: &mut Caller<'_, StoreData>,
1067    ts: Timestamp,
1068) -> wasmtime::Result<Val> {
1069    let (ty, vtable) = {
1070        let abi = temporal_abi(caller)?;
1071        (abi.instant.clone(), abi.instant_vtable)
1072    };
1073    make_temporal_struct(
1074        caller,
1075        ty,
1076        vtable,
1077        &[Val::I64(ts.as_second()), Val::I32(ts.subsec_nanosecond())],
1078    )
1079}
1080
1081pub(super) fn instant_from_struct(
1082    caller: &mut Caller<'_, StoreData>,
1083    st: Rooted<StructRef>,
1084    label: &'static str,
1085) -> wasmtime::Result<Timestamp> {
1086    Timestamp::new(st_i64(caller, st, 1, label)?, st_i32(caller, st, 2, label)?)
1087        .map_err(temporal_err(label))
1088}
1089
1090pub(super) fn instant_from_val(
1091    caller: &mut Caller<'_, StoreData>,
1092    val: &Val,
1093    label: &'static str,
1094) -> wasmtime::Result<Timestamp> {
1095    let st = as_struct_val(caller, val, label)?;
1096    instant_from_struct(caller, st, label)
1097}
1098
1099pub(super) fn epoch_milliseconds(ts: Timestamp) -> f64 {
1100    ts.as_nanosecond().div_euclid(1_000_000) as f64
1101}
1102
1103pub(super) fn make_zoned_date_time(
1104    caller: &mut Caller<'_, StoreData>,
1105    zoned: &Zoned,
1106    tz_id: &str,
1107) -> wasmtime::Result<Val> {
1108    let (ty, vtable) = {
1109        let abi = temporal_abi(caller)?;
1110        (abi.zoned_date_time.clone(), abi.zoned_date_time_vtable)
1111    };
1112    let bytes = (std::mem::size_of::<CachedZoned>() + 128) as u64;
1113    caller.data().tenant_limits.charge_host_bytes(bytes)?;
1114    let payload = CachedZoned {
1115        value: zoned.clone(),
1116        bytes,
1117        counter: caller.data().tenant_limits.host_attached_counter(),
1118    };
1119    let cached = wasmtime::ExternRef::new(&mut *caller, payload)?;
1120    let ts = zoned.timestamp();
1121    let tz = write_submilli_string_struct(caller, tz_id)?;
1122    make_temporal_struct(
1123        caller,
1124        ty,
1125        vtable,
1126        &[
1127            Val::I64(ts.as_second()),
1128            Val::I32(ts.subsec_nanosecond()),
1129            Val::AnyRef(Some(tz.to_anyref())),
1130            Val::ExternRef(Some(cached)),
1131        ],
1132    )
1133}
1134
1135pub(super) fn make_plain_date(
1136    caller: &mut Caller<'_, StoreData>,
1137    y: i32,
1138    m: i32,
1139    d: i32,
1140) -> wasmtime::Result<Val> {
1141    let (ty, vtable) = {
1142        let abi = temporal_abi(caller)?;
1143        (abi.plain_date.clone(), abi.plain_date_vtable)
1144    };
1145    make_plain_struct(caller, ty, vtable, &[y, m, d])
1146}
1147
1148pub(super) fn make_plain_time(
1149    caller: &mut Caller<'_, StoreData>,
1150    h: i32,
1151    m: i32,
1152    s: i32,
1153    ns: i32,
1154) -> wasmtime::Result<Val> {
1155    let (ty, vtable) = {
1156        let abi = temporal_abi(caller)?;
1157        (abi.plain_time.clone(), abi.plain_time_vtable)
1158    };
1159    make_plain_struct(caller, ty, vtable, &[h, m, s, ns])
1160}
1161
1162#[allow(clippy::too_many_arguments)]
1163pub(super) fn make_plain_date_time(
1164    caller: &mut Caller<'_, StoreData>,
1165    y: i32,
1166    m: i32,
1167    d: i32,
1168    h: i32,
1169    mi: i32,
1170    s: i32,
1171    ns: i32,
1172) -> wasmtime::Result<Val> {
1173    let (ty, vtable) = {
1174        let abi = temporal_abi(caller)?;
1175        (abi.plain_date_time.clone(), abi.plain_date_time_vtable)
1176    };
1177    make_plain_struct(caller, ty, vtable, &[y, m, d, h, mi, s, ns])
1178}
1179
1180pub(super) fn make_plain_year_month(
1181    caller: &mut Caller<'_, StoreData>,
1182    y: i32,
1183    m: i32,
1184) -> wasmtime::Result<Val> {
1185    let (ty, vtable) = {
1186        let abi = temporal_abi(caller)?;
1187        (abi.plain_year_month.clone(), abi.plain_year_month_vtable)
1188    };
1189    make_plain_struct(caller, ty, vtable, &[y, m])
1190}
1191
1192pub(super) fn make_plain_month_day(
1193    caller: &mut Caller<'_, StoreData>,
1194    m: i32,
1195    d: i32,
1196) -> wasmtime::Result<Val> {
1197    let (ty, vtable) = {
1198        let abi = temporal_abi(caller)?;
1199        (abi.plain_month_day.clone(), abi.plain_month_day_vtable)
1200    };
1201    make_temporal_struct(
1202        caller,
1203        ty,
1204        vtable,
1205        &[Val::I32(m), Val::I32(d), Val::I64(PLAIN_MONTH_DAY_MARKER)],
1206    )
1207}
1208
1209pub(super) fn plain_date_from_struct(
1210    caller: &mut Caller<'_, StoreData>,
1211    st: Rooted<StructRef>,
1212    label: &str,
1213) -> wasmtime::Result<civil::Date> {
1214    y_m_d_date(
1215        i64::from(st_i32(caller, st, 1, label)?),
1216        i64::from(st_i32(caller, st, 2, label)?),
1217        i64::from(st_i32(caller, st, 3, label)?),
1218        "PlainDate",
1219    )
1220}
1221
1222pub(super) fn plain_time_from_struct(
1223    caller: &mut Caller<'_, StoreData>,
1224    st: Rooted<StructRef>,
1225    field_offset: u32,
1226    label: &str,
1227) -> wasmtime::Result<civil::Time> {
1228    h_m_s_ns_time(
1229        i64::from(st_i32(caller, st, field_offset + 1, label)?),
1230        i64::from(st_i32(caller, st, field_offset + 2, label)?),
1231        i64::from(st_i32(caller, st, field_offset + 3, label)?),
1232        i64::from(st_i32(caller, st, field_offset + 4, label)?),
1233        label,
1234    )
1235}
1236
1237pub(super) fn st_i32_or(
1238    caller: &mut Caller<'_, StoreData>,
1239    st: Rooted<StructRef>,
1240    field: u32,
1241) -> i32 {
1242    match st.field(caller, field as usize) {
1243        Ok(Val::I32(v)) => v,
1244        _ => 0,
1245    }
1246}
1247
1248fn st_i64(
1249    caller: &mut Caller<'_, StoreData>,
1250    st: Rooted<StructRef>,
1251    field: u32,
1252    label: &str,
1253) -> wasmtime::Result<i64> {
1254    match st.field(caller, field as usize)? {
1255        Val::I64(v) => Ok(v),
1256        other => wasmtime::bail!("Temporal.{label}: expected i64 field {field}, got {other:?}"),
1257    }
1258}
1259
1260fn st_string(
1261    caller: &mut Caller<'_, StoreData>,
1262    st: Rooted<StructRef>,
1263    field: u32,
1264    label: &str,
1265) -> wasmtime::Result<String> {
1266    let val = st.field(&mut *caller, field as usize)?;
1267    read_string_arg(caller, &val, label)
1268}
1269
1270pub(super) fn span_from_duration_struct(
1271    caller: &mut Caller<'_, StoreData>,
1272    st: Rooted<StructRef>,
1273    label: &str,
1274) -> wasmtime::Result<Span> {
1275    let mut fields = Vec::with_capacity(10);
1276    for i in 1..=10 {
1277        fields.push(st.field(&mut *caller, i)?);
1278    }
1279    let fields = read_span_fields(&fields)?;
1280    super::duration::from_fields(fields, label).map_err(crate::runtime::host::range_error)
1281}
1282
1283struct CachedZoned {
1284    value: Zoned,
1285    counter: std::sync::Arc<std::sync::atomic::AtomicU64>,
1286    bytes: u64,
1287}
1288impl Drop for CachedZoned {
1289    fn drop(&mut self) {
1290        self.counter
1291            .fetch_sub(self.bytes, std::sync::atomic::Ordering::Relaxed);
1292    }
1293}
1294
1295pub(super) fn zoned_date_time_from_struct(
1296    caller: &mut Caller<'_, StoreData>,
1297    st: Rooted<StructRef>,
1298    _label: &'static str,
1299) -> wasmtime::Result<Zoned> {
1300    let Val::ExternRef(Some(cached)) = st.field(&mut *caller, 4)? else {
1301        return Err(crate::runtime::host::fatal_host_error(
1302            "Temporal.ZonedDateTime: missing resolved value",
1303        ));
1304    };
1305    let payload = cached
1306        .data(&*caller)?
1307        .and_then(|data| data.downcast_ref::<CachedZoned>())
1308        .ok_or_else(|| {
1309            crate::runtime::host::fatal_host_error("Temporal.ZonedDateTime: invalid resolved value")
1310        })?;
1311    Ok(payload.value.clone())
1312}
1313
1314pub(super) fn zoned_date_time_from_val(
1315    caller: &mut Caller<'_, StoreData>,
1316    val: &Val,
1317    label: &'static str,
1318) -> wasmtime::Result<Zoned> {
1319    let ty = temporal_abi(caller)?.zoned_date_time.clone();
1320    let st = cast_struct(caller, val, &ty, label)?;
1321    zoned_date_time_from_struct(caller, st, label)
1322}
1323
1324pub(super) fn zoned_date_time_timestamp_from_val(
1325    caller: &mut Caller<'_, StoreData>,
1326    val: &Val,
1327    label: &'static str,
1328) -> wasmtime::Result<Timestamp> {
1329    let ty = temporal_abi(caller)?.zoned_date_time.clone();
1330    let st = cast_struct(caller, val, &ty, label)?;
1331    instant_from_struct(caller, st, label)
1332}
1333
1334pub(super) fn zoned_date_time_identity_from_val(
1335    caller: &mut Caller<'_, StoreData>,
1336    val: &Val,
1337    label: &'static str,
1338) -> wasmtime::Result<(Timestamp, String)> {
1339    let ty = temporal_abi(caller)?.zoned_date_time.clone();
1340    let st = cast_struct(caller, val, &ty, label)?;
1341    let timestamp = instant_from_struct(caller, st, label)?;
1342    let time_zone_id = st_string(caller, st, 3, label)?;
1343    Ok((timestamp, time_zone_id))
1344}
1345
1346pub(super) fn zoned_date_time_time_zone_id_from_val(
1347    caller: &mut Caller<'_, StoreData>,
1348    val: &Val,
1349    label: &'static str,
1350) -> wasmtime::Result<String> {
1351    let ty = temporal_abi(caller)?.zoned_date_time.clone();
1352    let st = cast_struct(caller, val, &ty, label)?;
1353    st_string(caller, st, 3, label)
1354}
1355
1356pub(super) fn zoned_date_time_parts_from_val(
1357    caller: &mut Caller<'_, StoreData>,
1358    val: &Val,
1359    label: &'static str,
1360) -> wasmtime::Result<(Zoned, String)> {
1361    let ty = temporal_abi(caller)?.zoned_date_time.clone();
1362    let st = cast_struct(caller, val, &ty, label)?;
1363    let secs = st_i64(caller, st, 1, label)?;
1364    let nanos = st_i32(caller, st, 2, label)?;
1365    let tz_id = st_string(caller, st, 3, label)?;
1366    let zoned = make_zoned(caller, secs, nanos, &tz_id, label)?;
1367    Ok((zoned, tz_id))
1368}
1369
1370pub(super) fn read_duration_like(
1371    caller: &mut Caller<'_, StoreData>,
1372    val: &Val,
1373    intr: &IntrinsicTypes,
1374    label: &str,
1375) -> wasmtime::Result<Span> {
1376    if let Val::AnyRef(Some(any)) = val
1377        && let Some(st) = any.as_struct(&mut *caller)?
1378        && st.matches_ty(&*caller, &intr.temporal_duration)?
1379    {
1380        return span_from_duration_struct(caller, st, label);
1381    }
1382
1383    let mut fields = [0; 10];
1384    for (index, name) in DURATION_FIELD_NAMES.iter().enumerate() {
1385        fields[index] = object_duration_field(caller, val, name, index, label)?.unwrap_or(0);
1386    }
1387    super::duration::from_fields(fields, label).map_err(crate::runtime::host::range_error)
1388}
1389
1390pub(super) fn duration_field_from_val(
1391    caller: &mut Caller<'_, StoreData>,
1392    val: &Val,
1393    index: usize,
1394    label: &'static str,
1395) -> wasmtime::Result<i64> {
1396    let ty = temporal_abi(caller)?.duration.clone();
1397    let st = cast_struct(caller, val, &ty, label)?;
1398    duration_struct_field(caller, st, index, label)
1399}
1400
1401pub(super) fn duration_sign_from_val(
1402    caller: &mut Caller<'_, StoreData>,
1403    val: &Val,
1404    label: &'static str,
1405) -> wasmtime::Result<i32> {
1406    let ty = temporal_abi(caller)?.duration.clone();
1407    let st = cast_struct(caller, val, &ty, label)?;
1408    for index in 0..DURATION_FIELD_NAMES.len() {
1409        let value = duration_struct_field(caller, st, index, label)?;
1410        if value != 0 {
1411            return Ok(value.signum() as i32);
1412        }
1413    }
1414    Ok(0)
1415}
1416
1417fn duration_struct_field(
1418    caller: &mut Caller<'_, StoreData>,
1419    st: Rooted<StructRef>,
1420    index: usize,
1421    label: &str,
1422) -> wasmtime::Result<i64> {
1423    let field = u32::try_from(index + 1)
1424        .map_err(|_| wasmtime::Error::msg("Temporal.Duration: invalid field index"))?;
1425    if index < 4 {
1426        st_i32(caller, st, field, label).map(i64::from)
1427    } else {
1428        st_i64(caller, st, field, label)
1429    }
1430}
1431
1432pub(super) const DURATION_FIELD_NAMES: [&str; 10] = [
1433    "years",
1434    "months",
1435    "weeks",
1436    "days",
1437    "hours",
1438    "minutes",
1439    "seconds",
1440    "milliseconds",
1441    "microseconds",
1442    "nanoseconds",
1443];
1444
1445fn object_field(
1446    caller: &mut Caller<'_, StoreData>,
1447    obj: &Val,
1448    name: &str,
1449) -> wasmtime::Result<Option<Val>> {
1450    let Some(value) = crate::runtime::prelude::collection::object_field(caller, obj, name)? else {
1451        return Ok(None);
1452    };
1453    if is_undefined(caller, &value)? {
1454        return Ok(None);
1455    }
1456    Ok(Some(value))
1457}
1458
1459pub(super) fn object_time_bag(
1460    caller: &mut Caller<'_, StoreData>,
1461    obj: &Val,
1462    label: &'static str,
1463) -> wasmtime::Result<[Option<i64>; 6]> {
1464    Ok([
1465        object_i64_field(caller, obj, "hour", label)?,
1466        object_i64_field(caller, obj, "minute", label)?,
1467        object_i64_field(caller, obj, "second", label)?,
1468        object_i64_field(caller, obj, "millisecond", label)?,
1469        object_i64_field(caller, obj, "microsecond", label)?,
1470        object_i64_field(caller, obj, "nanosecond", label)?,
1471    ])
1472}
1473
1474pub(super) fn object_i64_field(
1475    caller: &mut Caller<'_, StoreData>,
1476    obj: &Val,
1477    name: &str,
1478    label: &str,
1479) -> wasmtime::Result<Option<i64>> {
1480    let Some(f) = object_integer_f64_field(caller, obj, name, label)? else {
1481        return Ok(None);
1482    };
1483    if f <= -I64_BOUND_EXCLUSIVE || f >= I64_BOUND_EXCLUSIVE {
1484        return Err(crate::runtime::host::range_error(format!(
1485            "Temporal.{label}{{{name}}}: value {f} is outside the supported integer range; use a smaller absolute value"
1486        )));
1487    }
1488    Ok(Some(f as i64))
1489}
1490
1491pub(super) fn object_duration_field(
1492    caller: &mut Caller<'_, StoreData>,
1493    obj: &Val,
1494    name: &str,
1495    index: usize,
1496    label: &str,
1497) -> wasmtime::Result<Option<i64>> {
1498    let Some(value) = object_integer_f64_field(caller, obj, name, label)? else {
1499        return Ok(None);
1500    };
1501    super::duration::field_from_f64(value, index, label)
1502        .map(Some)
1503        .map_err(crate::runtime::host::range_error)
1504}
1505
1506fn object_integer_f64_field(
1507    caller: &mut Caller<'_, StoreData>,
1508    obj: &Val,
1509    name: &str,
1510    label: &str,
1511) -> wasmtime::Result<Option<f64>> {
1512    let Some(value) = object_f64_field(caller, obj, name, label)? else {
1513        return Ok(None);
1514    };
1515    if !value.is_finite() {
1516        return Err(crate::runtime::host::range_error(format!(
1517            "Temporal.{label}{{{name}}}: non-finite value"
1518        )));
1519    }
1520    if value.fract() != 0.0 {
1521        return Err(crate::runtime::host::range_error(format!(
1522            "Temporal.{label}{{{name}}}: non-integer value {value}"
1523        )));
1524    }
1525    Ok(Some(value))
1526}
1527
1528pub(super) fn object_string_field(
1529    caller: &mut Caller<'_, StoreData>,
1530    obj: &Val,
1531    name: &str,
1532    label: &str,
1533) -> wasmtime::Result<Option<String>> {
1534    let Some(v) = object_field(caller, obj, name)? else {
1535        return Ok(None);
1536    };
1537    read_string_arg(caller, &v, label).map(Some)
1538}
1539
1540pub(super) fn object_diff_options(
1541    caller: &mut Caller<'_, StoreData>,
1542    obj: &Val,
1543    label: &'static str,
1544) -> wasmtime::Result<DiffOptions> {
1545    let smallest = object_string_field(caller, obj, "smallestUnit", label)?
1546        .map(|unit| unit_from_str(&unit, label))
1547        .transpose()?;
1548    let largest = object_string_field(caller, obj, "largestUnit", label)?
1549        .map(|unit| unit_from_str(&unit, label))
1550        .transpose()?;
1551    let mode = object_string_field(caller, obj, "roundingMode", label)?
1552        .map(|mode| round_mode_from_str(&mode, label))
1553        .transpose()?;
1554    let increment = object_i64_field(caller, obj, "roundingIncrement", label)?;
1555    Ok(DiffOptions {
1556        smallest,
1557        largest,
1558        mode,
1559        increment,
1560    })
1561}
1562
1563fn object_f64_field(
1564    caller: &mut Caller<'_, StoreData>,
1565    obj: &Val,
1566    name: &str,
1567    label: &str,
1568) -> wasmtime::Result<Option<f64>> {
1569    let Some(v) = object_field(caller, obj, name)? else {
1570        return Ok(None);
1571    };
1572    let boxed_number_type = caller
1573        .data()
1574        .host_abi
1575        .as_ref()
1576        .map(|abi| abi.boxed_number_type.clone())
1577        .ok_or_else(|| wasmtime::Error::msg("host_abi unset (prelude not instantiated)"))?;
1578    let Val::AnyRef(Some(any)) = v else {
1579        wasmtime::bail!("Temporal.{label}{{{name}}}: expected boxed number");
1580    };
1581    let Some(st) = any.as_struct(&mut *caller)? else {
1582        wasmtime::bail!("Temporal.{label}{{{name}}}: expected boxed number");
1583    };
1584    if !st.matches_ty(&*caller, &boxed_number_type)? {
1585        wasmtime::bail!("Temporal.{label}{{{name}}}: expected boxed number");
1586    }
1587    match st.field(&mut *caller, 1)? {
1588        Val::F64(bits) => Ok(Some(f64::from_bits(bits))),
1589        other => wasmtime::bail!("Temporal.{label}{{{name}}}: expected f64, got {other:?}"),
1590    }
1591}
1592
1593pub fn temporal_module_package_declaration() -> PackageDeclaration {
1594    let mut defs = PackageDeclaration::with_package(TEMPORAL_MODULE_NAME);
1595    let temporal_prefix = crate::mangle::host(TEMPORAL_MODULE_NAME, "Temporal");
1596
1597    let temporal = NamespaceSymbol {
1598        name: "Temporal".to_string(),
1599        mangled_prefix: temporal_prefix.clone(),
1600        declaration_span: DiagSpan::at(crate::FileId::TEMPORAL),
1601        values: BTreeMap::new(),
1602        types: BTreeMap::new(),
1603        namespaces: BTreeMap::new(),
1604        doc: None,
1605    };
1606
1607    // No symbols here; the prelude is the user surface. Inserting an empty
1608    // Temporal namespace lets load_namespaces merge it with the prelude's as a no-op.
1609    let _ = &temporal_prefix;
1610
1611    defs.namespaces.insert("Temporal".to_string(), temporal);
1612    defs
1613}
1614
1615pub(super) fn temporal_type(name: &str) -> Type {
1616    Type::interface_ref(
1617        Package::prelude(),
1618        format!("Temporal.{name}"),
1619        crate::mangle::extend(&crate::mangle::prelude("Temporal"), name),
1620        Vec::new(),
1621    )
1622}
1623
1624pub(super) fn object_shape_type(optional_numbers: &[&str]) -> Type {
1625    let fields = optional_numbers
1626        .iter()
1627        .map(|name| (name.to_string(), ObjectField::optional(Type::Number)))
1628        .collect();
1629    Type::Object {
1630        index: None,
1631        fields,
1632    }
1633}
1634
1635pub(super) fn declare_direct_fn(
1636    defs: &mut PackageDeclaration,
1637    name: &str,
1638    mangled_name: MangledName,
1639    params: Vec<Param>,
1640    ret: Type,
1641) {
1642    defs.values.insert(
1643        mangled_name.as_str().to_string(),
1644        ValueSymbol {
1645            name: name.to_string(),
1646            mangled_name,
1647            declaration_span: DiagSpan::at(crate::FileId::TEMPORAL),
1648            kind: ValueKind::Function {
1649                generics: Vec::new(),
1650                params,
1651                ret,
1652                type_predicate: None,
1653                doc: None,
1654            },
1655        },
1656    );
1657}
1658
1659pub(super) fn clamp_month(m: i64) -> i64 {
1660    m.clamp(1, 12)
1661}
1662
1663pub(super) fn days_in_month(year: i64, month: i64, label: &'static str) -> wasmtime::Result<i64> {
1664    let d = y_m_d_date(year, month, 1, label)?;
1665    Ok(i64::from(d.days_in_month()))
1666}
1667
1668fn date_derived_field(d: civil::Date, selector: i32) -> i32 {
1669    match selector {
1670        0 => i32::from(d.weekday().to_monday_one_offset()),
1671        1 => i32::from(d.day_of_year()),
1672        2 => i32::from(d.iso_week_date().week()),
1673        3 => i32::from(d.iso_week_date().year()),
1674        4 => 7,
1675        5 => i32::from(d.days_in_month()),
1676        6 => i32::from(d.days_in_year()),
1677        7 => 12,
1678        8 => i32::from(d.in_leap_year()),
1679        _ => 0,
1680    }
1681}
1682
1683pub(super) fn y_m_d_date(
1684    year: i64,
1685    month: i64,
1686    day: i64,
1687    label: &'static str,
1688) -> wasmtime::Result<civil::Date> {
1689    let year = checked_field(year, -9_999, 9_999, label, "year")?;
1690    let month = checked_field(month, 1, 12, label, "month")?;
1691    let day = checked_field(day, 1, 31, label, "day")?;
1692    civil::Date::new(year, month, day).map_err(|_| {
1693        crate::runtime::host::range_error(format!(
1694            "Temporal.{label}: the resulting date is outside the representable range"
1695        ))
1696    })
1697}
1698
1699fn checked_field<T>(
1700    value: i64,
1701    minimum: i64,
1702    maximum: i64,
1703    label: &str,
1704    field: &str,
1705) -> wasmtime::Result<T>
1706where
1707    T: TryFrom<i64>,
1708{
1709    let range_error = || {
1710        crate::runtime::host::range_error(format!(
1711            "Temporal.{label}{{{field}}}: value {value} is outside the supported range {minimum}..={maximum}; use a {field} within that range"
1712        ))
1713    };
1714    if !(minimum..=maximum).contains(&value) {
1715        return Err(range_error());
1716    }
1717    T::try_from(value).map_err(|_| range_error())
1718}
1719
1720fn h_m_s_ns_time(
1721    hour: i64,
1722    minute: i64,
1723    second: i64,
1724    nanosecond: i64,
1725    label: &str,
1726) -> wasmtime::Result<civil::Time> {
1727    let hour = checked_field(hour, 0, 23, label, "hour")?;
1728    let minute = checked_field(minute, 0, 59, label, "minute")?;
1729    let second = checked_field(second, 0, 59, label, "second")?;
1730    let nanosecond = checked_field(nanosecond, 0, 999_999_999, label, "nanosecond")?;
1731    civil::Time::new(hour, minute, second, nanosecond).map_err(|_| {
1732        crate::runtime::host::range_error(format!(
1733            "Temporal.{label}: the resulting time is outside the representable range"
1734        ))
1735    })
1736}
1737
1738/// Merge + constrain a date: month -> [1,12], day -> [1, days-in-month].
1739pub(super) fn merge_date(
1740    recv: (i32, i32, i32),
1741    bag_year: Option<i64>,
1742    bag_month: Option<i64>,
1743    bag_day: Option<i64>,
1744    label: &'static str,
1745) -> wasmtime::Result<(i64, i64, i64)> {
1746    let year = bag_year.unwrap_or_else(|| i64::from(recv.0));
1747    let month = clamp_month(bag_month.unwrap_or_else(|| i64::from(recv.1)));
1748    let dim = days_in_month(year, month, label)?;
1749    let day = bag_day.unwrap_or_else(|| i64::from(recv.2)).clamp(1, dim);
1750    Ok((year, month, day))
1751}
1752
1753/// Merge + constrain a time. The receiver's combined sub-second slot is split
1754/// into millisecond/microsecond/nanosecond components so the bag can override any
1755/// of them; the merged components are clamped and recombined.
1756pub(super) fn merge_time(
1757    recv: (i32, i32, i32, i32),
1758    bag: [Option<i64>; 6],
1759    label: &'static str,
1760) -> wasmtime::Result<civil::Time> {
1761    let hour = bag[0].unwrap_or_else(|| i64::from(recv.0)).clamp(0, 23);
1762    let minute = bag[1].unwrap_or_else(|| i64::from(recv.1)).clamp(0, 59);
1763    let second = bag[2].unwrap_or_else(|| i64::from(recv.2)).clamp(0, 59);
1764    let sub = i64::from(recv.3);
1765    let ms: i64 = checked_field(
1766        bag[3].unwrap_or(sub / 1_000_000).clamp(0, 999),
1767        0,
1768        999,
1769        label,
1770        "millisecond",
1771    )?;
1772    let us: i64 = checked_field(
1773        bag[4].unwrap_or((sub / 1_000) % 1_000).clamp(0, 999),
1774        0,
1775        999,
1776        label,
1777        "microsecond",
1778    )?;
1779    let ns: i64 = checked_field(
1780        bag[5].unwrap_or(sub % 1_000).clamp(0, 999),
1781        0,
1782        999,
1783        label,
1784        "nanosecond",
1785    )?;
1786    let subsec = ms * 1_000_000 + us * 1_000 + ns;
1787    h_m_s_ns_time(hour, minute, second, subsec, label)
1788}
1789
1790/// The reference `civil::Date` for PlainYearMonth arithmetic: day 1 of the month,
1791/// or its last day when `negative` (Temporal anchors a backwards shift on the end
1792/// of the month so day-of-month rollover behaves).
1793pub(super) fn year_month_reference(
1794    y: i32,
1795    m: i32,
1796    negative: bool,
1797    label: &'static str,
1798) -> wasmtime::Result<civil::Date> {
1799    let first = y_m_d_date(i64::from(y), i64::from(m), 1, label)?;
1800    Ok(if negative {
1801        first.last_of_month()
1802    } else {
1803        first
1804    })
1805}
1806
1807/// Exact `until` span from (ay, am) to (by, bm) as whole months split into
1808/// years + months.
1809/// The trailing (smallestUnit, largestUnit, roundingMode, roundingIncrement)
1810/// options every `until`/`since` host fn carries. Absent options leave jiff's
1811/// per-type defaults untouched, so option-free calls behave exactly as before.
1812pub(super) struct DiffOptions {
1813    smallest: Option<Unit>,
1814    largest: Option<Unit>,
1815    mode: Option<RoundMode>,
1816    increment: Option<i64>,
1817}
1818
1819impl DiffOptions {
1820    fn is_default(&self) -> bool {
1821        self.smallest.is_none()
1822            && self.largest.is_none()
1823            && self.mode.is_none()
1824            && self.increment.is_none()
1825    }
1826
1827    pub(super) fn timestamp(&self, to: Timestamp) -> TimestampDifference {
1828        let mut d = TimestampDifference::new(to);
1829        if let Some(u) = self.smallest {
1830            d = d.smallest(u);
1831        }
1832        if let Some(u) = self.largest {
1833            d = d.largest(u);
1834        }
1835        if let Some(m) = self.mode {
1836            d = d.mode(m);
1837        }
1838        if let Some(i) = self.increment {
1839            d = d.increment(i);
1840        }
1841        d
1842    }
1843
1844    pub(super) fn zoned<'a>(&self, to: &'a Zoned) -> ZonedDifference<'a> {
1845        let mut d = ZonedDifference::new(to);
1846        if let Some(u) = self.smallest {
1847            d = d.smallest(u);
1848        }
1849        if let Some(u) = self.largest {
1850            d = d.largest(u);
1851        }
1852        if let Some(m) = self.mode {
1853            d = d.mode(m);
1854        }
1855        if let Some(i) = self.increment {
1856            d = d.increment(i);
1857        }
1858        d
1859    }
1860
1861    pub(super) fn description(&self) -> String {
1862        round_options_description(self.smallest, self.largest, self.mode, self.increment)
1863    }
1864
1865    pub(super) fn date(&self, to: civil::Date) -> civil::DateDifference {
1866        let mut d = civil::DateDifference::new(to);
1867        if let Some(u) = self.smallest {
1868            d = d.smallest(u);
1869        }
1870        if let Some(u) = self.largest {
1871            d = d.largest(u);
1872        }
1873        if let Some(m) = self.mode {
1874            d = d.mode(m);
1875        }
1876        if let Some(i) = self.increment {
1877            d = d.increment(i);
1878        }
1879        d
1880    }
1881
1882    pub(super) fn time(&self, to: civil::Time) -> civil::TimeDifference {
1883        let mut d = civil::TimeDifference::new(to);
1884        if let Some(u) = self.smallest {
1885            d = d.smallest(u);
1886        }
1887        if let Some(u) = self.largest {
1888            d = d.largest(u);
1889        }
1890        if let Some(m) = self.mode {
1891            d = d.mode(m);
1892        }
1893        if let Some(i) = self.increment {
1894            d = d.increment(i);
1895        }
1896        d
1897    }
1898
1899    pub(super) fn datetime(&self, to: civil::DateTime) -> civil::DateTimeDifference {
1900        let mut d = civil::DateTimeDifference::new(to);
1901        if let Some(u) = self.smallest {
1902            d = d.smallest(u);
1903        }
1904        if let Some(u) = self.largest {
1905            d = d.largest(u);
1906        }
1907        if let Some(m) = self.mode {
1908            d = d.mode(m);
1909        }
1910        if let Some(i) = self.increment {
1911            d = d.increment(i);
1912        }
1913        d
1914    }
1915}
1916
1917/// Year-month difference with options. The option-free path keeps the exact
1918/// integer month split; with options the pair anchors on day 1 and rounds as
1919/// dates. Only year/month units are meaningful for year-months.
1920pub(super) fn year_month_span(
1921    ay: i32,
1922    am: i32,
1923    by: i32,
1924    bm: i32,
1925    o: &DiffOptions,
1926    label: &'static str,
1927) -> wasmtime::Result<Span> {
1928    if o.is_default() {
1929        return year_month_diff(ay, am, by, bm);
1930    }
1931    for unit in [o.smallest, o.largest].into_iter().flatten() {
1932        if !matches!(unit, Unit::Year | Unit::Month) {
1933            return Err(crate::runtime::host::range_error(format!(
1934                "{label}: largestUnit/smallestUnit must be \"years\" or \"months\" for a PlainYearMonth"
1935            )));
1936        }
1937    }
1938    let a = y_m_d_date(i64::from(ay), i64::from(am), 1, label)?;
1939    let b = y_m_d_date(i64::from(by), i64::from(bm), 1, label)?;
1940    let mut d = civil::DateDifference::new(b).largest(o.largest.unwrap_or(Unit::Year));
1941    if let Some(u) = o.smallest {
1942        d = d.smallest(u);
1943    }
1944    if let Some(m) = o.mode {
1945        d = d.mode(m);
1946    }
1947    if let Some(i) = o.increment {
1948        d = d.increment(i);
1949    }
1950    a.until(d).map_err(temporal_err(label))
1951}
1952
1953fn year_month_diff(ay: i32, am: i32, by: i32, bm: i32) -> wasmtime::Result<Span> {
1954    let months = (by - ay) * 12 + (bm - am);
1955    Span::new()
1956        .try_years(months / 12)
1957        .and_then(|s| s.try_months(months % 12))
1958        .map_err(temporal_err("PlainYearMonth.until"))
1959}
1960
1961/// Maps a `jiff` failure to a Temporal-native message. We deliberately drop the
1962/// underlying error text — it leaks crate internals (`jiff::Span`, …) that the
1963/// LLM consumer can't act on — and describe the failure in domain terms. These
1964/// operations only fail on out-of-range / overflow, so one phrasing fits — and
1965/// per the Temporal spec they throw `RangeError`.
1966pub(super) fn temporal_error(label: &str, detail: impl std::fmt::Display) -> String {
1967    format!("Temporal.{label}: {detail}")
1968}
1969
1970pub(super) fn temporal_err(label: &'static str) -> impl Fn(jiff::Error) -> wasmtime::Error {
1971    move |_e| {
1972        crate::runtime::host::range_error(temporal_error(
1973            label,
1974            "a date or time value is out of the representable range",
1975        ))
1976    }
1977}
1978
1979fn read_span_fields(slice: &[Val]) -> wasmtime::Result<[i64; 10]> {
1980    Ok([
1981        i64::from(read_i32(&slice[0], "Span.years")?),
1982        i64::from(read_i32(&slice[1], "Span.months")?),
1983        i64::from(read_i32(&slice[2], "Span.weeks")?),
1984        i64::from(read_i32(&slice[3], "Span.days")?),
1985        read_i64(&slice[4], "Span.hours")?,
1986        read_i64(&slice[5], "Span.minutes")?,
1987        read_i64(&slice[6], "Span.seconds")?,
1988        read_i64(&slice[7], "Span.milliseconds")?,
1989        read_i64(&slice[8], "Span.microseconds")?,
1990        read_i64(&slice[9], "Span.nanoseconds")?,
1991    ])
1992}
1993
1994// `jiff::Span`'s per-unit getters return narrower types than our carrier
1995// slots (i16 for years, i32 for hours, etc.). Widening conversions are
1996// always lossless; suppress `clippy::useless_conversion` so future jiff
1997// versions that widen a getter don't silently lose information.
1998#[allow(clippy::useless_conversion)]
1999fn write_span(results: &mut [Val], span: &Span) -> wasmtime::Result<()> {
2000    *abi_result(results, 0)? = Val::I32(i32::from(span.get_years()));
2001    *abi_result(results, 1)? = Val::I32(i32::from(span.get_months()));
2002    *abi_result(results, 2)? = Val::I32(span.get_weeks());
2003    *abi_result(results, 3)? = Val::I32(span.get_days());
2004    *abi_result(results, 4)? = Val::I64(i64::from(span.get_hours()));
2005    *abi_result(results, 5)? = Val::I64(span.get_minutes());
2006    *abi_result(results, 6)? = Val::I64(span.get_seconds());
2007    *abi_result(results, 7)? = Val::I64(span.get_milliseconds());
2008    *abi_result(results, 8)? = Val::I64(span.get_microseconds());
2009    *abi_result(results, 9)? = Val::I64(span.get_nanoseconds());
2010    Ok(())
2011}
2012
2013fn read_i32(v: &Val, name: &str) -> wasmtime::Result<i32> {
2014    match v {
2015        Val::I32(x) => Ok(*x),
2016        other => wasmtime::bail!("{name}: expected i32, got {other:?}"),
2017    }
2018}
2019
2020fn read_i64(v: &Val, name: &str) -> wasmtime::Result<i64> {
2021    match v {
2022        Val::I64(x) => Ok(*x),
2023        other => wasmtime::bail!("{name}: expected i64, got {other:?}"),
2024    }
2025}
2026
2027fn make_zoned(
2028    caller: &mut Caller<'_, StoreData>,
2029    secs: i64,
2030    nanos: i32,
2031    tz_id: &str,
2032    label: &'static str,
2033) -> wasmtime::Result<Zoned> {
2034    let (tz, _) = super::zoned_date_time::resolve_time_zone(caller, tz_id, label)?;
2035    let ts = Timestamp::new(secs, nanos).map_err(|_| {
2036        crate::runtime::host::range_error(format!(
2037            "Temporal.{label}: the instant is outside the representable range"
2038        ))
2039    })?;
2040    Ok(ts.to_zoned(tz))
2041}
2042
2043/// A `relativeTo` anchor for calendar-aware Duration operations.
2044pub(super) enum Anchor {
2045    Date(civil::Date),
2046    Zoned(Zoned),
2047}
2048
2049pub(super) fn object_relative_to(
2050    caller: &mut Caller<'_, StoreData>,
2051    options: &Val,
2052    label: &'static str,
2053) -> wasmtime::Result<Option<Anchor>> {
2054    let Some(value) = object_field(caller, options, "relativeTo")? else {
2055        return Ok(None);
2056    };
2057    let Val::AnyRef(Some(any)) = value else {
2058        wasmtime::bail!("Temporal.{label}: relativeTo must be a PlainDate or ZonedDateTime");
2059    };
2060    let Some(st) = any.as_struct(&mut *caller)? else {
2061        wasmtime::bail!("Temporal.{label}: relativeTo must be a PlainDate or ZonedDateTime");
2062    };
2063    let (plain_date, zoned_date_time) = {
2064        let abi = temporal_abi(caller)?;
2065        (abi.plain_date.clone(), abi.zoned_date_time.clone())
2066    };
2067    if st.matches_ty(&*caller, &plain_date)? {
2068        return plain_date_from_struct(caller, st, label)
2069            .map(Anchor::Date)
2070            .map(Some);
2071    }
2072    if st.matches_ty(&*caller, &zoned_date_time)? {
2073        return zoned_date_time_from_struct(caller, st, label)
2074            .map(Anchor::Zoned)
2075            .map(Some);
2076    }
2077    wasmtime::bail!("Temporal.{label}: relativeTo must be a PlainDate or ZonedDateTime")
2078}
2079
2080pub(super) fn read_f64(v: &Val, name: &str) -> wasmtime::Result<f64> {
2081    match v {
2082        Val::F64(bits) => Ok(f64::from_bits(*bits)),
2083        other => wasmtime::bail!("{name}: expected f64, got {other:?}"),
2084    }
2085}
2086
2087/// Map a Temporal unit option (singular or plural — both accepted) to `jiff::Unit`.
2088pub(super) fn unit_from_str(unit: &str, label: &str) -> wasmtime::Result<Unit> {
2089    Ok(match unit {
2090        "nanosecond" | "nanoseconds" => Unit::Nanosecond,
2091        "microsecond" | "microseconds" => Unit::Microsecond,
2092        "millisecond" | "milliseconds" => Unit::Millisecond,
2093        "second" | "seconds" => Unit::Second,
2094        "minute" | "minutes" => Unit::Minute,
2095        "hour" | "hours" => Unit::Hour,
2096        "day" | "days" => Unit::Day,
2097        "week" | "weeks" => Unit::Week,
2098        "month" | "months" => Unit::Month,
2099        "year" | "years" => Unit::Year,
2100        other => {
2101            return Err(crate::runtime::host::range_error(format!(
2102                "Temporal.{label}: unknown unit {other:?}; use year, month, week, day, hour, minute, second, millisecond, microsecond, or nanosecond"
2103            )));
2104        }
2105    })
2106}
2107
2108pub(super) fn round_mode_from_str(mode: &str, label: &str) -> wasmtime::Result<RoundMode> {
2109    Ok(match mode {
2110        "ceil" => RoundMode::Ceil,
2111        "floor" => RoundMode::Floor,
2112        "expand" => RoundMode::Expand,
2113        "trunc" => RoundMode::Trunc,
2114        "halfCeil" => RoundMode::HalfCeil,
2115        "halfFloor" => RoundMode::HalfFloor,
2116        "halfExpand" => RoundMode::HalfExpand,
2117        "halfTrunc" => RoundMode::HalfTrunc,
2118        "halfEven" => RoundMode::HalfEven,
2119        other => {
2120            return Err(crate::runtime::host::range_error(format!(
2121                "Temporal.{label}: unknown roundingMode {other:?}; use ceil, floor, expand, trunc, halfCeil, halfFloor, halfExpand, halfTrunc, or halfEven"
2122            )));
2123        }
2124    })
2125}
2126
2127fn temporal_option(value: Option<&str>) -> String {
2128    value.map_or_else(|| "omitted".to_string(), |value| format!("{value:?}"))
2129}
2130
2131pub(super) fn string_option_description(value: Option<&str>) -> String {
2132    temporal_option(value)
2133}
2134
2135pub(super) fn i64_option_description(value: Option<i64>) -> String {
2136    value.map_or_else(|| "omitted".to_string(), |value| value.to_string())
2137}
2138
2139pub(super) fn temporal_unit_name(unit: Unit) -> &'static str {
2140    match unit {
2141        Unit::Year => "year",
2142        Unit::Month => "month",
2143        Unit::Week => "week",
2144        Unit::Day => "day",
2145        Unit::Hour => "hour",
2146        Unit::Minute => "minute",
2147        Unit::Second => "second",
2148        Unit::Millisecond => "millisecond",
2149        Unit::Microsecond => "microsecond",
2150        Unit::Nanosecond => "nanosecond",
2151    }
2152}
2153
2154fn temporal_round_mode_name(mode: RoundMode) -> &'static str {
2155    match mode {
2156        RoundMode::Ceil => "ceil",
2157        RoundMode::Floor => "floor",
2158        RoundMode::Expand => "expand",
2159        RoundMode::Trunc => "trunc",
2160        RoundMode::HalfCeil => "halfCeil",
2161        RoundMode::HalfFloor => "halfFloor",
2162        RoundMode::HalfExpand => "halfExpand",
2163        RoundMode::HalfTrunc => "halfTrunc",
2164        RoundMode::HalfEven => "halfEven",
2165        _ => "unsupported",
2166    }
2167}
2168
2169pub(super) fn round_options_description(
2170    smallest: Option<Unit>,
2171    largest: Option<Unit>,
2172    mode: Option<RoundMode>,
2173    increment: Option<i64>,
2174) -> String {
2175    let smallest = temporal_option(smallest.map(temporal_unit_name));
2176    let largest = temporal_option(largest.map(temporal_unit_name));
2177    let mode = temporal_option(mode.map(temporal_round_mode_name));
2178    let increment = i64_option_description(increment);
2179    format!(
2180        "smallestUnit={smallest}, largestUnit={largest}, roundingMode={mode}, roundingIncrement={increment}"
2181    )
2182}
2183
2184pub(super) fn duration_to_string_with_options(
2185    span: &Span,
2186    smallest: Option<&str>,
2187    mode: Option<&str>,
2188    fractional_digits: Option<u8>,
2189) -> wasmtime::Result<String> {
2190    let rounded = round_span_for_to_string(span, smallest, mode, fractional_digits)?;
2191    if let Some(digits) = fractional_digits {
2192        return format_duration_with_fractional_digits(&rounded, digits);
2193    }
2194    Ok(rounded.to_string())
2195}
2196
2197fn round_span_for_to_string(
2198    span: &Span,
2199    smallest: Option<&str>,
2200    mode: Option<&str>,
2201    fractional_digits: Option<u8>,
2202) -> wasmtime::Result<Span> {
2203    if smallest.is_none() && mode.is_none() && fractional_digits.is_none() {
2204        return Ok(*span);
2205    }
2206
2207    let mut round = SpanRound::new();
2208    let smallest_unit = smallest
2209        .map(|unit| unit_from_str(unit, "Duration.toString"))
2210        .transpose()?;
2211    let fractional_rounding = fractional_digits
2212        .map(rounding_for_fractional_second_digits)
2213        .transpose()?;
2214    let fractional_unit = fractional_rounding.map(|rounding| rounding.0);
2215    let effective_unit = match (smallest_unit, fractional_unit) {
2216        (Some(unit), Some(fractional)) => Some(more_specific_unit(unit, fractional)),
2217        (Some(unit), None) => Some(unit),
2218        (None, Some(unit)) => Some(unit),
2219        (None, None) => None,
2220    };
2221    if let Some(unit) = effective_unit {
2222        round = round.smallest(unit);
2223    }
2224    if let Some(mode) = mode {
2225        round = round.mode(round_mode_from_str(mode, "Duration.toString")?);
2226    }
2227    if let Some(digits) = fractional_digits {
2228        let (unit, increment) = rounding_for_fractional_second_digits(digits)?;
2229        if increment > 1 && Some(unit) == effective_unit {
2230            round = round.increment(increment);
2231        }
2232    }
2233    span.round(round).map_err(temporal_err("Duration.toString"))
2234}
2235
2236fn rounding_for_fractional_second_digits(digits: u8) -> wasmtime::Result<(Unit, i64)> {
2237    Ok(match digits {
2238        0 => (Unit::Second, 1),
2239        1 => (Unit::Millisecond, 100),
2240        2 => (Unit::Millisecond, 10),
2241        3 => (Unit::Millisecond, 1),
2242        4 => (Unit::Microsecond, 100),
2243        5 => (Unit::Microsecond, 10),
2244        6 => (Unit::Microsecond, 1),
2245        7 => (Unit::Nanosecond, 100),
2246        8 => (Unit::Nanosecond, 10),
2247        9 => (Unit::Nanosecond, 1),
2248        _ => {
2249            return Err(crate::runtime::host::invariant_trap(
2250                "Temporal: fractionalSecondDigits outside 0..=9",
2251            ));
2252        }
2253    })
2254}
2255
2256fn fractional_second_increment(digits: u8) -> wasmtime::Result<i64> {
2257    let exponent = 9u8.checked_sub(digits).ok_or_else(|| {
2258        crate::runtime::host::invariant_trap("Temporal: invalid fractional digits")
2259    })?;
2260    Ok(10_i64.pow(u32::from(exponent)))
2261}
2262
2263fn more_specific_unit(a: Unit, b: Unit) -> Unit {
2264    if unit_specificity(a) <= unit_specificity(b) {
2265        a
2266    } else {
2267        b
2268    }
2269}
2270
2271fn unit_specificity(unit: Unit) -> u8 {
2272    match unit {
2273        Unit::Nanosecond => 0,
2274        Unit::Microsecond => 1,
2275        Unit::Millisecond => 2,
2276        Unit::Second => 3,
2277        Unit::Minute => 4,
2278        Unit::Hour => 5,
2279        Unit::Day => 6,
2280        Unit::Week => 7,
2281        Unit::Month => 8,
2282        Unit::Year => 9,
2283    }
2284}
2285
2286fn format_duration_with_fractional_digits(span: &Span, digits: u8) -> wasmtime::Result<String> {
2287    let divisor = u128::try_from(fractional_second_increment(digits)?)
2288        .map_err(|_| crate::runtime::host::invariant_trap("Temporal: invalid fraction divisor"))?;
2289    let negative = span_fields(span).into_iter().any(|value| value < 0);
2290    let years = span.get_years().unsigned_abs();
2291    let months = span.get_months().unsigned_abs();
2292    let weeks = span.get_weeks().unsigned_abs();
2293    let days = span.get_days().unsigned_abs();
2294    let hours = span.get_hours().unsigned_abs();
2295    let minutes = span.get_minutes().unsigned_abs();
2296    let seconds = span.get_seconds().unsigned_abs();
2297    let milliseconds = span.get_milliseconds().unsigned_abs();
2298    let microseconds = span.get_microseconds().unsigned_abs();
2299    let nanoseconds = span.get_nanoseconds().unsigned_abs();
2300    let has_date = years != 0 || months != 0 || weeks != 0 || days != 0;
2301    let has_time = hours != 0
2302        || minutes != 0
2303        || seconds != 0
2304        || milliseconds != 0
2305        || microseconds != 0
2306        || nanoseconds != 0;
2307
2308    let mut out = String::new();
2309    if negative {
2310        out.push('-');
2311    }
2312    out.push('P');
2313    if years != 0 {
2314        out.push_str(&format!("{years}Y"));
2315    }
2316    if months != 0 {
2317        out.push_str(&format!("{months}M"));
2318    }
2319    if weeks != 0 {
2320        out.push_str(&format!("{weeks}W"));
2321    }
2322    if days != 0 {
2323        out.push_str(&format!("{days}D"));
2324    }
2325    if has_time || !has_date {
2326        out.push('T');
2327        if hours != 0 {
2328            out.push_str(&format!("{hours}H"));
2329        }
2330        if minutes != 0 {
2331            out.push_str(&format!("{minutes}M"));
2332        }
2333        let subsecond_nanos = u128::from(milliseconds) * 1_000_000
2334            + u128::from(microseconds) * 1_000
2335            + u128::from(nanoseconds);
2336        let include_seconds = seconds != 0 || subsecond_nanos != 0 || digits > 0 || !has_time;
2337        if include_seconds {
2338            out.push_str(&seconds.to_string());
2339            if digits > 0 {
2340                let fraction = subsecond_nanos / divisor;
2341                out.push('.');
2342                out.push_str(&format!("{fraction:0width$}", width = usize::from(digits)));
2343            }
2344            out.push('S');
2345        }
2346    }
2347    Ok(out)
2348}
2349
2350pub(super) fn span_fields(span: &Span) -> [i64; 10] {
2351    [
2352        i64::from(span.get_years()),
2353        i64::from(span.get_months()),
2354        i64::from(span.get_weeks()),
2355        i64::from(span.get_days()),
2356        i64::from(span.get_hours()),
2357        span.get_minutes(),
2358        span.get_seconds(),
2359        span.get_milliseconds(),
2360        span.get_microseconds(),
2361        span.get_nanoseconds(),
2362    ]
2363}
2364
2365pub(super) fn span_total(span: &Span, unit: &str, anchor: Option<Anchor>) -> wasmtime::Result<f64> {
2366    let unit_enum = unit_from_str(unit, "Duration.total")?;
2367    if anchor.is_none()
2368        && (super::duration::has_calendar_units(span)
2369            || super::duration::is_calendar_unit(unit_enum))
2370    {
2371        return Err(crate::runtime::host::range_error(
2372            super::duration::calendar_anchor_error("Duration.total", "compute this total"),
2373        ));
2374    }
2375    if span_fields(span).into_iter().all(|value| value == 0) {
2376        return Ok(0.0);
2377    }
2378    let total = match anchor {
2379        Some(Anchor::Zoned(z)) => span.total((unit_enum, &z)),
2380        Some(Anchor::Date(d)) => span.total((unit_enum, SpanRelativeTo::from(d))),
2381        None => {
2382            // Time/day units resolve against a sentinel UTC anchor (days = 24h,
2383            // no DST) once calendar-unit requests have been rejected above.
2384            let sentinel = civil::date(2000, 1, 1)
2385                .at(0, 0, 0, 0)
2386                .to_zoned(TimeZone::UTC)
2387                .map_err(|_| {
2388                    wasmtime::Error::msg(
2389                        "Temporal.Duration.total: internal error computing the total",
2390                    )
2391                })?;
2392            span.total((unit_enum, &sentinel))
2393        }
2394    };
2395    total.map_err(|_| {
2396        crate::runtime::host::range_error(
2397            "Temporal.Duration.total: cannot compute the total — the duration may be out of range",
2398        )
2399    })
2400}
2401
2402/// The type/interface surface this module implements — its slice of the
2403/// prelude declaration (see `declaration::prelude_package_declaration`).
2404#[allow(clippy::too_many_lines)]
2405pub(crate) fn declare_types(defs: &mut crate::PackageDeclaration) {
2406    use crate::runtime::prelude::declaration::doc;
2407    use crate::runtime::prelude::declaration::{insert_temporal_plain_type, temporal_ref};
2408    use crate::{
2409        Dispatch, FileId, MethodSig, Param, PropertySig, Span, Type, TypeKind, TypeSymbol,
2410        ValueKind, ValueSymbol,
2411    };
2412    use std::collections::BTreeMap;
2413    let temporal_prefix = crate::mangle::prelude("Temporal");
2414    let temporal_instant_ref = || temporal_ref("Instant");
2415    let temporal_duration_ref = || temporal_ref("Duration");
2416    let temporal_zdt_ref = || temporal_ref("ZonedDateTime");
2417    let temporal_duration_fields_ref = || temporal_ref("DurationFields");
2418    // The argument type of every duration-consuming arithmetic method
2419    // (`add`/`subtract` on Instant, Duration, ZonedDateTime). Accepting the
2420    // `DurationFields` options bag alongside a constructed `Duration` mirrors
2421    // the Temporal proposal's DurationLike rule; the prelude wrapper normalizes
2422    // a bag into a `Duration` via `DurationConstructor#new` (see
2423    // `emit_coerce_duration_arg` in temporal.rs).
2424    let temporal_duration_like_ref = || {
2425        Type::union(vec![
2426            temporal_duration_ref(),
2427            temporal_duration_fields_ref(),
2428        ])
2429    };
2430    let temporal_to_json_sig = || MethodSig {
2431        optional: false,
2432        generics: Vec::new(),
2433        params: Vec::new(),
2434        ret: Type::String,
2435        predicate: None,
2436        doc: doc("/** Returns the ISO string form used by JSON.stringify. */"),
2437    };
2438    let temporal_plain_date_ref = || temporal_ref("PlainDate");
2439    // `relativeTo` anchors calendar-aware Duration operations.
2440    let temporal_relative_to_ref =
2441        || Type::union(vec![temporal_plain_date_ref(), temporal_zdt_ref()]);
2442    let temporal_round_options_ref = || temporal_ref("DurationRoundOptions");
2443    let temporal_instant_round_options_ref = || temporal_ref("InstantRoundOptions");
2444    let temporal_zdt_round_options_ref = || temporal_ref("ZonedDateTimeRoundOptions");
2445    let temporal_since_until_options_ref = || temporal_ref("SinceUntilOptions");
2446    let temporal_total_options_ref = || temporal_ref("DurationTotalOptions");
2447    let temporal_compare_options_ref = || temporal_ref("DurationCompareOptions");
2448    let mut temporal = crate::NamespaceSymbol {
2449        name: "Temporal".to_string(),
2450        mangled_prefix: temporal_prefix.clone(),
2451        declaration_span: Span::at(crate::FileId::PRELUDE),
2452        values: BTreeMap::new(),
2453        types: BTreeMap::new(),
2454        namespaces: BTreeMap::new(),
2455        doc: doc(
2456            "/** Date/time primitives — subset of the JS Temporal proposal. Instant (timestamp), Duration (span), ZonedDateTime (instant + IANA tz), plus a non-deterministic `Now` sub-namespace. */",
2457        ),
2458    };
2459
2460    // ---- Temporal.Instant ----
2461    temporal.types.insert(
2462        "Instant".to_string(),
2463        TypeSymbol {
2464            name: "Temporal.Instant".to_string(),
2465            mangled_name: crate::mangle::extend(&temporal_prefix, "Instant"),
2466            declaration_span: Span::at(crate::FileId::PRELUDE),
2467            kind: TypeKind::Interface { index: None,
2468                generics: Vec::new(),
2469                methods: BTreeMap::from([
2470                    (
2471                        "add".to_string(),
2472                        MethodSig {
2473                            optional: false,
2474                            generics: Vec::new(),
2475                            params: vec![Param::new("d", temporal_duration_like_ref())],
2476                            ret: temporal_instant_ref(),
2477                            predicate: None,
2478                            doc: doc("/** Returns a new Instant `d` later. Only time-unit components contribute. `d` may be a `Temporal.Duration` or a DurationLike bag (`{ hours: 1, minutes: 30 }`). */"),
2479                        },
2480                    ),
2481                    (
2482                        "subtract".to_string(),
2483                        MethodSig {
2484                            optional: false,
2485                            generics: Vec::new(),
2486                            params: vec![Param::new("d", temporal_duration_like_ref())],
2487                            ret: temporal_instant_ref(),
2488                            predicate: None,
2489                            doc: doc("/** Returns a new Instant `d` earlier. `d` may be a `Temporal.Duration` or a DurationLike bag (`{ hours: 1, minutes: 30 }`). */"),
2490                        },
2491                    ),
2492                    (
2493                        "until".to_string(),
2494                        MethodSig {
2495                            optional: false,
2496                            generics: Vec::new(),
2497                            params: vec![
2498                                Param::new("other", temporal_instant_ref()),
2499                                Param::with_default(
2500                                    "options",
2501                                    temporal_since_until_options_ref(),
2502                                    crate::DefaultValue::Undefined,
2503                                ),
2504                            ],
2505                            ret: temporal_duration_ref(),
2506                            predicate: None,
2507                            doc: doc("/** Returns the Duration from `this` to `other` (positive if `other` is later). `options` controls unit balancing and rounding. */"),
2508                        },
2509                    ),
2510                    (
2511                        "since".to_string(),
2512                        MethodSig {
2513                            optional: false,
2514                            generics: Vec::new(),
2515                            params: vec![
2516                                Param::new("other", temporal_instant_ref()),
2517                                Param::with_default(
2518                                    "options",
2519                                    temporal_since_until_options_ref(),
2520                                    crate::DefaultValue::Undefined,
2521                                ),
2522                            ],
2523                            ret: temporal_duration_ref(),
2524                            predicate: None,
2525                            doc: doc("/** Returns the Duration from `other` to `this` (positive if `this` is later). `options` controls unit balancing and rounding. */"),
2526                        },
2527                    ),
2528                    (
2529                        "round".to_string(),
2530                        MethodSig {
2531                            optional: false,
2532                            generics: Vec::new(),
2533                            params: vec![Param::new(
2534                                "roundTo",
2535                                Type::union(vec![
2536                                    Type::String,
2537                                    temporal_instant_round_options_ref(),
2538                                ]),
2539                            )],
2540                            ret: temporal_instant_ref(),
2541                            predicate: None,
2542                            doc: doc(
2543                                "/** Rounds this Instant to the requested time unit and increment. */",
2544                            ),
2545                        },
2546                    ),
2547                    (
2548                        "toZonedDateTimeISO".to_string(),
2549                        MethodSig {
2550                            optional: false,
2551                            generics: Vec::new(),
2552                            params: vec![Param::new("tz", Type::String)],
2553                            ret: temporal_zdt_ref(),
2554                            predicate: None,
2555                            doc: doc("/** Project this Instant onto the IANA time zone `tz`. Throws on unknown zone id. */"),
2556                        },
2557                    ),
2558                    (
2559                        "toString".to_string(),
2560                        MethodSig {
2561                            optional: false,
2562                            generics: Vec::new(),
2563                            params: Vec::new(),
2564                            ret: Type::String,
2565                            predicate: None,
2566                            doc: doc(
2567                                "/** Returns the ISO 8601 representation (e.g. `\"2024-03-09T15:30:45.123Z\"`). */",
2568                            ),
2569                        },
2570                    ),
2571                    ("toJSON".to_string(), temporal_to_json_sig()),
2572                    (
2573                        "equals".to_string(),
2574                        MethodSig {
2575                            optional: false,
2576                            generics: Vec::new(),
2577                            params: vec![Param::new("other", temporal_instant_ref())],
2578                            ret: Type::Boolean,
2579                            predicate: None,
2580                            doc: doc("/** `true` if `this` and `other` refer to the same instant. */"),
2581                        },
2582                    ),
2583                ]),
2584                properties: BTreeMap::from([
2585                    (
2586                        "epochMilliseconds".to_string(),
2587                        PropertySig {
2588                            ty: Type::Number,
2589                            readonly: true,
2590                            intrinsic: false,
2591                            optional: false,
2592                            doc: doc("/** Milliseconds since the Unix epoch (1970-01-01T00:00:00Z). */"),
2593                        },
2594                    ),
2595                    (
2596                        "epochNanoseconds".to_string(),
2597                        PropertySig {
2598                            ty: Type::BigInt,
2599                            readonly: true,
2600                            intrinsic: false,
2601                            optional: false,
2602                            doc: doc("/** Nanoseconds since the Unix epoch as a `bigint` (full precision). */"),
2603                        },
2604                    ),
2605                ]),
2606                dispatch: Dispatch::Direct,
2607                doc: doc(
2608                    "/** A point in time, to nanosecond precision. Independent of any calendar or time zone — see `ZonedDateTime` for wall-clock fields. */",
2609                ),
2610            },
2611        },
2612    );
2613    temporal.types.insert(
2614        "InstantConstructor".to_string(),
2615        TypeSymbol {
2616            name: "Temporal.InstantConstructor".to_string(),
2617            mangled_name: crate::mangle::extend(&temporal_prefix, "InstantConstructor"),
2618            declaration_span: Span::at(crate::FileId::PRELUDE),
2619            kind: TypeKind::Interface { index: None,
2620                generics: Vec::new(),
2621                methods: BTreeMap::from([
2622                    (
2623                        "from".to_string(),
2624                        MethodSig {
2625                            optional: false,
2626                            generics: Vec::new(),
2627                            params: vec![Param::new("iso", Type::String)],
2628                            ret: temporal_instant_ref(),
2629                            predicate: None,
2630                            doc: doc(
2631                                "/** Parse an ISO 8601 instant string (e.g. `\"2024-03-09T15:30:45.123Z\"`). Throws on malformed input. */",
2632                            ),
2633                        },
2634                    ),
2635                    (
2636                        "fromEpochMilliseconds".to_string(),
2637                        MethodSig {
2638                            optional: false,
2639                            generics: Vec::new(),
2640                            params: vec![Param::new("ms", Type::Number)],
2641                            ret: temporal_instant_ref(),
2642                            predicate: None,
2643                            doc: doc("/** Construct an Instant from milliseconds since the Unix epoch. */"),
2644                        },
2645                    ),
2646                    (
2647                        "fromEpochNanoseconds".to_string(),
2648                        MethodSig {
2649                            optional: false,
2650                            generics: Vec::new(),
2651                            params: vec![Param::new("ns", Type::BigInt)],
2652                            ret: temporal_instant_ref(),
2653                            predicate: None,
2654                            doc: doc(
2655                                "/** Construct an Instant from nanoseconds since the Unix epoch (full precision). */",
2656                            ),
2657                        },
2658                    ),
2659                    (
2660                        "compare".to_string(),
2661                        MethodSig {
2662                            optional: false,
2663                            generics: Vec::new(),
2664                            params: vec![
2665                                Param::new("a", temporal_instant_ref()),
2666                                Param::new("b", temporal_instant_ref()),
2667                            ],
2668                            ret: Type::Number,
2669                            predicate: None,
2670                            doc: doc(
2671                                "/** Orders two Instants: -1, 0, or 1. Usable as an Array#sort comparator. */",
2672                            ),
2673                        },
2674                    ),
2675                ]),
2676                properties: BTreeMap::new(),
2677                dispatch: Dispatch::Static,
2678                doc: doc(
2679                    "/** Constructor object for `Temporal.Instant`. Accessed via the global `Temporal.Instant` binding. */",
2680                ),
2681            },
2682        },
2683    );
2684    temporal.values.insert(
2685        "Instant".to_string(),
2686        ValueSymbol {
2687            name: "Instant".to_string(),
2688            mangled_name: crate::mangle::extend(&temporal_prefix, "Instant"),
2689            declaration_span: Span::at(crate::FileId::PRELUDE),
2690            kind: ValueKind::Const {
2691                ty: temporal_ref("InstantConstructor"),
2692                doc: doc("/** The `Temporal.Instant` constructor. */"),
2693            },
2694        },
2695    );
2696
2697    let dur_field_prop = |doc_text: &'static str| PropertySig {
2698        ty: Type::Number,
2699        readonly: true,
2700        intrinsic: false,
2701        optional: true,
2702        doc: doc(doc_text),
2703    };
2704    let dur_unit_prop = |doc_text: &'static str| PropertySig {
2705        ty: Type::Number,
2706        readonly: true,
2707        intrinsic: false,
2708        optional: false,
2709        doc: doc(doc_text),
2710    };
2711    temporal.types.insert(
2712        "DurationFields".to_string(),
2713        TypeSymbol {
2714            name: "Temporal.DurationFields".to_string(),
2715            mangled_name: crate::mangle::extend(&temporal_prefix, "DurationFields"),
2716            declaration_span: Span::at(crate::FileId::PRELUDE),
2717            kind: TypeKind::Interface { index: None,
2718                generics: Vec::new(),
2719                methods: BTreeMap::new(),
2720                properties: BTreeMap::from([
2721                    ("years".to_string(), dur_field_prop("/** Calendar years. */")),
2722                    ("months".to_string(), dur_field_prop("/** Calendar months. */")),
2723                    ("weeks".to_string(), dur_field_prop("/** Calendar weeks (7 days each). */")),
2724                    ("days".to_string(), dur_field_prop("/** Calendar days. */")),
2725                    ("hours".to_string(), dur_field_prop("/** Hours. */")),
2726                    ("minutes".to_string(), dur_field_prop("/** Minutes. */")),
2727                    ("seconds".to_string(), dur_field_prop("/** Seconds. */")),
2728                    ("milliseconds".to_string(), dur_field_prop("/** Milliseconds. */")),
2729                    ("microseconds".to_string(), dur_field_prop("/** Microseconds. */")),
2730                    ("nanoseconds".to_string(), dur_field_prop("/** Nanoseconds. */")),
2731                ]),
2732                dispatch: Dispatch::VTable,
2733                doc: doc(
2734                    "/** Options bag for `new Temporal.Duration({...})`. Every field is optional and defaults to 0. */",
2735                ),
2736            },
2737        },
2738    );
2739    // Partial all-optional fields bags for `with(...)` on each Plain*/ZonedDateTime
2740    // type. Field names are singular (year/month/day/hour/minute/second/
2741    // millisecond/microsecond/nanosecond) per the JS surface.
2742    {
2743        let mut insert_fields = |local: &str, names: &[&str]| {
2744            temporal.types.insert(
2745                local.to_string(),
2746                TypeSymbol {
2747                    name: format!("Temporal.{local}"),
2748                    mangled_name: crate::mangle::extend(&temporal_prefix, local),
2749                    declaration_span: Span::at(crate::FileId::PRELUDE),
2750                    kind: TypeKind::Interface { index: None,
2751                        generics: Vec::new(),
2752                        methods: BTreeMap::new(),
2753                        properties: names
2754                            .iter()
2755                            .map(|n| ((*n).to_string(), dur_field_prop("/** Optional field; omitted fields keep the receiver's value. */")))
2756                            .collect(),
2757                        dispatch: Dispatch::VTable,
2758                        doc: doc(
2759                            "/** Partial fields bag for `with(...)`. Every field is optional. */",
2760                        ),
2761                    },
2762                },
2763            );
2764        };
2765        insert_fields("PlainDateFields", &["year", "month", "day"]);
2766        insert_fields(
2767            "PlainTimeFields",
2768            &[
2769                "hour",
2770                "minute",
2771                "second",
2772                "millisecond",
2773                "microsecond",
2774                "nanosecond",
2775            ],
2776        );
2777        insert_fields(
2778            "PlainDateTimeFields",
2779            &[
2780                "year",
2781                "month",
2782                "day",
2783                "hour",
2784                "minute",
2785                "second",
2786                "millisecond",
2787                "microsecond",
2788                "nanosecond",
2789            ],
2790        );
2791        insert_fields("PlainYearMonthFields", &["year", "month"]);
2792        insert_fields("PlainMonthDayFields", &["month", "day"]);
2793        insert_fields(
2794            "ZonedDateTimeFields",
2795            &[
2796                "year",
2797                "month",
2798                "day",
2799                "hour",
2800                "minute",
2801                "second",
2802                "millisecond",
2803                "microsecond",
2804                "nanosecond",
2805            ],
2806        );
2807        // Required-field bags for the toPlainDate completion methods.
2808        let mut insert_required = |local: &str, field: &str, field_doc: &'static str| {
2809            temporal.types.insert(
2810                local.to_string(),
2811                TypeSymbol {
2812                    name: format!("Temporal.{local}"),
2813                    mangled_name: crate::mangle::extend(&temporal_prefix, local),
2814                    declaration_span: Span::at(crate::FileId::PRELUDE),
2815                    kind: TypeKind::Interface {
2816                        index: None,
2817                        generics: Vec::new(),
2818                        methods: BTreeMap::new(),
2819                        properties: BTreeMap::from([(
2820                            field.to_string(),
2821                            PropertySig {
2822                                ty: Type::Number,
2823                                readonly: true,
2824                                intrinsic: false,
2825                                optional: false,
2826                                doc: doc(field_doc),
2827                            },
2828                        )]),
2829                        dispatch: Dispatch::VTable,
2830                        doc: doc("/** The field that completes this value into a `PlainDate`. */"),
2831                    },
2832                },
2833            );
2834        };
2835        insert_required(
2836            "PlainYearMonthToDateFields",
2837            "day",
2838            "/** Day of the month (1–31); constrained to the month length. */",
2839        );
2840        insert_required(
2841            "PlainMonthDayToDateFields",
2842            "year",
2843            "/** Calendar year; a Feb-29 month-day constrains to Feb-28 in a non-leap year. */",
2844        );
2845        // Options object for PlainDate.toZonedDateTime({ timeZone, plainTime? }).
2846        temporal.types.insert(
2847            "PlainDateToZonedOptions".to_string(),
2848            TypeSymbol {
2849                name: "Temporal.PlainDateToZonedOptions".to_string(),
2850                mangled_name: crate::mangle::extend(&temporal_prefix, "PlainDateToZonedOptions"),
2851                declaration_span: Span::at(crate::FileId::PRELUDE),
2852                kind: TypeKind::Interface {
2853                    index: None,
2854                    generics: Vec::new(),
2855                    methods: BTreeMap::new(),
2856                    properties: BTreeMap::from([
2857                        (
2858                            "timeZone".to_string(),
2859                            PropertySig {
2860                                ty: Type::String,
2861                                readonly: true,
2862                                intrinsic: false,
2863                                optional: false,
2864                                doc: doc("/** IANA time-zone id (e.g. `\"America/New_York\"`). */"),
2865                            },
2866                        ),
2867                        (
2868                            "plainTime".to_string(),
2869                            PropertySig {
2870                                ty: temporal_ref("PlainTime"),
2871                                readonly: true,
2872                                intrinsic: false,
2873                                optional: true,
2874                                doc: doc(
2875                                    "/** Wall-clock time; defaults to midnight when omitted. */",
2876                                ),
2877                            },
2878                        ),
2879                    ]),
2880                    dispatch: Dispatch::VTable,
2881                    doc: doc("/** Options for `PlainDate.toZonedDateTime`. */"),
2882                },
2883            },
2884        );
2885    }
2886    let opt_string_prop = |doc_text: &'static str| PropertySig {
2887        ty: Type::String,
2888        readonly: true,
2889        intrinsic: false,
2890        optional: true,
2891        doc: doc(doc_text),
2892    };
2893    let relative_to_prop = |optional: bool, doc_text: &'static str| PropertySig {
2894        ty: temporal_relative_to_ref(),
2895        readonly: true,
2896        intrinsic: false,
2897        optional,
2898        doc: doc(doc_text),
2899    };
2900    let opt_interface = |name: &str,
2901                         properties: BTreeMap<String, PropertySig>,
2902                         doc_text: &'static str| TypeSymbol {
2903        name: format!("Temporal.{name}"),
2904        mangled_name: crate::mangle::extend(&temporal_prefix, name),
2905        declaration_span: Span::at(FileId::PRELUDE),
2906        kind: TypeKind::Interface {
2907            index: None,
2908            generics: Vec::new(),
2909            methods: BTreeMap::new(),
2910            properties,
2911            dispatch: Dispatch::VTable,
2912            doc: doc(doc_text),
2913        },
2914    };
2915    temporal.types.insert(
2916        "InstantRoundOptions".to_string(),
2917        opt_interface(
2918            "InstantRoundOptions",
2919            BTreeMap::from([
2920                (
2921                    "smallestUnit".to_string(),
2922                    PropertySig {
2923                        ty: Type::String,
2924                        readonly: true,
2925                        intrinsic: false,
2926                        optional: false,
2927                        doc: doc(
2928                            "/** Smallest time unit to retain, from hour through nanosecond. */",
2929                        ),
2930                    },
2931                ),
2932                (
2933                    "roundingMode".to_string(),
2934                    opt_string_prop("/** Rounding mode; defaults to `\"halfExpand\"`. */"),
2935                ),
2936                (
2937                    "roundingIncrement".to_string(),
2938                    dur_field_prop("/** Positive increment of `smallestUnit`; defaults to 1. */"),
2939                ),
2940            ]),
2941            "/** Options for `Temporal.Instant.round(...)`. */",
2942        ),
2943    );
2944    temporal.types.insert(
2945        "DurationRoundOptions".to_string(),
2946        opt_interface(
2947            "DurationRoundOptions",
2948            BTreeMap::from([
2949                ("smallestUnit".to_string(), opt_string_prop("/** Smallest unit to keep (e.g. `\"second\"`). */")),
2950                ("largestUnit".to_string(), opt_string_prop("/** Largest unit to balance into (e.g. `\"hour\"`). */")),
2951                ("roundingMode".to_string(), opt_string_prop("/** `\"halfExpand\"` (default), `\"ceil\"`, `\"floor\"`, `\"trunc\"`, `\"halfEven\"`, … */")),
2952                ("roundingIncrement".to_string(), dur_field_prop("/** Round to a multiple of this increment (default 1). */")),
2953                ("relativeTo".to_string(), relative_to_prop(true, "/** Anchor (PlainDate or ZonedDateTime) — required to round calendar units. */")),
2954            ]),
2955            "/** Options for `Temporal.Duration.round(...)`. */",
2956        ),
2957    );
2958    temporal.types.insert(
2959        "ZonedDateTimeRoundOptions".to_string(),
2960        opt_interface(
2961            "ZonedDateTimeRoundOptions",
2962            BTreeMap::from([
2963                (
2964                    "smallestUnit".to_string(),
2965                    PropertySig {
2966                        ty: Type::String,
2967                        readonly: true,
2968                        intrinsic: false,
2969                        optional: false,
2970                        doc: doc(
2971                            "/** Smallest wall-clock unit to retain, from day through nanosecond. */",
2972                        ),
2973                    },
2974                ),
2975                (
2976                    "roundingMode".to_string(),
2977                    opt_string_prop("/** Rounding mode; defaults to `\"halfExpand\"`. */"),
2978                ),
2979                (
2980                    "roundingIncrement".to_string(),
2981                    dur_field_prop("/** Positive increment of `smallestUnit`; defaults to 1. */"),
2982                ),
2983            ]),
2984            "/** Options for `Temporal.ZonedDateTime.round(...)`. */",
2985        ),
2986    );
2987    temporal.types.insert(
2988        "SinceUntilOptions".to_string(),
2989        opt_interface(
2990            "SinceUntilOptions",
2991            BTreeMap::from([
2992                ("largestUnit".to_string(), opt_string_prop("/** Largest unit to balance into (e.g. `\"hours\"`, `\"months\"`). */")),
2993                ("smallestUnit".to_string(), opt_string_prop("/** Smallest unit to keep; the result is rounded to it. */")),
2994                ("roundingMode".to_string(), opt_string_prop("/** `\"halfExpand\"` (default), `\"ceil\"`, `\"floor\"`, `\"trunc\"`, `\"halfEven\"`, … */")),
2995                ("roundingIncrement".to_string(), dur_field_prop("/** Round to a multiple of this increment of `smallestUnit` (default 1). */")),
2996            ]),
2997            "/** Options for `until`/`since` on every Temporal type. The receiver anchors calendar units — no `relativeTo` needed. */",
2998        ),
2999    );
3000    temporal.types.insert(
3001        "DurationTotalOptions".to_string(),
3002        opt_interface(
3003            "DurationTotalOptions",
3004            BTreeMap::from([
3005                (
3006                    "unit".to_string(),
3007                    PropertySig {
3008                        ty: Type::String,
3009                        readonly: true,
3010                        intrinsic: false,
3011                        optional: false,
3012                        doc: doc("/** The unit to total into (e.g. `\"hours\"`, `\"days\"`). */"),
3013                    },
3014                ),
3015                (
3016                    "relativeTo".to_string(),
3017                    relative_to_prop(
3018                        true,
3019                        "/** Anchor (PlainDate or ZonedDateTime) — required for calendar units. */",
3020                    ),
3021                ),
3022            ]),
3023            "/** Options for `Temporal.Duration.prototype.total(...)`. */",
3024        ),
3025    );
3026    temporal.types.insert(
3027        "DurationToStringOptions".to_string(),
3028        opt_interface(
3029            "DurationToStringOptions",
3030            BTreeMap::from([
3031                (
3032                    "smallestUnit".to_string(),
3033                    opt_string_prop("/** Smallest unit to keep while formatting. */"),
3034                ),
3035                (
3036                    "roundingMode".to_string(),
3037                    opt_string_prop(
3038                        "/** `\"trunc\"` (default), `\"ceil\"`, `\"floor\"`, `\"halfExpand\"`, … */",
3039                    ),
3040                ),
3041                (
3042                    "fractionalSecondDigits".to_string(),
3043                    dur_field_prop(
3044                        "/** Exact fractional second digits to print, from 0 through 9. */",
3045                    ),
3046                ),
3047            ]),
3048            "/** Options for `Temporal.Duration.prototype.toString(...)`. */",
3049        ),
3050    );
3051    temporal.types.insert(
3052        "DurationCompareOptions".to_string(),
3053        opt_interface(
3054            "DurationCompareOptions",
3055            BTreeMap::from([(
3056                "relativeTo".to_string(),
3057                relative_to_prop(true, "/** Anchor (PlainDate or ZonedDateTime) — required to compare calendar durations. */"),
3058            )]),
3059            "/** Options for `Temporal.Duration.compare(...)`. */",
3060        ),
3061    );
3062    temporal.types.insert(
3063        "Duration".to_string(),
3064        TypeSymbol {
3065            name: "Temporal.Duration".to_string(),
3066            mangled_name: crate::mangle::extend(&temporal_prefix, "Duration"),
3067            declaration_span: Span::at(crate::FileId::PRELUDE),
3068            kind: TypeKind::Interface { index: None,
3069                generics: Vec::new(),
3070                methods: BTreeMap::from([
3071                    (
3072                        "add".to_string(),
3073                        MethodSig {
3074                            optional: false,
3075                            generics: Vec::new(),
3076                            params: vec![Param::new("d", temporal_duration_like_ref())],
3077                            ret: temporal_duration_ref(),
3078                            predicate: None,
3079                            doc: doc("/** Returns `this + d` as a new Duration. `d` may be a `Temporal.Duration` or a DurationLike bag (`{ hours: 1, minutes: 30 }`). */"),
3080                        },
3081                    ),
3082                    (
3083                        "subtract".to_string(),
3084                        MethodSig {
3085                            optional: false,
3086                            generics: Vec::new(),
3087                            params: vec![Param::new("d", temporal_duration_like_ref())],
3088                            ret: temporal_duration_ref(),
3089                            predicate: None,
3090                            doc: doc("/** Returns `this - d` as a new Duration. `d` may be a `Temporal.Duration` or a DurationLike bag (`{ hours: 1, minutes: 30 }`). */"),
3091                        },
3092                    ),
3093                    (
3094                        "negated".to_string(),
3095                        MethodSig {
3096                            optional: false,
3097                            generics: Vec::new(),
3098                            params: Vec::new(),
3099                            ret: temporal_duration_ref(),
3100                            predicate: None,
3101                            doc: doc("/** Returns a Duration with every field's sign flipped. */"),
3102                        },
3103                    ),
3104                    (
3105                        "total".to_string(),
3106                        MethodSig {
3107                            optional: false,
3108                            generics: Vec::new(),
3109                            params: vec![Param::new(
3110                                "totalOf",
3111                                Type::union(vec![Type::String, temporal_total_options_ref()]),
3112                            )],
3113                            ret: Type::Number,
3114                            predicate: None,
3115                            doc: doc(
3116                                "/** Total length of this Duration in a unit — `total(\"hours\")` or `total({ unit, relativeTo })`. Calendar units (`\"weeks\"`/`\"months\"`/`\"years\"`) require a `relativeTo` anchor (a PlainDate or ZonedDateTime). */",
3117                            ),
3118                        },
3119                    ),
3120                    (
3121                        "round".to_string(),
3122                        MethodSig {
3123                            optional: false,
3124                            generics: Vec::new(),
3125                            params: vec![Param::new(
3126                                "roundTo",
3127                                Type::union(vec![
3128                                    Type::String,
3129                                    temporal_round_options_ref(),
3130                                ]),
3131                            )],
3132                            ret: temporal_duration_ref(),
3133                            predicate: None,
3134                            doc: doc(
3135                                "/** Rounds this Duration — `round(\"hour\")` (smallestUnit shorthand) or `round({ smallestUnit, largestUnit, roundingMode, roundingIncrement, relativeTo })`. Calendar units need a `relativeTo` anchor. */",
3136                            ),
3137                        },
3138                    ),
3139                    (
3140                        "toString".to_string(),
3141                        MethodSig {
3142                            optional: false,
3143                            generics: Vec::new(),
3144                            params: vec![Param::with_default(
3145                                "options",
3146                                temporal_ref("DurationToStringOptions"),
3147                                crate::DefaultValue::Undefined,
3148                            )],
3149                            ret: Type::String,
3150                            predicate: None,
3151                            doc: doc("/** Returns the ISO 8601 duration form (e.g. `\"PT1H30M\"`), optionally rounded/formatted. */"),
3152                        },
3153                    ),
3154                    ("toJSON".to_string(), temporal_to_json_sig()),
3155                    (
3156                        "with".to_string(),
3157                        MethodSig {
3158                            optional: false,
3159                            generics: Vec::new(),
3160                            params: vec![Param::new("fields", temporal_duration_like_ref())],
3161                            ret: temporal_duration_ref(),
3162                            predicate: None,
3163                            doc: doc(
3164                                "/** Returns a copy of this Duration with the given slots replaced (e.g. `d.with({ hours: 5 })`); unlisted slots are kept. */",
3165                            ),
3166                        },
3167                    ),
3168                    (
3169                        "abs".to_string(),
3170                        MethodSig {
3171                            optional: false,
3172                            generics: Vec::new(),
3173                            params: Vec::new(),
3174                            ret: temporal_duration_ref(),
3175                            predicate: None,
3176                            doc: doc(
3177                                "/** Returns a Duration with every slot's absolute value (a non-negative Duration). */",
3178                            ),
3179                        },
3180                    ),
3181                ]),
3182                properties: BTreeMap::from([
3183                    ("years".to_string(), dur_unit_prop("/** Calendar-years slot. */")),
3184                    ("months".to_string(), dur_unit_prop("/** Calendar-months slot. */")),
3185                    ("weeks".to_string(), dur_unit_prop("/** Calendar-weeks slot. */")),
3186                    ("days".to_string(), dur_unit_prop("/** Calendar-days slot. */")),
3187                    ("hours".to_string(), dur_unit_prop("/** Hours slot. */")),
3188                    ("minutes".to_string(), dur_unit_prop("/** Minutes slot. */")),
3189                    ("seconds".to_string(), dur_unit_prop("/** Seconds slot. */")),
3190                    ("milliseconds".to_string(), dur_unit_prop("/** Milliseconds slot. */")),
3191                    ("microseconds".to_string(), dur_unit_prop("/** Microseconds slot. */")),
3192                    ("nanoseconds".to_string(), dur_unit_prop("/** Nanoseconds slot. */")),
3193                    (
3194                        "sign".to_string(),
3195                        dur_unit_prop(
3196                            "/** `-1`, `0`, or `1`: the Duration's overall direction (every slot shares this sign). */",
3197                        ),
3198                    ),
3199                    (
3200                        "blank".to_string(),
3201                        PropertySig {
3202                            ty: Type::Boolean,
3203                            readonly: true,
3204                            intrinsic: false,
3205                            optional: false,
3206                            doc: doc(
3207                                "/** `true` when every slot is `0` (the Duration is empty / `sign === 0`). */",
3208                            ),
3209                        },
3210                    ),
3211                ]),
3212                dispatch: Dispatch::Direct,
3213                doc: doc(
3214                    "/** A span of time, expressed as separate calendar (`years`/`months`/`weeks`/`days`) and time (`hours`/…/`nanoseconds`) unit slots. Field reads return the literal slot value — use `total(unit)` for the computed total. */",
3215                ),
3216            },
3217        },
3218    );
3219    temporal.types.insert(
3220        "DurationConstructor".to_string(),
3221        TypeSymbol {
3222            name: "Temporal.DurationConstructor".to_string(),
3223            mangled_name: crate::mangle::extend(&temporal_prefix, "DurationConstructor"),
3224            declaration_span: Span::at(crate::FileId::PRELUDE),
3225            kind: TypeKind::Interface { index: None,
3226                generics: Vec::new(),
3227                methods: BTreeMap::from([
3228                    (
3229                        "new".to_string(),
3230                        MethodSig {
3231                            optional: false,
3232                            generics: Vec::new(),
3233                            params: vec![Param::new("fields", temporal_duration_fields_ref())],
3234                            ret: temporal_duration_ref(),
3235                            predicate: None,
3236                            doc: doc(
3237                                "/** Construct a Duration from a fields bag (e.g. `{ hours: 1, minutes: 30 }`). Every field defaults to 0. */",
3238                            ),
3239                        },
3240                    ),
3241                    (
3242                        "from".to_string(),
3243                        MethodSig {
3244                            optional: false,
3245                            generics: Vec::new(),
3246                            params: vec![Param::new(
3247                                "item",
3248                                Type::union(vec![
3249                                    Type::String,
3250                                    temporal_duration_ref(),
3251                                    temporal_duration_fields_ref(),
3252                                ]),
3253                            )],
3254                            ret: temporal_duration_ref(),
3255                            predicate: None,
3256                            doc: doc(
3257                                "/** Build a Duration from an ISO 8601 string (e.g. `\"PT1H30M\"`), an existing Duration, or a fields bag (`{ hours: 1 }`). */",
3258                            ),
3259                        },
3260                    ),
3261                    (
3262                        "compare".to_string(),
3263                        MethodSig {
3264                            optional: false,
3265                            generics: Vec::new(),
3266                            params: vec![
3267                                Param::new("a", temporal_duration_like_ref()),
3268                                Param::new("b", temporal_duration_like_ref()),
3269                                Param::with_default(
3270                                    "options",
3271                                    temporal_compare_options_ref(),
3272                                    crate::DefaultValue::Undefined,
3273                                ),
3274                            ],
3275                            ret: Type::Number,
3276                            predicate: None,
3277                            doc: doc(
3278                                "/** Compares two Durations: `-1`, `0`, or `1`. Calendar durations need `options.relativeTo` (a PlainDate or ZonedDateTime). */",
3279                            ),
3280                        },
3281                    ),
3282                ]),
3283                properties: BTreeMap::new(),
3284                dispatch: Dispatch::Static,
3285                doc: doc(
3286                    "/** Constructor object for `Temporal.Duration`. Accessed via the global `Temporal.Duration` binding. */",
3287                ),
3288            },
3289        },
3290    );
3291    temporal.values.insert(
3292        "Duration".to_string(),
3293        ValueSymbol {
3294            name: "Duration".to_string(),
3295            mangled_name: crate::mangle::extend(&temporal_prefix, "Duration"),
3296            declaration_span: Span::at(crate::FileId::PRELUDE),
3297            kind: ValueKind::Const {
3298                ty: temporal_ref("DurationConstructor"),
3299                doc: doc("/** The `Temporal.Duration` constructor. */"),
3300            },
3301        },
3302    );
3303
3304    let zdt_field_prop = |doc_text: &'static str| PropertySig {
3305        ty: Type::Number,
3306        readonly: true,
3307        intrinsic: false,
3308        optional: false,
3309        doc: doc(doc_text),
3310    };
3311    temporal.types.insert(
3312        "ZonedDateTime".to_string(),
3313        TypeSymbol {
3314            name: "Temporal.ZonedDateTime".to_string(),
3315            mangled_name: crate::mangle::extend(&temporal_prefix, "ZonedDateTime"),
3316            declaration_span: Span::at(crate::FileId::PRELUDE),
3317            kind: TypeKind::Interface { index: None,
3318                generics: Vec::new(),
3319                methods: BTreeMap::from([
3320                    (
3321                        "add".to_string(),
3322                        MethodSig {
3323                            optional: false,
3324                            generics: Vec::new(),
3325                            params: vec![Param::new("d", temporal_duration_like_ref())],
3326                            ret: temporal_zdt_ref(),
3327                            predicate: None,
3328                            doc: doc(
3329                                "/** Returns a new ZonedDateTime `d` later. Calendar-aware (DST transitions respected). `d` may be a `Temporal.Duration` or a DurationLike bag (`{ hours: 1, minutes: 30 }`). */",
3330                            ),
3331                        },
3332                    ),
3333                    (
3334                        "subtract".to_string(),
3335                        MethodSig {
3336                            optional: false,
3337                            generics: Vec::new(),
3338                            params: vec![Param::new("d", temporal_duration_like_ref())],
3339                            ret: temporal_zdt_ref(),
3340                            predicate: None,
3341                            doc: doc("/** Returns a new ZonedDateTime `d` earlier. `d` may be a `Temporal.Duration` or a DurationLike bag (`{ hours: 1, minutes: 30 }`). */"),
3342                        },
3343                    ),
3344                    (
3345                        "until".to_string(),
3346                        MethodSig {
3347                            optional: false,
3348                            generics: Vec::new(),
3349                            params: vec![
3350                                Param::new("other", temporal_zdt_ref()),
3351                                Param::with_default(
3352                                    "options",
3353                                    temporal_since_until_options_ref(),
3354                                    crate::DefaultValue::Undefined,
3355                                ),
3356                            ],
3357                            ret: temporal_duration_ref(),
3358                            predicate: None,
3359                            doc: doc("/** Returns the calendar-aware Duration from `this` to `other`. `options` controls unit balancing and rounding. */"),
3360                        },
3361                    ),
3362                    (
3363                        "since".to_string(),
3364                        MethodSig {
3365                            optional: false,
3366                            generics: Vec::new(),
3367                            params: vec![
3368                                Param::new("other", temporal_zdt_ref()),
3369                                Param::with_default(
3370                                    "options",
3371                                    temporal_since_until_options_ref(),
3372                                    crate::DefaultValue::Undefined,
3373                                ),
3374                            ],
3375                            ret: temporal_duration_ref(),
3376                            predicate: None,
3377                            doc: doc("/** Returns the calendar-aware Duration from `other` to `this`. `options` controls unit balancing and rounding. */"),
3378                        },
3379                    ),
3380                    (
3381                        "withTimeZone".to_string(),
3382                        MethodSig {
3383                            optional: false,
3384                            generics: Vec::new(),
3385                            params: vec![Param::new("tz", Type::String)],
3386                            ret: temporal_zdt_ref(),
3387                            predicate: None,
3388                            doc: doc(
3389                                "/** Same instant, different IANA time zone. Wall-clock fields shift accordingly. Throws on unknown zone id. */",
3390                            ),
3391                        },
3392                    ),
3393                    (
3394                        "with".to_string(),
3395                        MethodSig {
3396                            optional: false,
3397                            generics: Vec::new(),
3398                            params: vec![Param::new(
3399                                "fields",
3400                                temporal_ref("ZonedDateTimeFields"),
3401                            )],
3402                            ret: temporal_zdt_ref(),
3403                            predicate: None,
3404                            doc: doc(
3405                                "/** Returns a copy with the listed wall-clock fields replaced (keeping the time zone); unlisted fields are kept. Out-of-range values are constrained. */",
3406                            ),
3407                        },
3408                    ),
3409                    (
3410                        "round".to_string(),
3411                        MethodSig {
3412                            optional: false,
3413                            generics: Vec::new(),
3414                            params: vec![Param::new(
3415                                "roundTo",
3416                                Type::union(vec![
3417                                    Type::String,
3418                                    temporal_zdt_round_options_ref(),
3419                                ]),
3420                            )],
3421                            ret: temporal_zdt_ref(),
3422                            predicate: None,
3423                            doc: doc(
3424                                "/** Rounds this ZonedDateTime in its time zone, from day through nanosecond precision. */",
3425                            ),
3426                        },
3427                    ),
3428                    (
3429                        "startOfDay".to_string(),
3430                        MethodSig {
3431                            optional: false,
3432                            generics: Vec::new(),
3433                            params: Vec::new(),
3434                            ret: temporal_zdt_ref(),
3435                            predicate: None,
3436                            doc: doc(
3437                                "/** Returns the first valid instant on this date in the current time zone. */",
3438                            ),
3439                        },
3440                    ),
3441                    (
3442                        "toInstant".to_string(),
3443                        MethodSig {
3444                            optional: false,
3445                            generics: Vec::new(),
3446                            params: Vec::new(),
3447                            ret: temporal_instant_ref(),
3448                            predicate: None,
3449                            doc: doc("/** Drops the time-zone, returns the underlying Instant. */"),
3450                        },
3451                    ),
3452                    (
3453                        "toPlainDate".to_string(),
3454                        MethodSig {
3455                            optional: false,
3456                            generics: Vec::new(),
3457                            params: Vec::new(),
3458                            ret: temporal_plain_date_ref(),
3459                            predicate: None,
3460                            doc: doc(
3461                                "/** The wall-clock calendar date in this time zone, as a tz-free `PlainDate`. */",
3462                            ),
3463                        },
3464                    ),
3465                    (
3466                        "toPlainTime".to_string(),
3467                        MethodSig {
3468                            optional: false,
3469                            generics: Vec::new(),
3470                            params: Vec::new(),
3471                            ret: temporal_ref("PlainTime"),
3472                            predicate: None,
3473                            doc: doc(
3474                                "/** The wall-clock time in this time zone, as a tz-free `PlainTime` (with nanosecond precision). */",
3475                            ),
3476                        },
3477                    ),
3478                    (
3479                        "toPlainDateTime".to_string(),
3480                        MethodSig {
3481                            optional: false,
3482                            generics: Vec::new(),
3483                            params: Vec::new(),
3484                            ret: temporal_ref("PlainDateTime"),
3485                            predicate: None,
3486                            doc: doc(
3487                                "/** The wall-clock date and time in this time zone, as a tz-free `PlainDateTime`. */",
3488                            ),
3489                        },
3490                    ),
3491                    (
3492                        "toString".to_string(),
3493                        MethodSig {
3494                            optional: false,
3495                            generics: Vec::new(),
3496                            params: Vec::new(),
3497                            ret: Type::String,
3498                            predicate: None,
3499                            doc: doc(
3500                                "/** Returns the ISO 8601 form with bracketed tz (e.g. `\"2024-03-09T12:00:00+00:00[UTC]\"`). */",
3501                            ),
3502                        },
3503                    ),
3504                    ("toJSON".to_string(), temporal_to_json_sig()),
3505                    (
3506                        "equals".to_string(),
3507                        MethodSig {
3508                            optional: false,
3509                            generics: Vec::new(),
3510                            params: vec![Param::new("other", temporal_zdt_ref())],
3511                            ret: Type::Boolean,
3512                            predicate: None,
3513                            doc: doc(
3514                                "/** `true` if `this` and `other` have the same instant **and** the same time-zone id. */",
3515                            ),
3516                        },
3517                    ),
3518                ]),
3519                properties: BTreeMap::from([
3520                    (
3521                        "timeZoneId".to_string(),
3522                        PropertySig {
3523                            ty: Type::String,
3524                            readonly: true,
3525                            intrinsic: false,
3526                            optional: false,
3527                            doc: doc("/** The IANA time-zone id (e.g. `\"America/New_York\"`). */"),
3528                        },
3529                    ),
3530                    (
3531                        "epochMilliseconds".to_string(),
3532                        PropertySig {
3533                            ty: Type::Number,
3534                            readonly: true,
3535                            intrinsic: false,
3536                            optional: false,
3537                            doc: doc("/** Milliseconds since the Unix epoch (timezone-independent). */"),
3538                        },
3539                    ),
3540                    (
3541                        "epochNanoseconds".to_string(),
3542                        PropertySig {
3543                            ty: Type::BigInt,
3544                            readonly: true,
3545                            intrinsic: false,
3546                            optional: false,
3547                            doc: doc("/** Nanoseconds since the Unix epoch. */"),
3548                        },
3549                    ),
3550                    (
3551                        "offset".to_string(),
3552                        PropertySig {
3553                            ty: Type::String,
3554                            readonly: true,
3555                            intrinsic: false,
3556                            optional: false,
3557                            doc: doc("/** UTC offset at this instant, such as `\"-05:00\"`. */"),
3558                        },
3559                    ),
3560                    (
3561                        "offsetNanoseconds".to_string(),
3562                        zdt_field_prop("/** UTC offset at this instant in nanoseconds. */"),
3563                    ),
3564                    (
3565                        "hoursInDay".to_string(),
3566                        zdt_field_prop(
3567                            "/** Elapsed hours in this local day, accounting for time-zone transitions. */",
3568                        ),
3569                    ),
3570                    ("year".to_string(), zdt_field_prop("/** Calendar year in this time zone. */")),
3571                    ("month".to_string(), zdt_field_prop("/** Calendar month (1–12). */")),
3572                    ("day".to_string(), zdt_field_prop("/** Day of the month (1–31). */")),
3573                    ("hour".to_string(), zdt_field_prop("/** Hour of the day (0–23). */")),
3574                    ("minute".to_string(), zdt_field_prop("/** Minute of the hour (0–59). */")),
3575                    ("second".to_string(), zdt_field_prop("/** Second of the minute (0–59 or 60 on a leap-second). */")),
3576                    (
3577                        "dayOfWeek".to_string(),
3578                        zdt_field_prop("/** Day of the week, 1 (Monday) through 7 (Sunday). */"),
3579                    ),
3580                    (
3581                        "monthCode".to_string(),
3582                        PropertySig {
3583                            ty: Type::String,
3584                            readonly: true,
3585                            intrinsic: false,
3586                            optional: false,
3587                            doc: doc("/** ISO month code, e.g. `\"M06\"`. */"),
3588                        },
3589                    ),
3590                    ("dayOfYear".to_string(), zdt_field_prop("/** Day of the year (1–365 or 366). */")),
3591                    ("weekOfYear".to_string(), zdt_field_prop("/** ISO week number. */")),
3592                    ("yearOfWeek".to_string(), zdt_field_prop("/** ISO week-numbering year. */")),
3593                    ("daysInWeek".to_string(), zdt_field_prop("/** Days in this ISO week. */")),
3594                    ("daysInMonth".to_string(), zdt_field_prop("/** Days in this month. */")),
3595                    ("daysInYear".to_string(), zdt_field_prop("/** Days in this year. */")),
3596                    ("monthsInYear".to_string(), zdt_field_prop("/** Months in this year. */")),
3597                    (
3598                        "inLeapYear".to_string(),
3599                        PropertySig {
3600                            ty: Type::Boolean,
3601                            readonly: true,
3602                            intrinsic: false,
3603                            optional: false,
3604                            doc: doc("/** True when the ISO calendar year is a leap year. */"),
3605                        },
3606                    ),
3607                    ("millisecond".to_string(), zdt_field_prop("/** Millisecond within the second (0–999). */")),
3608                    ("microsecond".to_string(), zdt_field_prop("/** Microsecond within the millisecond (0–999). */")),
3609                    ("nanosecond".to_string(), zdt_field_prop("/** Nanosecond within the microsecond (0–999). */")),
3610                ]),
3611                dispatch: Dispatch::Direct,
3612                doc: doc(
3613                    "/** An Instant paired with an IANA time zone. Wall-clock fields (`year`/`month`/…/`dayOfWeek`) are computed in the tz. Arithmetic is DST-aware. */",
3614                ),
3615            },
3616        },
3617    );
3618    temporal.types.insert(
3619        "ZonedDateTimeConstructor".to_string(),
3620        TypeSymbol {
3621            name: "Temporal.ZonedDateTimeConstructor".to_string(),
3622            mangled_name: crate::mangle::extend(&temporal_prefix, "ZonedDateTimeConstructor"),
3623            declaration_span: Span::at(crate::FileId::PRELUDE),
3624            kind: TypeKind::Interface { index: None,
3625                generics: Vec::new(),
3626                methods: BTreeMap::from([
3627                    (
3628                        "from".to_string(),
3629                        MethodSig {
3630                            optional: false,
3631                            generics: Vec::new(),
3632                            params: vec![Param::new("iso", Type::String)],
3633                            ret: temporal_zdt_ref(),
3634                            predicate: None,
3635                            doc: doc(
3636                                "/** Parse an ISO 8601 zoned datetime with bracketed tz (e.g. `\"2026-01-15T10:00:00-05:00[America/New_York]\"`). Throws on malformed input or unknown zone id. */",
3637                            ),
3638                        },
3639                    ),
3640                    (
3641                        "compare".to_string(),
3642                        MethodSig {
3643                            optional: false,
3644                            generics: Vec::new(),
3645                            params: vec![
3646                                Param::new("a", temporal_zdt_ref()),
3647                                Param::new("b", temporal_zdt_ref()),
3648                            ],
3649                            ret: Type::Number,
3650                            predicate: None,
3651                            doc: doc(
3652                                "/** Orders two ZonedDateTimes by their exact instant: -1, 0, or 1. Usable as an Array#sort comparator. */",
3653                            ),
3654                        },
3655                    ),
3656                ]),
3657                properties: BTreeMap::new(),
3658                dispatch: Dispatch::Static,
3659                doc: doc("/** Constructor object for `Temporal.ZonedDateTime`. */"),
3660            },
3661        },
3662    );
3663    temporal.values.insert(
3664        "ZonedDateTime".to_string(),
3665        ValueSymbol {
3666            name: "ZonedDateTime".to_string(),
3667            mangled_name: crate::mangle::extend(&temporal_prefix, "ZonedDateTime"),
3668            declaration_span: Span::at(crate::FileId::PRELUDE),
3669            kind: ValueKind::Const {
3670                ty: temporal_ref("ZonedDateTimeConstructor"),
3671                doc: doc("/** The `Temporal.ZonedDateTime` constructor. */"),
3672            },
3673        },
3674    );
3675
3676    // ---- Plain* family (calendar/clock-only, ISO-8601, no time zone) ----
3677    let temporal_plain_date_ref = || temporal_ref("PlainDate");
3678    let temporal_plain_time_ref = || temporal_ref("PlainTime");
3679    // The four arithmetic instance methods shared by every Plain* type that
3680    // supports them: `add`/`subtract` take a `Duration | DurationLike` and return
3681    // the same type; `until`/`since` take another value and return a `Duration`.
3682    let arith_methods = |self_ty: Type| -> Vec<(String, MethodSig)> {
3683        vec![
3684            (
3685                "add".to_string(),
3686                MethodSig {
3687                    optional: false,
3688                    generics: Vec::new(),
3689                    params: vec![Param::new("duration", temporal_duration_like_ref())],
3690                    ret: self_ty.clone(),
3691                    predicate: None,
3692                    doc: doc("/** Returns a new value with `duration` added. */"),
3693                },
3694            ),
3695            (
3696                "subtract".to_string(),
3697                MethodSig {
3698                    optional: false,
3699                    generics: Vec::new(),
3700                    params: vec![Param::new("duration", temporal_duration_like_ref())],
3701                    ret: self_ty.clone(),
3702                    predicate: None,
3703                    doc: doc("/** Returns a new value with `duration` subtracted. */"),
3704                },
3705            ),
3706            (
3707                "until".to_string(),
3708                MethodSig {
3709                    optional: false,
3710                    generics: Vec::new(),
3711                    params: vec![
3712                        Param::new("other", self_ty.clone()),
3713                        Param::with_default(
3714                            "options",
3715                            temporal_since_until_options_ref(),
3716                            crate::DefaultValue::Undefined,
3717                        ),
3718                    ],
3719                    ret: temporal_duration_ref(),
3720                    predicate: None,
3721                    doc: doc(
3722                        "/** The `Duration` from `this` until `other`. `options` controls unit balancing and rounding. */",
3723                    ),
3724                },
3725            ),
3726            (
3727                "since".to_string(),
3728                MethodSig {
3729                    optional: false,
3730                    generics: Vec::new(),
3731                    params: vec![
3732                        Param::new("other", self_ty.clone()),
3733                        Param::with_default(
3734                            "options",
3735                            temporal_since_until_options_ref(),
3736                            crate::DefaultValue::Undefined,
3737                        ),
3738                    ],
3739                    ret: temporal_duration_ref(),
3740                    predicate: None,
3741                    doc: doc(
3742                        "/** The `Duration` from `other` since which `this` occurs. `options` controls unit balancing and rounding. */",
3743                    ),
3744                },
3745            ),
3746        ]
3747    };
3748    let temporal_plain_date_time_ref = || temporal_ref("PlainDateTime");
3749    let temporal_plain_year_month_ref = || temporal_ref("PlainYearMonth");
3750    // `with(fields): Self` — `fields_name` is the matching `…Fields` bag type.
3751    let with_method = |self_ty: Type, fields_name: &'static str| -> (String, MethodSig) {
3752        (
3753            "with".to_string(),
3754            MethodSig {
3755                optional: false,
3756                generics: Vec::new(),
3757                params: vec![Param::new("fields", temporal_ref(fields_name))],
3758                ret: self_ty,
3759                predicate: None,
3760                doc: doc(
3761                    "/** Returns a copy with the listed fields replaced; unlisted fields are kept. Out-of-range values are constrained. */",
3762                ),
3763            },
3764        )
3765    };
3766    let temporal_plain_month_day_ref = || temporal_ref("PlainMonthDay");
3767    // Projections onto the coarser carriers, shared by PlainDate and PlainDateTime.
3768    let to_year_month_method = || {
3769        (
3770            "toPlainYearMonth".to_string(),
3771            MethodSig {
3772                optional: false,
3773                generics: Vec::new(),
3774                params: Vec::new(),
3775                ret: temporal_plain_year_month_ref(),
3776                predicate: None,
3777                doc: doc("/** The year-and-month part as a `PlainYearMonth`. */"),
3778            },
3779        )
3780    };
3781    let to_month_day_method = || {
3782        (
3783            "toPlainMonthDay".to_string(),
3784            MethodSig {
3785                optional: false,
3786                generics: Vec::new(),
3787                params: Vec::new(),
3788                ret: temporal_plain_month_day_ref(),
3789                predicate: None,
3790                doc: doc("/** The month-and-day part as a `PlainMonthDay`. */"),
3791            },
3792        )
3793    };
3794    insert_temporal_plain_type(
3795        &mut temporal,
3796        &temporal_prefix,
3797        "PlainDate",
3798        "/** A calendar date (year, month, day) with no time or time zone. ISO-8601 proleptic Gregorian. */",
3799        &[
3800            ("year", "/** Calendar year. */"),
3801            ("month", "/** Calendar month (1–12). */"),
3802            ("day", "/** Day of the month (1–31). */"),
3803            (
3804                "dayOfWeek",
3805                "/** Day of the week, 1 (Monday) through 7 (Sunday). */",
3806            ),
3807            ("monthCode", "/** ISO month code, e.g. `\"M06\"`. */"),
3808            ("dayOfYear", "/** Day of the year (1–365 or 366). */"),
3809            ("weekOfYear", "/** ISO week number. */"),
3810            ("yearOfWeek", "/** ISO week-numbering year. */"),
3811            ("daysInWeek", "/** Days in this ISO week. */"),
3812            ("daysInMonth", "/** Days in this month. */"),
3813            ("daysInYear", "/** Days in this year. */"),
3814            ("monthsInYear", "/** Months in this year. */"),
3815            (
3816                "inLeapYear",
3817                "/** True when the ISO calendar year is a leap year. */",
3818            ),
3819        ],
3820        {
3821            let mut m = arith_methods(temporal_plain_date_ref());
3822            m.push(with_method(temporal_plain_date_ref(), "PlainDateFields"));
3823            m.push(to_year_month_method());
3824            m.push(to_month_day_method());
3825            m.push((
3826                "toPlainDateTime".to_string(),
3827                MethodSig {
3828                    optional: false,
3829                    generics: Vec::new(),
3830                    params: vec![Param::with_default(
3831                        "time",
3832                        temporal_plain_time_ref(),
3833                        crate::DefaultValue::Undefined,
3834                    )],
3835                    ret: temporal_plain_date_time_ref(),
3836                    predicate: None,
3837                    doc: doc(
3838                        "/** Combines this date with `time` (default midnight) into a `PlainDateTime`. */",
3839                    ),
3840                },
3841            ));
3842            m.push((
3843                "toZonedDateTime".to_string(),
3844                MethodSig {
3845                    optional: false,
3846                    generics: Vec::new(),
3847                    params: vec![Param::new(
3848                        "timeZoneOrOptions",
3849                        Type::Union(vec![
3850                            Type::String,
3851                            temporal_ref("PlainDateToZonedOptions"),
3852                        ]),
3853                    )],
3854                    ret: temporal_zdt_ref(),
3855                    predicate: None,
3856                    doc: doc(
3857                        "/** Interprets this date in a time zone — pass a zone id (midnight) or `{ timeZone, plainTime? }`. Resolves DST gaps with `compatible` disambiguation. */",
3858                    ),
3859                },
3860            ));
3861            m
3862        },
3863        true,
3864    );
3865    insert_temporal_plain_type(
3866        &mut temporal,
3867        &temporal_prefix,
3868        "PlainTime",
3869        "/** A wall-clock time (hour, minute, second, nanosecond) with no date or time zone. */",
3870        &[
3871            ("hour", "/** Hour of the day (0–23). */"),
3872            ("minute", "/** Minute of the hour (0–59). */"),
3873            ("second", "/** Second of the minute (0–59). */"),
3874            (
3875                "millisecond",
3876                "/** Millisecond within the second (0–999). */",
3877            ),
3878            (
3879                "microsecond",
3880                "/** Microsecond within the millisecond (0–999). */",
3881            ),
3882            (
3883                "nanosecond",
3884                "/** Nanoseconds within the second (0–999_999_999). */",
3885            ),
3886        ],
3887        {
3888            let mut m = arith_methods(temporal_plain_time_ref());
3889            m.push(with_method(temporal_plain_time_ref(), "PlainTimeFields"));
3890            m
3891        },
3892        true,
3893    );
3894    insert_temporal_plain_type(
3895        &mut temporal,
3896        &temporal_prefix,
3897        "PlainDateTime",
3898        "/** A calendar date and wall-clock time with no time zone. */",
3899        &[
3900            ("year", "/** Calendar year. */"),
3901            ("month", "/** Calendar month (1–12). */"),
3902            ("day", "/** Day of the month (1–31). */"),
3903            ("hour", "/** Hour of the day (0–23). */"),
3904            ("minute", "/** Minute of the hour (0–59). */"),
3905            ("second", "/** Second of the minute (0–59). */"),
3906            (
3907                "dayOfWeek",
3908                "/** Day of the week, 1 (Monday) through 7 (Sunday). */",
3909            ),
3910            ("monthCode", "/** ISO month code, e.g. `\"M06\"`. */"),
3911            ("dayOfYear", "/** Day of the year (1–365 or 366). */"),
3912            ("weekOfYear", "/** ISO week number. */"),
3913            ("yearOfWeek", "/** ISO week-numbering year. */"),
3914            ("daysInWeek", "/** Days in this ISO week. */"),
3915            ("daysInMonth", "/** Days in this month. */"),
3916            ("daysInYear", "/** Days in this year. */"),
3917            ("monthsInYear", "/** Months in this year. */"),
3918            (
3919                "inLeapYear",
3920                "/** True when the ISO calendar year is a leap year. */",
3921            ),
3922            (
3923                "millisecond",
3924                "/** Millisecond within the second (0–999). */",
3925            ),
3926            (
3927                "microsecond",
3928                "/** Microsecond within the millisecond (0–999). */",
3929            ),
3930            (
3931                "nanosecond",
3932                "/** Nanoseconds within the second (0–999_999_999). */",
3933            ),
3934        ],
3935        {
3936            let mut m = arith_methods(temporal_plain_date_time_ref());
3937            m.push((
3938                "toPlainDate".to_string(),
3939                MethodSig {
3940                    optional: false,
3941                    generics: Vec::new(),
3942                    params: Vec::new(),
3943                    ret: temporal_plain_date_ref(),
3944                    predicate: None,
3945                    doc: doc("/** The calendar-date part as a `PlainDate`. */"),
3946                },
3947            ));
3948            m.push((
3949                "toPlainTime".to_string(),
3950                MethodSig {
3951                    optional: false,
3952                    generics: Vec::new(),
3953                    params: Vec::new(),
3954                    ret: temporal_plain_time_ref(),
3955                    predicate: None,
3956                    doc: doc("/** The wall-clock-time part as a `PlainTime`. */"),
3957                },
3958            ));
3959            m.push(to_year_month_method());
3960            m.push(to_month_day_method());
3961            m.push((
3962                "toZonedDateTime".to_string(),
3963                MethodSig {
3964                    optional: false,
3965                    generics: Vec::new(),
3966                    params: vec![Param::new("timeZone", Type::String)],
3967                    ret: temporal_zdt_ref(),
3968                    predicate: None,
3969                    doc: doc(
3970                        "/** Interprets this wall clock in `timeZone`, resolving DST gaps with `compatible` disambiguation. */",
3971                    ),
3972                },
3973            ));
3974            m.push(with_method(
3975                temporal_plain_date_time_ref(),
3976                "PlainDateTimeFields",
3977            ));
3978            m
3979        },
3980        true,
3981    );
3982    insert_temporal_plain_type(
3983        &mut temporal,
3984        &temporal_prefix,
3985        "PlainYearMonth",
3986        "/** A calendar year and month with no day or time zone (e.g. `2026-06`). */",
3987        &[
3988            ("year", "/** Calendar year. */"),
3989            ("month", "/** Calendar month (1–12). */"),
3990            ("monthCode", "/** ISO month code, e.g. `\"M06\"`. */"),
3991            ("daysInMonth", "/** Days in this month. */"),
3992            ("daysInYear", "/** Days in this year. */"),
3993            ("monthsInYear", "/** Months in this year. */"),
3994            (
3995                "inLeapYear",
3996                "/** True when the ISO calendar year is a leap year. */",
3997            ),
3998        ],
3999        {
4000            let mut m = arith_methods(temporal_plain_year_month_ref());
4001            m.push(with_method(
4002                temporal_plain_year_month_ref(),
4003                "PlainYearMonthFields",
4004            ));
4005            m.push((
4006                "toPlainDate".to_string(),
4007                MethodSig {
4008                    optional: false,
4009                    generics: Vec::new(),
4010                    params: vec![Param::new(
4011                        "fields",
4012                        temporal_ref("PlainYearMonthToDateFields"),
4013                    )],
4014                    ret: temporal_plain_date_ref(),
4015                    predicate: None,
4016                    doc: doc(
4017                        "/** Adds a `day` to make a `PlainDate`; the day constrains to the month length. */",
4018                    ),
4019                },
4020            ));
4021            m
4022        },
4023        true,
4024    );
4025    insert_temporal_plain_type(
4026        &mut temporal,
4027        &temporal_prefix,
4028        "PlainMonthDay",
4029        "/** A calendar month and day with no year or time zone (e.g. `06-07`). */",
4030        &[
4031            ("month", "/** Calendar month (1–12). */"),
4032            ("monthCode", "/** ISO month code, e.g. `\"M06\"`. */"),
4033            ("day", "/** Day of the month (1–31). */"),
4034        ],
4035        vec![
4036            with_method(temporal_ref("PlainMonthDay"), "PlainMonthDayFields"),
4037            (
4038                "toPlainDate".to_string(),
4039                MethodSig {
4040                    optional: false,
4041                    generics: Vec::new(),
4042                    params: vec![Param::new(
4043                        "fields",
4044                        temporal_ref("PlainMonthDayToDateFields"),
4045                    )],
4046                    ret: temporal_plain_date_ref(),
4047                    predicate: None,
4048                    doc: doc(
4049                        "/** Adds a `year` to make a `PlainDate`; a Feb-29 month-day constrains to Feb-28 in a non-leap year. */",
4050                    ),
4051                },
4052            ),
4053        ],
4054        false,
4055    );
4056
4057    let now_prefix = crate::mangle::extend(&temporal_prefix, "Now");
4058    let mut now = crate::NamespaceSymbol {
4059        name: "Now".to_string(),
4060        mangled_prefix: now_prefix.clone(),
4061        declaration_span: Span::at(crate::FileId::PRELUDE),
4062        values: BTreeMap::new(),
4063        types: BTreeMap::new(),
4064        namespaces: BTreeMap::new(),
4065        doc: doc(
4066            "/** Non-deterministic time / time-zone queries. Calls land in the durable-execution log so replays can reproduce the original wall-clock values. */",
4067        ),
4068    };
4069    now.values.insert(
4070        "instant".to_string(),
4071        ValueSymbol {
4072            name: "instant".to_string(),
4073            mangled_name: crate::mangle::extend(&now_prefix, "instant"),
4074            declaration_span: Span::at(crate::FileId::PRELUDE),
4075            kind: ValueKind::Function {
4076                generics: Vec::new(),
4077                params: Vec::new(),
4078                ret: temporal_instant_ref(),
4079                type_predicate: None,
4080                doc: doc(
4081                    "/** The current wall-clock instant. Non-deterministic — reads the system clock. */",
4082                ),
4083            },
4084        },
4085    );
4086    now.values.insert(
4087        "timeZoneId".to_string(),
4088        ValueSymbol {
4089            name: "timeZoneId".to_string(),
4090            mangled_name: crate::mangle::extend(&now_prefix, "timeZoneId"),
4091            declaration_span: Span::at(crate::FileId::PRELUDE),
4092            kind: ValueKind::Function {
4093                generics: Vec::new(),
4094                params: Vec::new(),
4095                ret: Type::String,
4096                type_predicate: None,
4097                doc: doc(
4098                    "/** The system IANA time-zone id (or `\"UTC\"` if the system zone can't be detected). Non-deterministic. */",
4099                ),
4100            },
4101        },
4102    );
4103    now.values.insert(
4104        "zonedDateTimeISO".to_string(),
4105        ValueSymbol {
4106            name: "zonedDateTimeISO".to_string(),
4107            mangled_name: crate::mangle::extend(&now_prefix, "zonedDateTimeISO"),
4108            declaration_span: Span::at(crate::FileId::PRELUDE),
4109            kind: ValueKind::Function {
4110                generics: Vec::new(),
4111                params: vec![Param::with_default(
4112                    "tz",
4113                    Type::Union(vec![Type::String, Type::Undefined]),
4114                    crate::DefaultValue::Undefined,
4115                )],
4116                ret: temporal_zdt_ref(),
4117                type_predicate: None,
4118                doc: doc(
4119                    "/** The current ZonedDateTime in `tz`, or the system zone if `tz` is omitted / undefined. Non-deterministic. */",
4120                ),
4121            },
4122        },
4123    );
4124    let mut insert_now_plain = |name: &str, ret: Type, doc_text: &'static str| {
4125        now.values.insert(
4126            name.to_string(),
4127            ValueSymbol {
4128                name: name.to_string(),
4129                mangled_name: crate::mangle::extend(&now_prefix, name),
4130                declaration_span: Span::at(FileId::PRELUDE),
4131                kind: ValueKind::Function {
4132                    generics: Vec::new(),
4133                    params: vec![Param::with_default(
4134                        "tz",
4135                        Type::Union(vec![Type::String, Type::Undefined]),
4136                        crate::DefaultValue::Undefined,
4137                    )],
4138                    ret,
4139                    type_predicate: None,
4140                    doc: doc(doc_text),
4141                },
4142            },
4143        );
4144    };
4145    insert_now_plain(
4146        "plainDateISO",
4147        temporal_plain_date_ref(),
4148        "/** Today's date in `tz`, or the system (local) zone if `tz` is omitted / undefined. Non-deterministic. */",
4149    );
4150    insert_now_plain(
4151        "plainTimeISO",
4152        temporal_plain_time_ref(),
4153        "/** The current wall-clock time in `tz`, or the system (local) zone if `tz` is omitted / undefined. Non-deterministic. */",
4154    );
4155    insert_now_plain(
4156        "plainDateTimeISO",
4157        temporal_ref("PlainDateTime"),
4158        "/** The current date and wall-clock time in `tz`, or the system (local) zone if `tz` is omitted / undefined. Non-deterministic. */",
4159    );
4160    now.values.insert(
4161        "zonedDateTime".to_string(),
4162        ValueSymbol {
4163            name: "zonedDateTime".to_string(),
4164            mangled_name: crate::mangle::extend(&now_prefix, "zonedDateTime"),
4165            declaration_span: Span::at(FileId::PRELUDE),
4166            kind: ValueKind::Function {
4167                generics: Vec::new(),
4168                params: vec![Param::with_default(
4169                    "tz",
4170                    Type::Union(vec![Type::String, Type::Undefined]),
4171                    crate::DefaultValue::Undefined,
4172                )],
4173                ret: temporal_zdt_ref(),
4174                type_predicate: None,
4175                doc: doc(
4176                    "/** The current ZonedDateTime in `tz`, or the system zone if `tz` is undefined. Non-deterministic. Alias of `zonedDateTimeISO` (our subset has no calendar argument). */",
4177                ),
4178            },
4179        },
4180    );
4181    temporal.namespaces.insert("Now".to_string(), now);
4182
4183    for (local_name, ty_sym) in &temporal.types {
4184        let flat_key = format!("Temporal#{local_name}");
4185        defs.types.insert(flat_key, ty_sym.clone());
4186    }
4187
4188    defs.namespaces.insert("Temporal".to_string(), temporal);
4189}
4190
4191#[cfg(test)]
4192mod invariant_tests {
4193    use super::*;
4194    #[test]
4195    fn fractional_digits_are_checked_even_for_date_only_durations() {
4196        for digits in [10, u8::MAX] {
4197            assert!(
4198                rounding_for_fractional_second_digits(digits)
4199                    .unwrap_err()
4200                    .is::<wasmtime::Trap>()
4201            );
4202            assert!(
4203                format_duration_with_fractional_digits(&Span::new().days(1), digits)
4204                    .unwrap_err()
4205                    .is::<wasmtime::Trap>()
4206            );
4207        }
4208        assert_eq!(
4209            format_duration_with_fractional_digits(&Span::new().seconds(1).milliseconds(25), 3)
4210                .unwrap(),
4211            "PT1.025S"
4212        );
4213    }
4214}