diff --git a/Cargo.lock b/Cargo.lock index e6a8ad8..66a1fd8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -41,6 +41,12 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "allocator-api2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c880a97d28a3681c0267bd29cff89621202715b065127cd445fa0f0fe0aa2880" + [[package]] name = "anes" version = "0.1.6" @@ -276,6 +282,15 @@ dependencies = [ "syn", ] +[[package]] +name = "boxing" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a817f12ef805b34fe1565bea00630d84f8f08bf26200b05c41456c77cdada88" +dependencies = [ + "sptr", +] + [[package]] name = "bstr" version = "1.12.1" @@ -293,7 +308,7 @@ version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" dependencies = [ - "allocator-api2", + "allocator-api2 0.2.21", ] [[package]] @@ -528,15 +543,6 @@ dependencies = [ "unicode-segmentation", ] -[[package]] -name = "convert_case" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "affbf0190ed2caf063e3def54ff444b449371d55c58e513a95ab98eca50adb49" -dependencies = [ - "unicode-segmentation", -] - [[package]] name = "cooked-waker" version = "5.0.0" @@ -847,7 +853,7 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" dependencies = [ - "convert_case 0.10.0", + "convert_case", "proc-macro2", "quote", "rustc_version", @@ -1261,6 +1267,30 @@ dependencies = [ "slab", ] +[[package]] +name = "gc-arena" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cd70cf88a32937834aae9614ff2569b5d9467fa0c42c5d7762fd94a8de88266" +dependencies = [ + "allocator-api2 0.2.21", + "gc-arena-derive", + "hashbrown 0.14.5", + "sptr", +] + +[[package]] +name = "gc-arena-derive" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c612a69f5557a11046b77a7408d2836fe77077f842171cd211c5ef504bd3cddd" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -1354,6 +1384,9 @@ name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "allocator-api2 0.2.21", +] [[package]] name = "hashbrown" @@ -1370,7 +1403,7 @@ version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ - "allocator-api2", + "allocator-api2 0.2.21", "equivalent", "foldhash 0.2.0", ] @@ -2059,8 +2092,10 @@ dependencies = [ name = "nix-js" version = "0.1.0" dependencies = [ + "allocator-api2 0.4.0", "anyhow", "base64 0.22.1", + "boxing", "bumpalo", "bzip2", "clap", @@ -2073,6 +2108,7 @@ dependencies = [ "ere", "fastwebsockets", "flate2", + "gc-arena", "ghost-cell", "hashbrown 0.16.1", "hex", @@ -2085,7 +2121,6 @@ dependencies = [ "miette", "mimalloc", "nix-compat", - "nix-js-macros", "nix-nar", "num_enum", "regex", @@ -2099,6 +2134,7 @@ dependencies = [ "serde_json", "sha1", "sha2", + "smallvec", "string-interner", "tap", "tar", @@ -2113,16 +2149,6 @@ dependencies = [ "xz2", ] -[[package]] -name = "nix-js-macros" -version = "0.1.0" -dependencies = [ - "convert_case 0.11.0", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "nix-nar" version = "0.3.1" @@ -3193,6 +3219,12 @@ dependencies = [ "der", ] +[[package]] +name = "sptr" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b9b39299b249ad65f3b7e96443bad61c02ca5cd3589f46cb6d610a0fd6c0d6a" + [[package]] name = "sqlite-wasm-rs" version = "0.5.2" @@ -3612,9 +3644,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.12+spec-1.1.0" +version = "0.9.9+spec-1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +checksum = "eb5238e643fc34a1d5d7e753e1532a91912d74b63b92b3ea51fde8d1b7bc79dd" dependencies = [ "indexmap", "serde_core", diff --git a/Cargo.toml b/Cargo.toml index 6e7ea10..203e2eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,7 @@ [workspace] resolver = "3" members = [ - "nix-js", - "nix-js-macros" + "nix-js" ] [profile.profiling] diff --git a/nix-js-macros/Cargo.toml b/nix-js-macros/Cargo.toml deleted file mode 100644 index b37fd1c..0000000 --- a/nix-js-macros/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "nix-js-macros" -version = "0.1.0" -edition = "2024" - -[lib] -proc-macro = true - -[dependencies] -convert_case = "0.11" -proc-macro2 = "1.0" -quote = "1.0" -syn = { version = "2.0", features = ["full"] } diff --git a/nix-js-macros/src/ir.rs b/nix-js-macros/src/ir.rs deleted file mode 100644 index 7be10de..0000000 --- a/nix-js-macros/src/ir.rs +++ /dev/null @@ -1,241 +0,0 @@ -//! Implements the `ir!` procedural macro. -//! -//! This macro is designed to reduce the boilerplate associated with defining -//! an Intermediate Representation (IR) that follows a specific pattern. It generates: -//! 1. An enum representing the different kinds of IR nodes. -//! 2. Structs for each of the variants that have fields. -//! 3. `From` implementations to easily convert from a struct variant (e.g., `BinOp`) to the main enum (`Ir::BinOp`). -//! 4. A `To[IrName]` trait to provide a convenient `.to_ir()` method on the variant structs. - -use convert_case::{Case, Casing}; -use proc_macro::TokenStream; -use quote::{format_ident, quote}; -use syn::{ - Expr, ExprPath, FieldsNamed, GenericArgument, GenericParam, Generics, Ident, Path, PathSegment, - Token, Type, TypePath, parenthesized, - parse::{Parse, ParseStream, Result}, - punctuated::Punctuated, - token, -}; - -/// Represents one of the variants passed to the `ir!` macro. -enum VariantInput { - /// A unit-like variant, e.g., `Arg`. - Unit(Ident), - /// A tuple-like variant with one unnamed field, e.g., `ExprRef(ExprId)`. - Tuple(Ident, Type), - /// A struct-like variant with named fields, e.g., `BinOp { lhs: ExprId, rhs: ExprId, kind: BinOpKind }`. - Struct(Ident, FieldsNamed), -} - -/// The top-level input for the `ir!` macro. -struct MacroInput { - /// The name of the main IR enum to be generated (e.g., `Ir`). - base_name: Ident, - generics: Generics, - /// The list of variants for the enum. - variants: Punctuated, -} - -impl Parse for VariantInput { - fn parse(input: ParseStream) -> Result { - let name: Ident = input.parse()?; - - if input.peek(token::Paren) { - // Parse a tuple-like variant: `Variant(Type)` - let content; - parenthesized!(content in input); - let ty: Type = content.parse()?; - - if !content.is_empty() { - return Err(content.error("Expected a single type inside parentheses")); - } - - Ok(VariantInput::Tuple(name, ty)) - } else if input.peek(token::Brace) { - // Parse a struct-like variant: `Variant { field: Type, ... }` - let fields: FieldsNamed = input.parse()?; - Ok(VariantInput::Struct(name, fields)) - } else { - // Parse a unit-like variant: `Variant` - Ok(VariantInput::Unit(name)) - } - } -} - -impl Parse for MacroInput { - fn parse(input: ParseStream) -> Result { - let base_name = input.parse()?; - let generics = Generics::parse(input)?; - input.parse::()?; - let variants = Punctuated::parse_terminated(input)?; - - Ok(MacroInput { - base_name, - generics, - variants, - }) - } -} - -/// The implementation of the `ir!` macro. -pub fn ir_impl(input: TokenStream) -> TokenStream { - let parsed_input = syn::parse_macro_input!(input as MacroInput); - - let base_name = &parsed_input.base_name; - let generic_params = &parsed_input.generics.params; - let mk_ident_path = |ident| Path { - leading_colon: None, - segments: Punctuated::from_iter(std::iter::once(PathSegment { - ident, - arguments: Default::default(), - })), - }; - let generic_args = { - generic_params - .iter() - .map(|arg| match arg { - GenericParam::Lifetime(lifetime) => { - GenericArgument::Lifetime(lifetime.lifetime.clone()) - } - GenericParam::Const(cnst) => GenericArgument::Const(Expr::Path(ExprPath { - path: mk_ident_path(cnst.ident.clone()), - attrs: Vec::new(), - qself: None, - })), - GenericParam::Type(ty) => GenericArgument::Type(Type::Path(TypePath { - path: mk_ident_path(ty.ident.clone()), - qself: None, - })), - }) - .collect::>() - }; - let where_clause = &parsed_input.generics.where_clause; - let to_trait_name = format_ident!("To{}", base_name); - let to_trait_fn_name = format_ident!("to_{}", base_name.to_string().to_case(Case::Snake)); - - let mut enum_variants = Vec::new(); - let mut struct_defs = Vec::new(); - let mut span_arms = Vec::new(); - let mut from_impls = Vec::new(); - let mut to_trait_impls = Vec::new(); - - for variant in parsed_input.variants { - match variant { - VariantInput::Unit(name) => { - let inner_type = name.clone(); - - struct_defs.push(quote! { - #[derive(Debug)] - pub struct #name { - pub span: rnix::TextRange, - } - }); - - enum_variants.push(quote! { #name(#inner_type) }); - span_arms.push(quote! { Self::#name(inner) => inner.span }); - from_impls.push(quote! { - impl <#generic_params> From<#inner_type> for #base_name <#generic_args> #where_clause { - fn from(val: #inner_type) -> Self { #base_name::#name(val) } - } - }); - to_trait_impls.push(quote! { - impl <#generic_params> #to_trait_name <#generic_args> for #name #where_clause { - fn #to_trait_fn_name(self) -> #base_name <#generic_args> { #base_name::from(self) } - } - }); - } - VariantInput::Tuple(name, ty) => { - let field_name = format_ident!("inner"); - - struct_defs.push(quote! { - #[derive(Debug)] - pub struct #name { - pub #field_name: #ty, - pub span: rnix::TextRange, - } - }); - - let inner_type = name.clone(); - enum_variants.push(quote! { #name(#inner_type) }); - span_arms.push(quote! { Self::#name(inner) => inner.span }); - from_impls.push(quote! { - impl <#generic_params> From<#inner_type> for #base_name <#generic_args> #where_clause { - fn from(val: #inner_type) -> Self { #base_name::#name(val) } - } - }); - to_trait_impls.push(quote! { - impl <#generic_params> #to_trait_name <#generic_args> for #name #where_clause { - fn #to_trait_fn_name(self) -> #base_name <#generic_args> { #base_name::from(self) } - } - }); - } - VariantInput::Struct(name, mut fields) => { - let inner_type = name.clone(); - - fields.named.iter_mut().for_each(|field| { - field.vis = syn::Visibility::Public(syn::token::Pub::default()); - }); - fields.named.push(syn::Field { - attrs: vec![], - vis: syn::Visibility::Public(syn::token::Pub::default()), - mutability: syn::FieldMutability::None, - ident: Some(format_ident!("span")), - colon_token: Some(syn::token::Colon::default()), - ty: syn::parse_quote!(rnix::TextRange), - }); - - struct_defs.push(quote! { - #[derive(Debug)] - pub struct #name <#generic_params> #where_clause #fields - }); - enum_variants.push(quote! { #name(#inner_type <#generic_args>) }); - span_arms.push(quote! { Self::#name(inner) => inner.span }); - from_impls.push(quote! { - impl <#generic_params> From<#inner_type <#generic_args>> for #base_name <#generic_args> #where_clause { - fn from(val: #inner_type <#generic_args>) -> Self { #base_name::#name(val) } - } - }); - to_trait_impls.push(quote! { - impl <#generic_params> #to_trait_name <#generic_args> for #name <#generic_args> #where_clause { - fn #to_trait_fn_name(self) -> #base_name <#generic_args> { #base_name::from(self) } - } - }); - } - } - } - - // Assemble the final generated code. - let expanded = quote! { - /// The main IR enum, generated by the `ir!` macro. - #[derive(Debug)] - pub enum #base_name <#generic_params> #where_clause { - #( #enum_variants ),* - } - - // The struct definitions for the enum variants. - #( #struct_defs )* - - impl <#generic_params> #base_name <#generic_args> #where_clause { - pub fn span(&self) -> rnix::TextRange { - match self { - #( #span_arms ),* - } - } - } - - // `From` implementations for converting variant structs into the main enum. - #( #from_impls )* - - /// A trait for converting a variant struct into the main IR enum. - pub trait #to_trait_name <#generic_params> #where_clause { - /// Performs the conversion. - fn #to_trait_fn_name(self) -> #base_name <#generic_args>; - } - - // Implement the `ToIr` trait for each variant struct. - #( #to_trait_impls )* - }; - - TokenStream::from(expanded) -} diff --git a/nix-js-macros/src/lib.rs b/nix-js-macros/src/lib.rs deleted file mode 100644 index bee68f8..0000000 --- a/nix-js-macros/src/lib.rs +++ /dev/null @@ -1,13 +0,0 @@ -//! This crate provides procedural macros for the nix-js project. -use proc_macro::TokenStream; - -mod ir; - -/// A procedural macro to reduce boilerplate when defining an Intermediate Representation (IR). -/// -/// It generates an enum for the IR, along with `Ref` and `Mut` variants, -/// `From` implementations, and a `ToIr` trait. -#[proc_macro] -pub fn ir(input: TokenStream) -> TokenStream { - ir::ir_impl(input) -} diff --git a/nix-js/Cargo.toml b/nix-js/Cargo.toml index c04788b..88b6fbd 100644 --- a/nix-js/Cargo.toml +++ b/nix-js/Cargo.toml @@ -2,7 +2,6 @@ name = "nix-js" version = "0.1.0" edition = "2024" -build = "build.rs" [dependencies] mimalloc = "0.1" @@ -54,7 +53,7 @@ bzip2 = "0.6" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" # spec 1.0.0 -toml = "0.9.9" +toml = "=0.9.9" dirs = "6.0" tempfile = "3.24" rusqlite = { version = "0.38", features = ["bundled"] } @@ -62,7 +61,6 @@ rusqlite = { version = "0.38", features = ["bundled"] } rnix = "0.14" rowan = "0.16" -nix-js-macros = { path = "../nix-js-macros" } ere = "0.2.4" num_enum = "0.7.5" tap = "1.0.1" @@ -74,8 +72,13 @@ hyper-util = { version = "0.1", features = ["tokio"], optional = true } http-body-util = { version = "0.1", optional = true } http = { version = "1", optional = true } uuid = { version = "1", features = ["v4"], optional = true } -ghost-cell = "0.2.6" -colored = "3.1.1" + +ghost-cell = "0.2" +colored = "3.1" +boxing = "0.1" +gc-arena = { version = "0.5.3", features = ["allocator-api2"] } +allocator-api2 = "0.4.0" +smallvec = "1.15.1" [features] inspector = ["dep:fastwebsockets", "dep:hyper", "dep:hyper-util", "dep:http-body-util", "dep:http", "dep:uuid"] diff --git a/nix-js/build.rs b/nix-js/build.rs deleted file mode 100644 index 56e09e1..0000000 --- a/nix-js/build.rs +++ /dev/null @@ -1,70 +0,0 @@ -use std::path::Path; -use std::process::Command; - -fn main() { - let runtime_ts_dir = Path::new("runtime-ts"); - let dist_runtime = runtime_ts_dir.join("dist/runtime.js"); - - if !runtime_ts_dir.exists() { - println!("cargo::warning=runtime-ts directory not found, using existing runtime.js"); - return; - } - - println!("cargo::rerun-if-changed=runtime-ts/src"); - println!("cargo::rerun-if-changed=runtime-ts/package.json"); - println!("cargo::rerun-if-changed=runtime-ts/tsconfig.json"); - println!("cargo::rerun-if-changed=runtime-ts/build.mjs"); - - if !runtime_ts_dir.join("node_modules").exists() { - println!("Installing npm dependencies..."); - let npm_cmd = if cfg!(target_os = "windows") { - "npm.cmd" - } else { - "npm" - }; - let status = Command::new(npm_cmd) - .arg("install") - .current_dir(runtime_ts_dir) - .status() - .expect("Failed to run npm install. Is Node.js installed?"); - - if !status.success() { - panic!("npm install failed. Please check your Node.js installation."); - } - } - - println!("Running TypeScript type checking..."); - let npm_cmd = if cfg!(target_os = "windows") { - "npm.cmd" - } else { - "npm" - }; - let status = Command::new(npm_cmd) - .arg("run") - .arg("typecheck") - .current_dir(runtime_ts_dir) - .status() - .expect("Failed to run type checking"); - - if !status.success() { - panic!("TypeScript type checking failed! Fix type errors before building."); - } - - println!("Building runtime.js from TypeScript..."); - let status = Command::new(npm_cmd) - .arg("run") - .arg("build") - .current_dir(runtime_ts_dir) - .status() - .expect("Failed to build runtime"); - - if !status.success() { - panic!("Runtime build failed!"); - } - - if dist_runtime.exists() { - println!("Successfully built runtime.js",); - } else { - panic!("dist/runtime.js not found after build"); - } -} diff --git a/nix-js/runtime-ts/package-lock.json b/nix-js/runtime-ts/package-lock.json index af4cafe..9ac7154 100644 --- a/nix-js/runtime-ts/package-lock.json +++ b/nix-js/runtime-ts/package-lock.json @@ -9,12 +9,12 @@ "version": "0.1.0", "dependencies": { "globals": "^17.3.0", - "jiti": "^2.6.1", "js-sdsl": "^4.4.2" }, "devDependencies": { "esbuild": "^0.24.2", "eslint": "^9.39.2", + "jiti": "^2.6.1", "typescript": "^5.7.2", "typescript-eslint": "^8.55.0" } @@ -1518,6 +1518,7 @@ "version": "2.6.1", "resolved": "https://registry.npmmirror.com/jiti/-/jiti-2.6.1.tgz", "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, "license": "MIT", "peer": true, "bin": { diff --git a/nix-js/src/runtime.rs b/nix-js/src/runtime.rs index f45227c..d809527 100644 --- a/nix-js/src/runtime.rs +++ b/nix-js/src/runtime.rs @@ -15,6 +15,7 @@ use crate::value::{AttrSet, List, Symbol, Value}; pub(crate) mod inspector; mod ops; use ops::*; +mod value; type ScopeRef<'p, 's> = v8::PinnedRef<'p, v8::HandleScope<'s>>; type LocalValue<'a> = v8::Local<'a, v8::Value>; diff --git a/nix-js/src/runtime/value.rs b/nix-js/src/runtime/value.rs new file mode 100644 index 0000000..10e754b --- /dev/null +++ b/nix-js/src/runtime/value.rs @@ -0,0 +1,500 @@ +use std::fmt; +use std::marker::PhantomData; + +use boxing::nan::raw::{RawBox, RawStore, RawTag, Value as RawValue}; +use gc_arena::{Collect, Gc}; +use hashbrown::HashTable; + +// --------------------------------------------------------------------------- +// Tag layout +// --------------------------------------------------------------------------- +// +// Positive tags (sign=false) — inline data in 6 bytes: +// 1: SmallInt — i32 +// 2: Bool — u8 (0 or 1) +// 3: Null — no payload +// 4: SmallString — SmallStringId (u32) +// +// Negative tags (sign=true) — GC heap pointer (48-bit address): +// 1: BigInt → Gc<'gc, i64> +// 2: String → Gc<'gc, NixString> +// 3: SmallAttrSet → Gc<'gc, SmallAttrSet<'gc>> +// 4: AttrSet → Gc<'gc, AttrSet<'gc>> +// 5: List → Gc<'gc, Box<[Value<'gc>]>> +// +// Floats are stored directly as f64 (no tag). + +const TAG_SMI: (bool, u8) = (false, 1); +const TAG_BOOL: (bool, u8) = (false, 2); +const TAG_NULL: (bool, u8) = (false, 3); +const TAG_SMALL_STRING: (bool, u8) = (false, 4); +const TAG_BIG_INT: (bool, u8) = (true, 1); +const TAG_STRING: (bool, u8) = (true, 2); +const TAG_SMALL_ATTRS: (bool, u8) = (true, 3); +const TAG_ATTRS: (bool, u8) = (true, 4); +const TAG_LIST: (bool, u8) = (true, 5); + +/// # Nix runtime value representation +/// +/// NaN-boxed value fitting in 8 bytes. Morally equivalent to: +/// ```ignore +/// enum NixValue<'gc> { +/// Float(SingleNaNF64), +/// SmallInt(i32), +/// BigInt(Gc<'gc, i64>), +/// Bool(bool), +/// Null, +/// SmallString(SmallStringId), +/// String(Gc<'gc, NixString>), +/// SmallAttrSet(Gc<'gc, SmallAttrSet<'gc>>), +/// AttrSet(Gc<'gc, AttrSet<'gc>>), +/// List(Gc<'gc, Box<[Value<'gc>]>>), +/// } +/// ``` +#[repr(transparent)] +pub(crate) struct Value<'gc> { + raw: RawBox, + _marker: PhantomData>, +} + +impl<'gc> Clone for Value<'gc> { + #[inline] + fn clone(&self) -> Self { + Self { + raw: self.raw.clone(), + _marker: PhantomData, + } + } +} + +unsafe impl<'gc> Collect for Value<'gc> { + fn trace(&self, cc: &gc_arena::Collection) { + let Some(tag) = self.raw.tag() else { return }; + let (neg, val) = tag.neg_val(); + if !neg { + return; // inline values hold no GC pointers + } + // Negative tags are heap pointers — reconstruct the Gc and trace it. + unsafe { + match val { + 1 => self.load_gc::().trace(cc), + 2 => self.load_gc::().trace(cc), + 3 => self.load_gc::>().trace(cc), + 4 => self.load_gc::>().trace(cc), + 5 => self.load_gc::]>>().trace(cc), + _ => debug_assert!(false, "invalid negative tag value: {val}"), + } + } + } + + fn needs_trace() -> bool + where + Self: Sized, + { + true + } +} + +// --------------------------------------------------------------------------- +// Private helpers +// --------------------------------------------------------------------------- + +impl<'gc> Value<'gc> { + #[inline(always)] + fn mk_tag(neg: bool, val: u8) -> RawTag { + debug_assert!((1..=7).contains(&val)); + // Safety: val is asserted to be in 1..=7. + unsafe { RawTag::new_unchecked(neg, val) } + } + + #[inline(always)] + fn from_raw_value(rv: RawValue) -> Self { + Self { + raw: RawBox::from_value(rv), + _marker: PhantomData, + } + } + + /// Store a GC pointer with the given (negative) tag value. + #[inline(always)] + fn store_gc(tag_val: u8, gc: Gc<'gc, T>) -> Self { + let ptr = Gc::as_ptr(gc); + Self::from_raw_value(RawValue::store(Self::mk_tag(true, tag_val), ptr)) + } + + /// Load a GC pointer from a value with a negative tag. + /// + /// # Safety + /// + /// The value must actually store a `Gc<'gc, T>` with the matching type. + #[inline(always)] + unsafe fn load_gc(&self) -> Gc<'gc, T> { + unsafe { + let rv = self.raw.value().unwrap_unchecked(); + let ptr: *const T = <*const T as RawStore>::from_val(rv); + Gc::from_ptr(ptr) + } + } + + /// Returns the `(negative, val)` tag, or `None` for a float. + #[inline(always)] + fn tag_nv(&self) -> Option<(bool, u8)> { + self.raw.tag().map(|t| t.neg_val()) + } +} + +// --------------------------------------------------------------------------- +// Constructors +// --------------------------------------------------------------------------- + +impl<'gc> Value<'gc> { + #[inline] + pub(crate) fn new_float(val: f64) -> Self { + Self { + raw: RawBox::from_float(val), + _marker: PhantomData, + } + } + + #[inline] + pub(crate) fn new_smi(val: i32) -> Self { + Self::from_raw_value(RawValue::store(Self::mk_tag(TAG_SMI.0, TAG_SMI.1), val)) + } + + #[inline] + pub(crate) fn new_int(gc: Gc<'gc, i64>) -> Self { + Self::store_gc(TAG_BIG_INT.1, gc) + } + + #[inline] + pub(crate) fn new_bool(val: bool) -> Self { + Self::from_raw_value(RawValue::store( + Self::mk_tag(TAG_BOOL.0, TAG_BOOL.1), + val, + )) + } + + #[inline] + pub(crate) fn new_null() -> Self { + Self::from_raw_value(RawValue::empty(Self::mk_tag(TAG_NULL.0, TAG_NULL.1))) + } + + #[inline] + pub(crate) fn new_small_string(id: SmallStringId) -> Self { + Self::from_raw_value(RawValue::store( + Self::mk_tag(TAG_SMALL_STRING.0, TAG_SMALL_STRING.1), + id.0, + )) + } + + #[inline] + pub(crate) fn new_string(gc: Gc<'gc, NixString>) -> Self { + Self::store_gc(TAG_STRING.1, gc) + } + + #[inline] + pub(crate) fn new_small_attrs(gc: Gc<'gc, SmallAttrSet<'gc>>) -> Self { + Self::store_gc(TAG_SMALL_ATTRS.1, gc) + } + + #[inline] + pub(crate) fn new_attrs(gc: Gc<'gc, AttrSet<'gc>>) -> Self { + Self::store_gc(TAG_ATTRS.1, gc) + } + + #[inline] + pub(crate) fn new_list(gc: Gc<'gc, Box<[Value<'gc>]>>) -> Self { + Self::store_gc(TAG_LIST.1, gc) + } +} + +// --------------------------------------------------------------------------- +// Type checks +// --------------------------------------------------------------------------- + +impl<'gc> Value<'gc> { + #[inline] + pub(crate) fn is_float(&self) -> bool { + self.raw.is_float() + } + + #[inline] + pub(crate) fn is_smi(&self) -> bool { + self.tag_nv() == Some(TAG_SMI) + } + + #[inline] + pub(crate) fn is_big_int(&self) -> bool { + self.tag_nv() == Some(TAG_BIG_INT) + } + + /// True for float, small int, or big int. + #[inline] + pub(crate) fn is_number(&self) -> bool { + match self.tag_nv() { + None => true, + Some(TAG_SMI) | Some(TAG_BIG_INT) => true, + _ => false, + } + } + + #[inline] + pub(crate) fn is_bool(&self) -> bool { + self.tag_nv() == Some(TAG_BOOL) + } + + #[inline] + pub(crate) fn is_null(&self) -> bool { + self.tag_nv() == Some(TAG_NULL) + } + + #[inline] + pub(crate) fn is_small_string(&self) -> bool { + self.tag_nv() == Some(TAG_SMALL_STRING) + } + + #[inline] + pub(crate) fn is_heap_string(&self) -> bool { + self.tag_nv() == Some(TAG_STRING) + } + + /// True for small string or heap string. + #[inline] + pub(crate) fn is_string(&self) -> bool { + matches!(self.tag_nv(), Some(TAG_SMALL_STRING | TAG_STRING)) + } + + #[inline] + pub(crate) fn is_small_attrs(&self) -> bool { + self.tag_nv() == Some(TAG_SMALL_ATTRS) + } + + #[inline] + pub(crate) fn is_heap_attrs(&self) -> bool { + self.tag_nv() == Some(TAG_ATTRS) + } + + /// True for small or heap attr set. + #[inline] + pub(crate) fn is_attrs(&self) -> bool { + matches!(self.tag_nv(), Some(TAG_SMALL_ATTRS | TAG_ATTRS)) + } + + #[inline] + pub(crate) fn is_list(&self) -> bool { + self.tag_nv() == Some(TAG_LIST) + } +} + +// --------------------------------------------------------------------------- +// Accessors +// --------------------------------------------------------------------------- + +impl<'gc> Value<'gc> { + #[inline] + pub(crate) fn as_float(&self) -> Option { + self.raw.float().copied() + } + + #[inline] + pub(crate) fn as_smi(&self) -> Option { + if self.is_smi() { + Some(unsafe { + let rv = self.raw.value().unwrap_unchecked(); + ::from_val(rv) + }) + } else { + None + } + } + + #[inline] + pub(crate) fn as_big_int(&self) -> Option> { + if self.is_big_int() { + Some(unsafe { self.load_gc() }) + } else { + None + } + } + + /// Read the integer value as `i64` regardless of smi/big-int representation. + #[inline] + pub(crate) fn as_i64(&self) -> Option { + match self.tag_nv() { + Some(TAG_SMI) => Some(unsafe { + let rv = self.raw.value().unwrap_unchecked(); + ::from_val(rv) as i64 + }), + Some(TAG_BIG_INT) => Some(unsafe { *self.load_gc::() }), + _ => None, + } + } + + #[inline] + pub(crate) fn as_bool(&self) -> Option { + if self.is_bool() { + Some(unsafe { + let rv = self.raw.value().unwrap_unchecked(); + ::from_val(rv) + }) + } else { + None + } + } + + #[inline] + pub(crate) fn as_small_string(&self) -> Option { + if self.is_small_string() { + Some(SmallStringId(unsafe { + let rv = self.raw.value().unwrap_unchecked(); + ::from_val(rv) + })) + } else { + None + } + } + + #[inline] + pub(crate) fn as_heap_string(&self) -> Option> { + if self.is_heap_string() { + Some(unsafe { self.load_gc() }) + } else { + None + } + } + + #[inline] + pub(crate) fn as_small_attr_set(&self) -> Option>> { + if self.is_small_attrs() { + Some(unsafe { self.load_gc() }) + } else { + None + } + } + + #[inline] + pub(crate) fn as_heap_attr_set(&self) -> Option>> { + if self.is_heap_attrs() { + Some(unsafe { self.load_gc() }) + } else { + None + } + } + + #[inline] + pub(crate) fn as_list(&self) -> Option]>>> { + if self.is_list() { + Some(unsafe { self.load_gc() }) + } else { + None + } + } +} + +// --------------------------------------------------------------------------- +// Debug +// --------------------------------------------------------------------------- + +impl fmt::Debug for Value<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.tag_nv() { + None => { + let v = self.raw.float().unwrap(); + write!(f, "Float({v:?})") + } + Some(TAG_SMI) => write!(f, "SmallInt({:?})", self.as_smi().unwrap()), + Some(TAG_BOOL) => write!(f, "Bool({:?})", self.as_bool().unwrap()), + Some(TAG_NULL) => write!(f, "Null"), + Some(TAG_SMALL_STRING) => { + write!(f, "SmallString({:?})", self.as_small_string().unwrap()) + } + Some(TAG_BIG_INT) => write!(f, "BigInt(Gc<..>)"), + Some(TAG_STRING) => write!(f, "String(Gc<..>)"), + Some(TAG_SMALL_ATTRS) => write!(f, "SmallAttrSet(Gc<..>)"), + Some(TAG_ATTRS) => write!(f, "AttrSet(Gc<..>)"), + Some(TAG_LIST) => write!(f, "List(Gc<..>)"), + Some((neg, val)) => write!(f, "Unknown(neg={neg}, val={val})"), + } + } +} + +// =========================================================================== +// Supporting types +// =========================================================================== + +// TODO: size? +#[repr(transparent)] +#[derive(Clone, Copy, PartialEq, Eq, Debug, Collect)] +#[collect(require_static)] +pub(crate) struct SmallStringId(u32); + +/// Heap-allocated Nix string. +/// +/// Stored on the GC heap via `Gc<'gc, NixString>`. The string data itself +/// lives in a standard `Box` owned by this struct; the GC only manages +/// the outer allocation. +#[derive(Collect)] +#[collect(require_static)] +pub(crate) struct NixString { + data: Box, + // TODO: string context for derivation dependency tracking +} + +impl NixString { + pub(crate) fn new(s: impl Into>) -> Self { + Self { data: s.into() } + } + + pub(crate) fn as_str(&self) -> &str { + &self.data + } +} + +impl fmt::Debug for NixString { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(&self.data, f) + } +} + +/// Fixed-size attribute set (up to 8 entries). +#[derive(Collect)] +#[collect(no_drop)] +pub(crate) struct SmallAttrSet<'gc> { + // TODO: proper key storage, length tracking, and lookup + inner: [Value<'gc>; 8], +} + +/// Hash-table-backed attribute set. +pub(crate) struct AttrSet<'gc> { + inner: HashTable>, +} + +unsafe impl<'gc> Collect for AttrSet<'gc> { + fn trace(&self, cc: &gc_arena::Collection) { + for entry in self.inner.iter() { + Collect::trace(&entry.key, cc); + Collect::trace(&entry.value, cc); + } + } + + fn needs_trace() -> bool + where + Self: Sized, + { + true + } +} + +#[derive(Collect)] +#[collect(no_drop)] +struct AttrSetEntry<'gc> { + key: AttrKey<'gc>, + value: Value<'gc>, +} + +#[derive(Collect)] +#[collect(no_drop)] +pub(crate) enum AttrKey<'gc> { + Small(SmallStringId), + Large(Gc<'gc, str>), +}