Skip to main content

candela_vm/
rt.rs

1//! Runtime data types shared by the VM and the compiler.
2//!
3//! These live in the self-contained `candela-vm` crate so the runtime core
4//! (`vm` + `data` + `gc` + `embed` + `artifact`) links without the parser,
5//! compiler, or REPL. The `candela` compiler crate depends on this crate and
6//! re-exports these types (aliasing `candela_vm::rt` as `crate::rt`, etc.), so
7//! its own source keeps its import paths (`compiler_data::Struct`,
8//! `type_system::DataType`, `expr::Span`).
9
10use crate::data::Data;
11pub use crate::gc::GcState;
12pub use crate::gc::GcStats;
13pub use crate::gc::PoolStats;
14use crate::instr::Instr;
15use crate::vm::MapPool;
16use crate::vm::ObjectPool;
17use crate::vm::StringPool;
18#[cfg(not(target_arch = "wasm32"))]
19use libloading::Library;
20use serde::Deserialize;
21use serde::Serialize;
22use smol_strc::SmolStr;
23use std::cell::Cell;
24use std::cell::RefCell;
25use std::hint::unreachable_unchecked;
26use std::path::Path;
27use std::path::PathBuf;
28#[cfg(not(target_arch = "wasm32"))]
29use std::rc::Rc;
30
31#[cfg(not(target_arch = "wasm32"))]
32use libffi::middle::Type;
33
34/// A span of code in a `Source`'s `contents`.
35#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
36pub struct Span {
37    pub start: u32,
38    pub end: u32,
39}
40
41impl Span {
42    #[inline(always)]
43    #[must_use]
44    pub const fn extend(self, span: Self) -> Self {
45        Self {
46            start: self.start,
47            end: span.end,
48        }
49    }
50}
51
52impl From<std::range::Range<usize>> for Span {
53    #[inline(always)]
54    fn from(value: std::range::Range<usize>) -> Self {
55        Self {
56            start: value.start as u32,
57            end: value.end as u32,
58        }
59    }
60}
61
62impl From<std::ops::Range<usize>> for Span {
63    #[inline(always)]
64    fn from(value: std::ops::Range<usize>) -> Self {
65        Self {
66            start: value.start as u32,
67            end: value.end as u32,
68        }
69    }
70}
71
72impl From<Span> for std::ops::Range<usize> {
73    #[inline(always)]
74    fn from(val: Span) -> Self {
75        val.start as usize..val.end as usize
76    }
77}
78
79impl From<(usize, usize)> for Span {
80    #[inline(always)]
81    fn from((start, end): (usize, usize)) -> Self {
82        Self {
83            start: start as u32,
84            end: end as u32,
85        }
86    }
87}
88
89impl From<(u32, u32)> for Span {
90    #[inline(always)]
91    fn from((start, end): (u32, u32)) -> Self {
92        Self { start, end }
93    }
94}
95
96/// A resolved candela type. Attached to compiled artifacts (struct field types,
97/// dynamic-library / host function signatures) so the VM can marshal values.
98// `to_c_type` uses `unsafe` internally, which trips clippy's
99// `unsafe_derive_deserialize`; the derive itself introduces no unsoundness (the
100// deserialized value is an ordinary enum), so the lint is not applicable here.
101#[allow(clippy::unsafe_derive_deserialize)]
102#[derive(Debug, Clone, Serialize, Deserialize)]
103pub enum DataType {
104    /// Array(None) = Unknown[]
105    Array(Option<Box<Self>>),
106    Float,
107    Int,
108    Bool,
109    String,
110    Null,
111    Unknown,
112    Union(Box<[Self]>),
113    Fn(u16),
114    /// A function held as a value, spelled `fn(A, B) -> R`. A call through one
115    /// dispatches on the function the value carries rather than on a function
116    /// the compiler picked, which is what lets one slot hold a different
117    /// function each time.
118    FnValue(Box<FnValue>),
119    Struct(u16),
120    Enum(u16),
121    Map(Box<(Option<Self>, Option<Self>)>),
122}
123
124/// What a `fn(...)` type says about the functions that may reach it.
125#[derive(Debug, Clone, Serialize, Deserialize)]
126pub struct FnValue {
127    /// Parameter types and return type, from a `fn(A, B) -> R` annotation.
128    /// `None` where the type came from the functions written into the position
129    /// instead, and the first call through it settles the types.
130    pub sig: Option<(Box<[DataType]>, DataType)>,
131    /// The functions the compiler watched reach this position, in the order it
132    /// saw them. A call through the value compiles each one at the call's
133    /// argument types. Empty for a declared type, where the annotation already
134    /// says what to compile against.
135    pub candidates: Box<[u16]>,
136    /// The set of functions a variable of this type may hold, by its index in
137    /// the compiler's table of such sets. A variable grows its set as the code
138    /// writes more functions into it, and a closure that captured the variable
139    /// reads the set rather than the copy it took, so a function written in
140    /// after the closure still reaches it. `None` for a position whose
141    /// candidates are settled where it is built, such as a list literal.
142    pub set: Option<u32>,
143}
144
145impl FnValue {
146    /// The type of a position that holds one of `candidates` and takes its
147    /// argument types from the call.
148    #[must_use]
149    pub fn inferred(candidates: Box<[u16]>) -> Self {
150        Self {
151            sig: None,
152            candidates,
153            set: None,
154        }
155    }
156
157    /// The type of a variable whose functions are collected in `set`.
158    #[must_use]
159    pub fn from_set(set: u32) -> Self {
160        Self {
161            sig: None,
162            candidates: Box::from([]),
163            set: Some(set),
164        }
165    }
166}
167
168impl std::fmt::Display for FnValue {
169    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
170        let Some((args, return_type)) = &self.sig else {
171            return write!(f, "function");
172        };
173        write!(f, "fn(")?;
174        for (i, arg) in args.iter().enumerate() {
175            if i > 0 {
176                write!(f, ", ")?;
177            }
178            write!(f, "{arg}")?;
179        }
180        write!(f, ")")?;
181        if !matches!(return_type, DataType::Null) {
182            write!(f, " -> {return_type}")?;
183        }
184        Ok(())
185    }
186}
187
188impl std::fmt::Display for DataType {
189    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
190        match self {
191            Self::Float => write!(f, "float"),
192            Self::Int => write!(f, "int"),
193            Self::Bool => write!(f, "bool"),
194            Self::String => write!(f, "string"),
195            Self::Array(array_type) => match array_type {
196                Some(array_type) => write!(f, "{array_type}[]"),
197                None => write!(f, "Unknown[]"),
198            },
199            Self::Null => write!(f, "null"),
200            Self::Unknown => write!(f, "Unknown"),
201            Self::Union(types) => write!(
202                f,
203                "{}",
204                types
205                    .into_iter()
206                    .map(|x| format!("{x}"))
207                    .collect::<Vec<_>>()
208                    .join("|")
209            ),
210            Self::Struct(_) => write!(f, "struct"),
211            Self::Enum(_) => write!(f, "enum"),
212            Self::Map(m) => write!(
213                f,
214                "{{{}: {}}}",
215                m.0.as_ref().unwrap_or(&Self::Unknown),
216                m.1.as_ref().unwrap_or(&Self::Unknown)
217            ),
218            Self::Fn(_) => write!(f, "function"),
219            Self::FnValue(sig) => write!(f, "{sig}"),
220        }
221    }
222}
223
224impl DataType {
225    #[cfg(not(target_arch = "wasm32"))]
226    #[must_use]
227    pub fn to_c_type(&self, structs: &[Struct]) -> Type {
228        match self {
229            Self::Int => libffi::middle::Type::i64(),
230            Self::Float => libffi::middle::Type::f64(),
231            Self::String | Self::Array(_) => libffi::middle::Type::pointer(),
232            Self::Null => libffi::middle::Type::void(),
233            Self::Struct(id) => libffi::middle::Type::structure(
234                structs[*id as usize]
235                    .fields
236                    .iter()
237                    .map(|(_, field_type, _)| field_type.to_c_type(structs)),
238            ),
239            // Enums (and maps, unions, functions) have no C representation, so
240            // they cannot appear in a `dylib`/`host` signature. Reaching here is
241            // a defined panic rather than undefined behavior.
242            other => {
243                panic!("type {other} has no C representation and cannot cross the FFI boundary")
244            }
245        }
246    }
247}
248
249#[derive(Debug)]
250pub struct ErrorCatch {
251    pub catch_loc: u32,
252    pub error_reg: u16,
253    pub call_frames_len: u32,
254    pub args_len: u32,
255    /// How deep the recursion stack was when the `try` began. A call made
256    /// inside the `try` saves the resuming function's registers from here up,
257    /// so a catch unwinds back to this depth.
258    pub recursion_len: u32,
259}
260
261/// Marshalling signature for a `host` function, indexed by
262/// the compiler's `FnSignature::id`. `types[0]` is the return
263/// type; `types[1..]` are the argument types.
264#[derive(Debug, Clone)]
265pub struct HostFnSig {
266    /// [ return_type, arg_types... ]
267    pub types: Box<[DataType]>,
268    /// The `(namespace, name)` this signature was declared under, used by the
269    /// `Engine` to bind the matching registered closure.
270    pub namespace: SmolStr,
271    pub name: SmolStr,
272    /// Declared with `...` in the `host` block: the call site forwards any
273    /// number of arguments of any type to the registered closure and the
274    /// `Engine` skips signature validation. `types` holds only the return type.
275    pub variadic: bool,
276}
277
278impl HostFnSig {
279    #[inline(always)]
280    #[must_use]
281    pub fn get_return_type(&self) -> &DataType {
282        unsafe { self.types.get_unchecked(0) }
283    }
284    #[inline(always)]
285    #[must_use]
286    pub fn get_arg(&self, idx: usize) -> &DataType {
287        unsafe { self.types.get_unchecked(1 + idx) }
288    }
289    #[inline(always)]
290    #[must_use]
291    pub fn arg_count(&self) -> usize {
292        self.types.len() - 1
293    }
294}
295
296/// Where a recorded `dylib` spec is resolved from when an artifact loads.
297///
298/// A program names its own libraries, and those keep the spec the source wrote
299/// and the search it has always had. A standard-library module names its
300/// library by a path relative to its own file, which the runtime never sees, so
301/// such a spec is recorded relative to the `libs` directory the toolchain lays
302/// out and resolved through [`std_lib_dir`].
303#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
304pub enum LibraryOrigin {
305    /// A library the program itself names.
306    Program,
307    /// A library the shipped standard library names, recorded relative to the
308    /// `libs` directory.
309    StandardLibrary,
310}
311
312#[derive(Debug)]
313#[allow(clippy::pub_underscore_fields)]
314pub struct DynamicLibFn {
315    /// [ return_type, arg_types... ]
316    pub types: Box<[DataType]>,
317    /// The library spec recorded for this binding: a bare logical name (`z`,
318    /// `sqlite3`) or a path, as the source wrote it, or, for a
319    /// [`LibraryOrigin::StandardLibrary`] binding, that path made relative to
320    /// the `libs` directory. Recorded so a `.cdlb` artifact can re-resolve the
321    /// library by name at load time; it is not read on the live call path (the
322    /// resolved `_lib`/`ptr`/`cif` below are).
323    pub library: SmolStr,
324    /// Which search the spec is resolved through.
325    pub origin: LibraryOrigin,
326    /// The C symbol this binding resolves to. Recorded alongside `library` so a
327    /// `.cdlb` can re-bind the symbol at load without the source tree.
328    pub symbol: SmolStr,
329    // Keeps the loaded library alive for as long as its function pointers are
330    // callable; `pub` so the compiler can construct it, `_`-prefixed because it
331    // is never read directly.
332    #[cfg(not(target_arch = "wasm32"))]
333    pub _lib: Rc<Library>,
334    #[cfg(not(target_arch = "wasm32"))]
335    pub ptr: libffi::middle::CodePtr,
336    #[cfg(not(target_arch = "wasm32"))]
337    pub cif: libffi::middle::Cif,
338    /// The runtime's own function standing in for the symbol, where there is
339    /// no library to load; see [`crate::intrinsics`].
340    #[cfg(target_arch = "wasm32")]
341    pub intrinsic: crate::intrinsics::Intrinsic,
342}
343
344/// Target operating system for dynamic-library filename resolution.
345///
346/// Kept explicit, rather than only branching on `cfg!(target_os)` at the call
347/// site, so the logical-name -> filename mapping ([`resolve_library_filename`])
348/// can be unit-tested for every platform on any build host.
349#[derive(Clone, Copy, Debug, PartialEq, Eq)]
350pub enum TargetOs {
351    Linux,
352    Macos,
353    Windows,
354}
355
356impl TargetOs {
357    /// The OS this runtime was built for. Targets that are none of
358    /// Linux/macOS/Windows fall back to the Linux convention.
359    pub const CURRENT: Self = if cfg!(target_os = "macos") {
360        Self::Macos
361    } else if cfg!(target_os = "windows") {
362        Self::Windows
363    } else {
364        Self::Linux
365    };
366
367    /// Dynamic-library filename extension for this OS (no leading dot).
368    #[must_use]
369    pub const fn dynamic_lib_extension(self) -> &'static str {
370        match self {
371            Self::Linux => "so",
372            Self::Macos => "dylib",
373            Self::Windows => "dll",
374        }
375    }
376
377    /// Filename prefix a bare logical name gets on this OS (`lib` on
378    /// Linux/macOS, none on Windows).
379    #[must_use]
380    pub const fn dynamic_lib_prefix(self) -> &'static str {
381        match self {
382            Self::Linux | Self::Macos => "lib",
383            Self::Windows => "",
384        }
385    }
386}
387
388/// Maps a candela `dylib`/`import` library spec to a concrete filename for `os`.
389///
390/// A bare logical name (`m`, `sqlite3`, `z`), no path separator and no
391/// extension, becomes the platform convention: `libm.so` on Linux,
392/// `libm.dylib` on macOS, `m.dll` on Windows. The same candela source therefore
393/// names the right file on every target, and the OS loader searches its standard
394/// library paths for it.
395///
396/// A spec that already carries a path separator or an explicit extension is
397/// treated as an explicit path and honored: an extension is kept as-is; a path
398/// with no extension is completed with the platform extension (and gets no `lib`
399/// prefix). This keeps absolute and workspace-relative library paths working
400/// across all three targets.
401#[must_use]
402pub fn resolve_library_filename(spec: &str, os: TargetOs) -> String {
403    let has_separator = spec.contains('/') || spec.contains('\\');
404    let has_extension = Path::new(spec).extension().is_some();
405    if has_extension {
406        spec.to_owned()
407    } else if has_separator {
408        format!("{spec}.{}", os.dynamic_lib_extension())
409    } else {
410        format!(
411            "{}{spec}.{}",
412            os.dynamic_lib_prefix(),
413            os.dynamic_lib_extension()
414        )
415    }
416}
417
418thread_local! {
419    static DYLIB_DIRS: RefCell<Vec<PathBuf>> = const { RefCell::new(Vec::new()) };
420    static ARGV_SKIP: Cell<usize> = const { Cell::new(DEFAULT_ARGV_SKIP) };
421}
422
423/// Sets the directories a `dylib` import's library is looked for in on this
424/// thread, and returns the ones that were in effect, so a caller can put them
425/// back.
426///
427/// A host whose native libraries do not sit beside the script names their
428/// directories here: an application that keeps its candela sources in `src/`
429/// and its shared libraries in `lib/` points this at `lib/`, and `dylib "md"`
430/// finds `libmd.so` there. A project that pulls packages in adds one entry per
431/// package root, so a package's own native libraries load the same way. An
432/// empty list goes back to searching beside the importing file only.
433///
434/// The directories are searched in order, ahead of everything else; whatever
435/// the library name resolved to before is still tried after them, so a program
436/// that names a system library keeps working. The setting is read when a
437/// program is compiled and when a `.cdlb` artifact is loaded, so set it before
438/// either.
439pub fn set_dylib_dirs(dirs: Vec<PathBuf>) -> Vec<PathBuf> {
440    DYLIB_DIRS.replace(dirs)
441}
442
443/// The directories [`set_dylib_dirs`] put in effect on this thread.
444#[must_use]
445pub fn dylib_dirs() -> Vec<PathBuf> {
446    DYLIB_DIRS.with(|dirs| dirs.borrow().clone())
447}
448
449/// How many leading command-line arguments belong to the command rather than to
450/// the program. `argv()` steps over them.
451///
452/// Two is the shape both runners have always had: the binary, then the file it
453/// was pointed at.
454const DEFAULT_ARGV_SKIP: usize = 2;
455
456/// Says how many leading command-line arguments are the command's own, and
457/// returns the count that was in effect.
458///
459/// `candela-vm game.cdlb a b` and `candela game.cdl a b` both put the program's
460/// first argument third, which is the default. A command that spells a verb out
461/// first, as `candela run game.cdl a b` does, has one more of its own to step
462/// over and says so here.
463pub fn set_argv_skip(count: usize) -> usize {
464    ARGV_SKIP.replace(count)
465}
466
467/// The count [`set_argv_skip`] put in effect on this thread.
468#[must_use]
469pub fn argv_skip() -> usize {
470    ARGV_SKIP.get()
471}
472
473/// The directory the standard library ships in: `CANDELA_LIB_PATH` when it is
474/// set, and `libs/` beside the running executable otherwise, which is where the
475/// toolchain installs it next to `candela` and `candela-vm`.
476///
477/// This is the one answer to "where is `libs`", read by the compiler resolving
478/// a library import and by an artifact load re-opening a standard-library
479/// dynamic library.
480#[must_use]
481pub fn std_lib_dir() -> Option<PathBuf> {
482    if let Some(base) = std::env::var_os("CANDELA_LIB_PATH") {
483        return Some(PathBuf::from(base));
484    }
485    std::env::current_exe().ok().map(|exe| {
486        exe.canonicalize()
487            .unwrap_or(exe)
488            .parent()
489            .unwrap_or_else(|| Path::new("."))
490            .join("libs")
491    })
492}
493
494/// Opens `filename` under each [`dylib_dirs`] directory in turn, then, for a
495/// standard-library file, under [`std_lib_dir`], and hands the name to the OS
496/// loader when it is under none of them.
497///
498/// The loader's own error comes back, so a caller reports the same message it
499/// did before a directory was ever set.
500#[cfg(not(target_arch = "wasm32"))]
501pub(crate) fn open_library(
502    filename: &str,
503    origin: LibraryOrigin,
504) -> Result<Library, libloading::Error> {
505    for dir in dylib_dirs() {
506        if let Ok(lib) = unsafe { Library::new(dir.join(filename)) } {
507            return Ok(lib);
508        }
509    }
510    if matches!(origin, LibraryOrigin::StandardLibrary)
511        && let Some(dir) = std_lib_dir()
512        && let Ok(lib) = unsafe { Library::new(dir.join(filename)) }
513    {
514        return Ok(lib);
515    }
516    unsafe { Library::new(filename) }
517}
518
519#[cfg(test)]
520mod dynlib_tests {
521    use super::TargetOs;
522    use super::resolve_library_filename;
523
524    #[test]
525    fn logical_name_maps_to_each_os_convention() {
526        assert_eq!(resolve_library_filename("m", TargetOs::Linux), "libm.so");
527        assert_eq!(resolve_library_filename("m", TargetOs::Macos), "libm.dylib");
528        assert_eq!(resolve_library_filename("m", TargetOs::Windows), "m.dll");
529
530        assert_eq!(
531            resolve_library_filename("sqlite3", TargetOs::Linux),
532            "libsqlite3.so"
533        );
534        assert_eq!(
535            resolve_library_filename("sqlite3", TargetOs::Macos),
536            "libsqlite3.dylib"
537        );
538        assert_eq!(
539            resolve_library_filename("sqlite3", TargetOs::Windows),
540            "sqlite3.dll"
541        );
542    }
543
544    #[test]
545    fn explicit_extension_is_honored_on_every_os() {
546        for os in [TargetOs::Linux, TargetOs::Macos, TargetOs::Windows] {
547            assert_eq!(resolve_library_filename("libfoo.so.6", os), "libfoo.so.6");
548            assert_eq!(
549                resolve_library_filename("/usr/lib/libz.so", os),
550                "/usr/lib/libz.so"
551            );
552            assert_eq!(resolve_library_filename("plugin.dll", os), "plugin.dll");
553        }
554    }
555
556    #[test]
557    fn path_without_extension_gets_platform_extension_and_no_prefix() {
558        assert_eq!(
559            resolve_library_filename("../std_src/math/math", TargetOs::Linux),
560            "../std_src/math/math.so"
561        );
562        assert_eq!(
563            resolve_library_filename("../std_src/math/math", TargetOs::Macos),
564            "../std_src/math/math.dylib"
565        );
566        assert_eq!(
567            resolve_library_filename("./plugins/foo", TargetOs::Windows),
568            "./plugins/foo.dll"
569        );
570    }
571}
572
573impl DynamicLibFn {
574    #[inline(always)]
575    #[must_use]
576    pub fn get_return_type(&self) -> &DataType {
577        unsafe { self.types.get_unchecked(0) }
578    }
579}
580
581#[derive(Debug)]
582pub struct Struct {
583    pub name: SmolStr,
584    pub fields: Box<[(SmolStr, DataType, Span)]>,
585    pub id: u16,
586    pub name_span: Span,
587}
588
589/// A native tagged-union enum type. Its values are stored in the object pool
590/// like structs: element 0 of the pool entry is the variant tag (an int index
591/// into `variants`), and elements `1..` are the variant's payload in order.
592#[derive(Debug)]
593pub struct EnumType {
594    pub name: SmolStr,
595    pub variants: Box<[EnumVariant]>,
596    pub id: u16,
597    pub name_span: Span,
598}
599
600#[derive(Debug)]
601pub struct EnumVariant {
602    pub name: SmolStr,
603    /// Payload field types, in declaration order. Empty for a nullary variant.
604    pub payload: Box<[DataType]>,
605    pub name_span: Span,
606}
607
608pub struct Pools {
609    pub objs: ObjectPool,
610    pub maps: MapPool,
611    pub strings: StringPool,
612    pub gc: GcState,
613}
614
615impl Pools {
616    pub fn new(objs: ObjectPool, maps: MapPool, strings: StringPool) -> Self {
617        Self {
618            objs,
619            maps,
620            strings,
621            gc: GcState::default(),
622        }
623    }
624
625    /// Does up to about `budget` units of collection work while the program
626    /// is idle, with `registers` as the roots, and answers whether no
627    /// collection work is left. See [`GcState::collect`].
628    pub fn collect(&mut self, registers: &[Data], budget: u32) -> bool {
629        self.gc.collect(
630            &mut self.objs,
631            &mut self.maps,
632            &mut self.strings,
633            registers,
634            budget,
635        )
636    }
637
638    /// What the collector has done over these pools, and how large they are.
639    #[must_use]
640    pub fn gc_stats(&self) -> GcStats {
641        self.gc.stats(&self.objs, &self.maps, &self.strings)
642    }
643}
644
645pub struct Source {
646    pub filename: SmolStr,
647    pub contents: String,
648}
649
650#[derive(Copy, Clone)]
651pub struct InstrSrc {
652    pub instr: Instr,
653    pub span: Span,
654    pub file_id: u16,
655}
656
657impl DataType {
658    #[inline(always)]
659    #[must_use]
660    pub const fn is_indexable(&self) -> bool {
661        matches!(self, Self::String | Self::Array(_) | Self::Unknown)
662    }
663
664    /// Collapses a union of return types to a single type when they all agree
665    /// (ignoring `Unknown`), or to `Unknown` when only unknowns remain.
666    #[must_use]
667    pub fn check_poly(self) -> Self {
668        if let Self::Union(ref elems) = self {
669            if let Some(new) = reduce_null_struct(elems) {
670                return new;
671            }
672            let mut concrete = elems
673                .iter()
674                .filter(|elem_type| **elem_type != Self::Unknown);
675            if let Some(first_type) = concrete.next() {
676                if concrete.all(|x| x == first_type) {
677                    first_type.clone()
678                } else {
679                    self
680                }
681            } else if !elems.is_empty() {
682                Self::Unknown
683            } else {
684                unsafe { unreachable_unchecked() }
685            }
686        } else {
687            unsafe { unreachable_unchecked() }
688        }
689    }
690}
691
692fn reduce_null_struct(types: &[DataType]) -> Option<DataType> {
693    let mut struct_type = None;
694    for t in types {
695        match t {
696            DataType::Null | DataType::Unknown => {}
697            DataType::Struct(_) => {
698                if let Some(struct_type) = &struct_type {
699                    if struct_type != t {
700                        return None;
701                    }
702                } else {
703                    struct_type = Some(t.clone());
704                }
705            }
706            _ => return None,
707        }
708    }
709    struct_type
710}
711
712impl PartialEq for DataType {
713    fn eq(&self, other: &Self) -> bool {
714        match (self, other) {
715            // Array(None) is compatible with any array type
716            (Self::Float, Self::Float)
717            | (Self::Int, Self::Int)
718            | (Self::Bool, Self::Bool)
719            | (Self::String, Self::String)
720            | (Self::Null, Self::Null)
721            | (Self::Unknown, Self::Unknown)
722            | (Self::Array(_), Self::Array(None))
723            | (Self::Array(None), Self::Array(_)) => true,
724            (Self::Array(Some(a)), Self::Array(Some(b))) => a == b,
725            (Self::Union(a), Self::Union(b)) => a == b,
726            (Self::Struct(a), Self::Struct(b)) => a == b,
727            (Self::Enum(a), Self::Enum(b)) => a == b,
728            (Self::Fn(_), Self::Fn(_)) => true,
729            (Self::FnValue(a), Self::FnValue(b)) => match (&a.sig, &b.sig) {
730                (Some(a), Some(b)) => a.0 == b.0 && a.1 == b.1,
731                _ => true,
732            },
733            (Self::Map(a), Self::Map(b)) => {
734                (a.0.is_none() || b.0.is_none() || a.0 == b.0)
735                    && (a.1.is_none() || b.1.is_none() || a.1 == b.1)
736            }
737            (t, Self::Union(p)) | (Self::Union(p), t) => p.contains(t),
738            _ => false,
739        }
740    }
741}
742
743impl std::hash::Hash for DataType {
744    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
745        // All Array variants hash identically, which is required because Array(None) == Array(Some(_))
746        match self {
747            Self::Array(_) => 0u8.hash(state),
748            Self::Float => 1u8.hash(state),
749            Self::Int => 2u8.hash(state),
750            Self::Bool => 3u8.hash(state),
751            Self::String => 4u8.hash(state),
752            Self::Null => 6u8.hash(state),
753            Self::Unknown => 7u8.hash(state),
754            Self::Union(p) => {
755                8u8.hash(state);
756                p.hash(state);
757            }
758            Self::Fn(f) => {
759                9u8.hash(state);
760                f.hash(state);
761            }
762            // Every function type hashes alike: two that compare equal must,
763            // and a signature is compared field by field rather than by id.
764            Self::FnValue(_) => 13u8.hash(state),
765            Self::Struct(s) => {
766                10u8.hash(state);
767                s.hash(state);
768            }
769            Self::Enum(e) => {
770                12u8.hash(state);
771                e.hash(state);
772            }
773            Self::Map(m) => {
774                11u8.hash(state);
775                m.hash(state);
776            }
777        }
778    }
779}