diff --git a/Cargo.lock b/Cargo.lock index d5d29f2..f7f279e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + [[package]] name = "adler2" version = "2.0.1" @@ -79,6 +88,30 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b7e4c2464d97fe331d41de9d5db0def0a96f4d823b8b32a2efd503578988973" +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-link", +] + +[[package]] +name = "backtrace-ext" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "537beee3be4a18fb023b570f80e3ae28003db9167a751266b259926e25539d50" +dependencies = [ + "backtrace", +] + [[package]] name = "base64" version = "0.22.1" @@ -1125,6 +1158,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + [[package]] name = "glob" version = "0.3.3" @@ -1523,6 +1562,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "is_ci" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" + [[package]] name = "is_executable" version = "1.0.5" @@ -1735,6 +1780,36 @@ dependencies = [ "autocfg", ] +[[package]] +name = "miette" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7" +dependencies = [ + "backtrace", + "backtrace-ext", + "cfg-if", + "miette-derive", + "owo-colors", + "supports-color", + "supports-hyperlinks", + "supports-unicode", + "terminal_size", + "textwrap", + "unicode-width 0.1.14", +] + +[[package]] +name = "miette-derive" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "mimalloc" version = "0.1.48" @@ -1830,6 +1905,7 @@ name = "nix-js" version = "0.1.0" dependencies = [ "anyhow", + "base64", "bzip2", "criterion", "deno_core", @@ -1840,6 +1916,7 @@ dependencies = [ "hashbrown 0.16.1", "hex", "itertools 0.14.0", + "miette", "mimalloc", "nix-compat", "nix-js-macros", @@ -1848,11 +1925,13 @@ dependencies = [ "regex", "reqwest", "rnix", + "rowan", "rusqlite", "rustyline", "serde", "serde_json", "sha2", + "sourcemap", "string-interner", "tar", "tempfile", @@ -1971,6 +2050,15 @@ dependencies = [ "syn", ] +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -1995,6 +2083,12 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" +[[package]] +name = "owo-colors" +version = "4.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52" + [[package]] name = "parking_lot" version = "0.12.5" @@ -2493,6 +2587,12 @@ dependencies = [ "smallvec", ] +[[package]] +name = "rustc-demangle" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" + [[package]] name = "rustc-hash" version = "1.1.0" @@ -2598,7 +2698,7 @@ dependencies = [ "nix", "radix_trie", "unicode-segmentation", - "unicode-width", + "unicode-width 0.1.14", "utf8parse", "windows-sys 0.52.0", ] @@ -2876,6 +2976,27 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "supports-color" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c64fc7232dd8d2e4ac5ce4ef302b1d81e0b80d055b9d77c7c4f51f6aa4c867d6" +dependencies = [ + "is_ci", +] + +[[package]] +name = "supports-hyperlinks" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e396b6523b11ccb83120b115a0b7366de372751aa6edf19844dfb13a6af97e91" + +[[package]] +name = "supports-unicode" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2" + [[package]] name = "symlink" version = "0.1.0" @@ -3007,12 +3128,32 @@ dependencies = [ "writeable", ] +[[package]] +name = "terminal_size" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" +dependencies = [ + "rustix 1.1.3", + "windows-sys 0.60.2", +] + [[package]] name = "text-size" version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f18aa187839b2bdb1ad2fa35ead8c4c2976b64e4363c386d45ac0f7ee85c9233" +[[package]] +name = "textwrap" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" +dependencies = [ + "unicode-linebreak", + "unicode-width 0.2.2", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -3327,6 +3468,12 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + [[package]] name = "unicode-segmentation" version = "1.12.0" @@ -3339,6 +3486,12 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + [[package]] name = "unicode-xid" version = "0.2.6" diff --git a/nix-js-macros/src/ir.rs b/nix-js-macros/src/ir.rs index 4b32a87..0bee8ea 100644 --- a/nix-js-macros/src/ir.rs +++ b/nix-js-macros/src/ir.rs @@ -99,6 +99,14 @@ pub fn ir_impl(input: TokenStream) -> TokenStream { 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) }); ref_variants.push(quote! { #name(&'a #inner_type) }); mut_variants.push(quote! { #name(&'a mut #inner_type) }); @@ -116,14 +124,45 @@ pub fn ir_impl(input: TokenStream) -> TokenStream { }); } VariantInput::Tuple(name, ty) => { - enum_variants.push(quote! { #name(#ty) }); - ref_variants.push(quote! { #name(&'a #ty) }); - mut_variants.push(quote! { #name(&'a mut #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) }); + ref_variants.push(quote! { #name(&'a #inner_type) }); + mut_variants.push(quote! { #name(&'a mut #inner_type) }); as_ref_arms.push(quote! { Self::#name(inner) => #ref_name::#name(inner) }); as_mut_arms.push(quote! { Self::#name(inner) => #mut_name::#name(inner) }); + from_impls.push(quote! { + impl From<#inner_type> for #base_name { + fn from(val: #inner_type) -> Self { #base_name::#name(val) } + } + }); + to_trait_impls.push(quote! { + impl #to_trait_name for #name { + fn #to_trait_fn_name(self) -> #base_name { #base_name::from(self) } + } + }); } - VariantInput::Struct(name, fields) => { + VariantInput::Struct(name, mut fields) => { let inner_type = name.clone(); + + 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 #fields diff --git a/nix-js/Cargo.toml b/nix-js/Cargo.toml index c9866d0..4445a9b 100644 --- a/nix-js/Cargo.toml +++ b/nix-js/Cargo.toml @@ -24,6 +24,7 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] } derive_more = { version = "2", features = ["full"] } thiserror = "2" +miette = { version = "7.4", features = ["fancy"] } hashbrown = "0.16" petgraph = "0.8" @@ -40,6 +41,9 @@ nix-nar = "0.3" sha2 = "0.10" hex = "0.4" +sourcemap = "9.0" +base64 = "0.22" + # Fetcher dependencies reqwest = { version = "0.12", features = ["blocking", "rustls-tls"], default-features = false } tar = "0.4" @@ -54,6 +58,7 @@ tempfile = "3.24" rusqlite = { version = "0.33", features = ["bundled"] } rnix = "0.12" +rowan = "0.15" nix-js-macros = { path = "../nix-js-macros" } diff --git a/nix-js/runtime-ts/src/builtins/io.ts b/nix-js/runtime-ts/src/builtins/io.ts index e264bd2..e111a9d 100644 --- a/nix-js/runtime-ts/src/builtins/io.ts +++ b/nix-js/runtime-ts/src/builtins/io.ts @@ -360,20 +360,15 @@ export const toFile = (contentsArg: NixValue): StringWithContext => { const name = forceString(nameArg); - if (name.includes('/')) { + if (name.includes("/")) { throw new Error("builtins.toFile: name cannot contain '/'"); } - if (name === '.' || name === '..') { + if (name === "." || name === "..") { throw new Error("builtins.toFile: invalid name"); } const context: NixStringContext = new Set(); - const contents = coerceToString( - contentsArg, - StringCoercionMode.ToString, - false, - context - ); + const contents = coerceToString(contentsArg, StringCoercionMode.ToString, false, context); const references: string[] = Array.from(context); diff --git a/nix-js/runtime-ts/src/helpers.ts b/nix-js/runtime-ts/src/helpers.ts index 623ea7c..9060a38 100644 --- a/nix-js/runtime-ts/src/helpers.ts +++ b/nix-js/runtime-ts/src/helpers.ts @@ -11,6 +11,75 @@ import { force } from "./thunk"; import { mkPath } from "./path"; import { CatchableError, isNixPath } from "./types"; +interface StackFrame { + span: string; + message: string; +} + +const callStack: StackFrame[] = []; +const MAX_STACK_DEPTH = 1000; + +export const STACK_TRACE = { enabled: false }; + +function enrichError(error: unknown): Error { + const err = error instanceof Error ? error : new Error(String(error)); + + if (!STACK_TRACE.enabled || callStack.length === 0) { + return err; + } + + // Use compact format for easy parsing (no regex needed) + // Format: NIX_STACK_FRAME:context:start:end:message + const nixStackLines = callStack.map((frame) => { + return `NIX_STACK_FRAME:context:${frame.span}:${frame.message}`; + }); + + // Prepend stack frames to error stack + err.stack = `${nixStackLines.join('\n')}\n${err.stack || ''}`; + + return err; +} + +/** + * Push an error context onto the stack + * Used for tracking evaluation context (e.g., "while evaluating the condition") + */ +export const pushContext = (message: string, span: string): void => { + if (!STACK_TRACE.enabled) return; + + if (callStack.length >= MAX_STACK_DEPTH) { + callStack.shift(); + } + callStack.push({ span, message }); +}; + +/** + * Pop an error context from the stack + */ +export const popContext = (): void => { + if (!STACK_TRACE.enabled) return; + callStack.pop(); +}; + +/** + * Execute a function with error context tracking + * Automatically pushes context before execution and pops after + */ +export const withContext = (message: string, span: string, fn: () => T): T => { + if (!STACK_TRACE.enabled) { + return fn(); + } + + pushContext(message, span); + try { + return fn(); + } catch (error) { + throw enrichError(error); + } finally { + popContext(); + } +}; + /** * Concatenate multiple values into a string or path with context * This is used for string interpolation like "hello ${world}" @@ -107,7 +176,29 @@ export const resolvePath = (currentDir: string, path: NixValue): NixPath => { return mkPath(resolved); }; -export const select = (obj: NixValue, attrpath: NixValue[]): NixValue => { +export const select = (obj: NixValue, attrpath: NixValue[], span?: string): NixValue => { + if (STACK_TRACE.enabled && span) { + const pathStrings = attrpath.map(a => forceString(a)); + const path = pathStrings.join('.'); + const message = path ? `while selecting attribute [${path}]` : 'while selecting attribute'; + + if (callStack.length >= MAX_STACK_DEPTH) { + callStack.shift(); + } + callStack.push({ span, message }); + try { + return select_impl(obj, attrpath); + } catch (error) { + throw enrichError(error); + } finally { + callStack.pop(); + } + } else { + return select_impl(obj, attrpath); + } +}; + +function select_impl(obj: NixValue, attrpath: NixValue[]): NixValue { let attrs = forceAttrs(obj); for (const attr of attrpath.slice(0, -1)) { @@ -124,9 +215,31 @@ export const select = (obj: NixValue, attrpath: NixValue[]): NixValue => { throw new Error(`Attribute '${last}' not found`); } return attrs[last]; +} + +export const selectWithDefault = (obj: NixValue, attrpath: NixValue[], default_val: NixValue, span?: string): NixValue => { + if (STACK_TRACE.enabled && span) { + const pathStrings = attrpath.map(a => forceString(a)); + const path = pathStrings.join('.'); + const message = path ? `while selecting attribute [${path}]` : 'while selecting attribute'; + + if (callStack.length >= MAX_STACK_DEPTH) { + callStack.shift(); + } + callStack.push({ span, message }); + try { + return selectWithDefault_impl(obj, attrpath, default_val); + } catch (error) { + throw enrichError(error); + } finally { + callStack.pop(); + } + } else { + return selectWithDefault_impl(obj, attrpath, default_val); + } }; -export const selectWithDefault = (obj: NixValue, attrpath: NixValue[], default_val: NixValue): NixValue => { +function selectWithDefault_impl(obj: NixValue, attrpath: NixValue[], default_val: NixValue): NixValue { let attrs = forceAttrs(obj); for (const attr of attrpath.slice(0, -1)) { @@ -146,7 +259,7 @@ export const selectWithDefault = (obj: NixValue, attrpath: NixValue[], default_v return attrs[last]; } return default_val; -}; +} export const hasAttr = (obj: NixValue, attrpath: NixValue[]): NixBool => { if (!isAttrs(obj)) { @@ -208,7 +321,25 @@ export const validateParams = ( return forced_arg; }; -export const call = (func: NixValue, arg: NixValue): NixValue => { +export const call = (func: NixValue, arg: NixValue, span?: string): NixValue => { + if (STACK_TRACE.enabled && span) { + if (callStack.length >= MAX_STACK_DEPTH) { + callStack.shift(); + } + callStack.push({ span, message: 'from call site' }); + try { + return call_impl(func, arg); + } catch (error) { + throw enrichError(error); + } finally { + callStack.pop(); + } + } else { + return call_impl(func, arg); + } +}; + +function call_impl(func: NixValue, arg: NixValue): NixValue { const forcedFunc = force(func); if (typeof forcedFunc === "function") { return forcedFunc(arg); @@ -223,7 +354,7 @@ export const call = (func: NixValue, arg: NixValue): NixValue => { return forceFunction(functor(forcedFunc))(arg); } throw new Error(`attempt to call something which is not a function but ${typeName(forcedFunc)}`); -}; +} export const assert = (assertion: NixValue, expr: NixValue, assertionRaw: string): NixValue => { if (forceBool(assertion)) { @@ -231,3 +362,10 @@ export const assert = (assertion: NixValue, expr: NixValue, assertionRaw: string } throw new CatchableError(`assertion '${assertionRaw}' failed`); }; + +export const ifFunc = (cond: NixValue, consq: NixValue, alter: NixValue) => { + if (forceBool(cond)) { + return consq; + } + return alter; +}; diff --git a/nix-js/runtime-ts/src/index.ts b/nix-js/runtime-ts/src/index.ts index bd3c0f3..2f33f36 100644 --- a/nix-js/runtime-ts/src/index.ts +++ b/nix-js/runtime-ts/src/index.ts @@ -14,6 +14,10 @@ import { concatStringsWithContext, call, assert, + STACK_TRACE, + pushContext, + popContext, + withContext, } from "./helpers"; import { op } from "./operators"; import { builtins, PRIMOP_METADATA } from "./builtins"; @@ -34,6 +38,7 @@ export const Nix = { HAS_CONTEXT, IS_PATH, DEBUG_THUNKS, + STACK_TRACE, assert, call, @@ -46,6 +51,10 @@ export const Nix = { concatStringsWithContext, StringCoercionMode, + pushContext, + popContext, + withContext, + op, builtins, PRIMOP_METADATA, diff --git a/nix-js/src/bin/eval.rs b/nix-js/src/bin/eval.rs index 3ce4270..3113520 100644 --- a/nix-js/src/bin/eval.rs +++ b/nix-js/src/bin/eval.rs @@ -18,8 +18,8 @@ fn main() -> Result<()> { Ok(()) } Err(err) => { - eprintln!("Error: {err}"); - Err(anyhow::anyhow!("{err}")) + eprintln!("{:?}", miette::Report::new(err)); + exit(1); } } } diff --git a/nix-js/src/bin/repl.rs b/nix-js/src/bin/repl.rs index f1ad163..13c4c14 100644 --- a/nix-js/src/bin/repl.rs +++ b/nix-js/src/bin/repl.rs @@ -33,7 +33,7 @@ fn main() -> Result<()> { } else { match context.eval_code(&line) { Ok(value) => println!("{value}"), - Err(err) => eprintln!("Error: {err}"), + Err(err) => eprintln!("{:?}", miette::Report::new(err)), } } } diff --git a/nix-js/src/codegen.rs b/nix-js/src/codegen.rs index 0e1c892..013d3bb 100644 --- a/nix-js/src/codegen.rs +++ b/nix-js/src/codegen.rs @@ -6,13 +6,25 @@ use crate::ir::*; pub(crate) fn compile(expr: &Ir, ctx: &impl CodegenContext) -> String { let code = expr.compile(ctx); - let debug_prefix = if std::env::var("NIX_JS_DEBUG_THUNKS").is_ok() { - "Nix.DEBUG_THUNKS.enabled=true;" + + let mut debug_flags = Vec::new(); + if std::env::var("NIX_JS_DEBUG_THUNKS").is_ok() { + debug_flags.push("Nix.DEBUG_THUNKS.enabled=true"); + } + if std::env::var("NIX_JS_STACK_TRACE").is_ok() { + debug_flags.push("Nix.STACK_TRACE.enabled=true"); + } + let debug_prefix = if debug_flags.is_empty() { + String::new() } else { - "" + format!("{};", debug_flags.join(";")) }; + let cur_dir = ctx.get_current_dir().display().to_string().escape_quote(); - format!("(()=>{{{}const currentDir={};return {}}})()", debug_prefix, cur_dir, code) + format!( + "(()=>{{{}const currentDir={};return {}}})()", + debug_prefix, cur_dir, code + ) } trait Compile { @@ -48,12 +60,16 @@ impl EscapeQuote for str { } } +fn encode_span(span: rnix::TextRange) -> String { + format!("\"{}:{}\"", usize::from(span.start()), usize::from(span.end())) +} + impl Compile for Ir { fn compile(&self, ctx: &Ctx) -> String { match self { - Ir::Int(int) => format!("{int}n"), // Generate BigInt literal - Ir::Float(float) => float.to_string(), - Ir::Bool(bool) => bool.to_string(), + Ir::Int(int) => format!("{}n", int.inner), // Generate BigInt literal + Ir::Float(float) => float.inner.to_string(), + Ir::Bool(bool) => bool.inner.to_string(), Ir::Null(_) => "null".to_string(), Ir::Str(s) => s.val.escape_quote(), Ir::Path(p) => { @@ -61,11 +77,23 @@ impl Compile for Ir { let path_expr = ctx.get_ir(p.expr).compile(ctx); format!("Nix.resolvePath(currentDir,{})", path_expr) } - &Ir::If(If { cond, consq, alter }) => { - let cond = ctx.get_ir(cond).compile(ctx); + &Ir::If(If { + cond, consq, alter, span + }) => { + let cond_code = ctx.get_ir(cond).compile(ctx); let consq = ctx.get_ir(consq).compile(ctx); let alter = ctx.get_ir(alter).compile(ctx); - format!("({cond})?({consq}):({alter})") + + // Only add context tracking if STACK_TRACE is enabled + if std::env::var("NIX_JS_STACK_TRACE").is_ok() { + let cond_span = encode_span(ctx.get_ir(cond).span()); + format!( + "(Nix.withContext(\"while evaluating a branch condition\",{},()=>({})))?({}):({})", + cond_span, cond_code, consq, alter + ) + } else { + format!("({cond_code})?({consq}):({alter})") + } } Ir::BinOp(x) => x.compile(ctx), Ir::UnOp(x) => x.compile(ctx), @@ -73,18 +101,18 @@ impl Compile for Ir { Ir::AttrSet(x) => x.compile(ctx), Ir::List(x) => x.compile(ctx), Ir::Call(x) => x.compile(ctx), - Ir::Arg(x) => format!("arg{}", x.0), + Ir::Arg(x) => format!("arg{}", x.inner.0), Ir::Let(x) => x.compile(ctx), Ir::Select(x) => x.compile(ctx), - &Ir::Thunk(expr_id) => { + &Ir::Thunk(Thunk { inner: expr_id, .. }) => { let inner = ctx.get_ir(expr_id).compile(ctx); format!("Nix.createThunk(()=>({}),\"expr{}\")", inner, expr_id.0) } - &Ir::ExprRef(expr_id) => { + &Ir::ExprRef(ExprRef { inner: expr_id, .. }) => { format!("expr{}", expr_id.0) } Ir::Builtins(_) => "Nix.builtins".to_string(), - &Ir::Builtin(Builtin(name)) => { + &Ir::Builtin(Builtin { inner: name, .. }) => { format!("Nix.builtins[{}]", ctx.get_sym(name).escape_quote()) } Ir::ConcatStrings(x) => x.compile(ctx), @@ -93,13 +121,24 @@ impl Compile for Ir { assertion, expr, ref assertion_raw, + span, }) => { - let assertion = ctx.get_ir(assertion).compile(ctx); + let assertion_code = ctx.get_ir(assertion).compile(ctx); let expr = ctx.get_ir(expr).compile(ctx); - format!( - "Nix.assert({assertion},{expr},{})", - assertion_raw.escape_quote() - ) + + // Only add context tracking if STACK_TRACE is enabled + if std::env::var("NIX_JS_STACK_TRACE").is_ok() { + let assertion_span = encode_span(ctx.get_ir(assertion).span()); + format!( + "Nix.assert(Nix.withContext(\"while evaluating the condition of the assert statement\",{},()=>({})),{},{})", + assertion_span, assertion_code, expr, assertion_raw.escape_quote() + ) + } else { + format!( + "Nix.assert({},{},{})", + assertion_code, expr, assertion_raw.escape_quote() + ) + } } } } @@ -108,25 +147,43 @@ impl Compile for Ir { impl Compile for BinOp { fn compile(&self, ctx: &Ctx) -> String { use BinOpKind::*; + let lhs = ctx.get_ir(self.lhs).compile(ctx); let rhs = ctx.get_ir(self.rhs).compile(ctx); + + // Only add context tracking if STACK_TRACE is enabled + let stack_trace_enabled = std::env::var("NIX_JS_STACK_TRACE").is_ok(); + + // Helper to wrap operation with context (only if enabled) + let with_ctx = |op_name: &str, op_call: String| { + if stack_trace_enabled { + let span = encode_span(self.span); + format!( + "Nix.withContext(\"while evaluating the {} operator\",{},()=>({}))", + op_name, span, op_call + ) + } else { + op_call + } + }; + match self.kind { - Add => format!("Nix.op.add({},{})", lhs, rhs), - Sub => format!("Nix.op.sub({},{})", lhs, rhs), - Mul => format!("Nix.op.mul({},{})", lhs, rhs), - Div => format!("Nix.op.div({},{})", lhs, rhs), - Eq => format!("Nix.op.eq({},{})", lhs, rhs), - Neq => format!("Nix.op.neq({},{})", lhs, rhs), - Lt => format!("Nix.op.lt({},{})", lhs, rhs), - Gt => format!("Nix.op.gt({},{})", lhs, rhs), - Leq => format!("Nix.op.lte({},{})", lhs, rhs), - Geq => format!("Nix.op.gte({},{})", lhs, rhs), + Add => with_ctx("+", format!("Nix.op.add({},{})", lhs, rhs)), + Sub => with_ctx("-", format!("Nix.op.sub({},{})", lhs, rhs)), + Mul => with_ctx("*", format!("Nix.op.mul({},{})", lhs, rhs)), + Div => with_ctx("/", format!("Nix.op.div({},{})", lhs, rhs)), + Eq => with_ctx("==", format!("Nix.op.eq({},{})", lhs, rhs)), + Neq => with_ctx("!=", format!("Nix.op.neq({},{})", lhs, rhs)), + Lt => with_ctx("<", format!("Nix.op.lt({},{})", lhs, rhs)), + Gt => with_ctx(">", format!("Nix.op.gt({},{})", lhs, rhs)), + Leq => with_ctx("<=", format!("Nix.op.lte({},{})", lhs, rhs)), + Geq => with_ctx(">=", format!("Nix.op.gte({},{})", lhs, rhs)), // Short-circuit operators: use JavaScript native && and || - And => format!("Nix.force({})&&Nix.force({})", lhs, rhs), - Or => format!("Nix.force({})||Nix.force({})", lhs, rhs), - Impl => format!("(!Nix.force({})||Nix.force({}))", lhs, rhs), - Con => format!("Nix.op.concat({},{})", lhs, rhs), - Upd => format!("Nix.op.update({},{})", lhs, rhs), + And => with_ctx("&&", format!("Nix.force({})&&Nix.force({})", lhs, rhs)), + Or => with_ctx("||", format!("Nix.force({})||Nix.force({})", lhs, rhs)), + Impl => with_ctx("->", format!("(!Nix.force({})||Nix.force({}))", lhs, rhs)), + Con => with_ctx("++", format!("Nix.op.concat({},{})", lhs, rhs)), + Upd => with_ctx("//", format!("Nix.op.update({},{})", lhs, rhs)), PipeL => format!("Nix.call({},{})", rhs, lhs), PipeR => format!("Nix.call({},{})", lhs, rhs), } @@ -146,7 +203,7 @@ impl Compile for UnOp { impl Compile for Func { fn compile(&self, ctx: &Ctx) -> String { - let id = ctx.get_ir(self.arg).as_ref().unwrap_arg().0; + let id = ctx.get_ir(self.arg).as_ref().unwrap_arg().inner.0; let body = ctx.get_ir(self.body).compile(ctx); // Generate parameter validation code @@ -170,7 +227,7 @@ impl Func { return String::new(); } - let id = ctx.get_ir(self.arg).as_ref().unwrap_arg().0; + let id = ctx.get_ir(self.arg).as_ref().unwrap_arg().inner.0; // Build required parameter array let required = if let Some(req) = &self.param.required { @@ -203,7 +260,8 @@ impl Compile for Call { fn compile(&self, ctx: &Ctx) -> String { let func = ctx.get_ir(self.func).compile(ctx); let arg = ctx.get_ir(self.arg).compile(ctx); - format!("Nix.call({func},{arg})") + let span_str = encode_span(self.span); + format!("Nix.call({func},{arg},{span_str})") } } @@ -222,7 +280,7 @@ fn should_keep_thunk(ir: &Ir) -> bool { } fn unwrap_thunk(ir: &Ir, ctx: &impl CodegenContext) -> String { - if let Ir::Thunk(inner) = ir { + if let Ir::Thunk(Thunk { inner, .. }) = ir { let inner_ir = ctx.get_ir(*inner); if should_keep_thunk(inner_ir) { ir.compile(ctx) @@ -266,13 +324,14 @@ impl Compile for Select { Attr::Dynamic(expr_id) => ctx.get_ir(*expr_id).compile(ctx), }) .join(","); + let span_str = encode_span(self.span); if let Some(default) = self.default { format!( - "Nix.selectWithDefault({lhs},[{attrpath}],{})", + "Nix.selectWithDefault({lhs},[{attrpath}],{},{span_str})", ctx.get_ir(default).compile(ctx) ) } else { - format!("Nix.select({lhs},[{attrpath}])") + format!("Nix.select({lhs},[{attrpath}],{span_str})") } } } @@ -280,16 +339,38 @@ impl Compile for Select { impl Compile for AttrSet { fn compile(&self, ctx: &Ctx) -> String { let mut attrs = Vec::new(); + let stack_trace_enabled = std::env::var("NIX_JS_STACK_TRACE").is_ok(); for (&sym, &expr) in &self.stcs { let key = ctx.get_sym(sym); - let value = ctx.get_ir(expr).compile(ctx); + let value_code = ctx.get_ir(expr).compile(ctx); + + let value = if stack_trace_enabled { + let value_span = encode_span(ctx.get_ir(expr).span()); + format!( + "Nix.withContext(\"while evaluating the attribute '{}'\",{},()=>({}))", + key, value_span, value_code + ) + } else { + value_code + }; attrs.push(format!("{}:{}", key.escape_quote(), value)); } + // FIXME: duplicated key for (key_expr, value_expr) in &self.dyns { let key = ctx.get_ir(*key_expr).compile(ctx); - let value = ctx.get_ir(*value_expr).compile(ctx); + let value_code = ctx.get_ir(*value_expr).compile(ctx); + + let value = if stack_trace_enabled { + let value_span = encode_span(ctx.get_ir(*value_expr).span()); + format!( + "Nix.withContext(\"while evaluating a dynamic attribute\",{},()=>({}))", + value_span, value_code + ) + } else { + value_code + }; attrs.push(format!("[{}]:{}", key, value)); } @@ -299,10 +380,24 @@ impl Compile for AttrSet { impl Compile for List { fn compile(&self, ctx: &Ctx) -> String { + let stack_trace_enabled = std::env::var("NIX_JS_STACK_TRACE").is_ok(); + let list = self .items .iter() - .map(|item| ctx.get_ir(*item).compile(ctx)) + .enumerate() + .map(|(idx, item)| { + let item_code = ctx.get_ir(*item).compile(ctx); + if stack_trace_enabled { + let item_span = encode_span(ctx.get_ir(*item).span()); + format!( + "Nix.withContext(\"while evaluating list element {}\",{},()=>({}))", + idx, item_span, item_code + ) + } else { + item_code + } + }) .join(","); format!("[{list}]") } @@ -310,10 +405,24 @@ impl Compile for List { impl Compile for ConcatStrings { fn compile(&self, ctx: &Ctx) -> String { + let stack_trace_enabled = std::env::var("NIX_JS_STACK_TRACE").is_ok(); + let parts: Vec = self .parts .iter() - .map(|part| ctx.get_ir(*part).compile(ctx)) + .enumerate() + .map(|(_idx, part)| { + let part_code = ctx.get_ir(*part).compile(ctx); + if stack_trace_enabled { + let part_span = encode_span(ctx.get_ir(*part).span()); + format!( + "Nix.withContext(\"while evaluating a path segment\",{},()=>({}))", + part_span, part_code + ) + } else { + part_code + } + }) .collect(); format!("Nix.concatStringsWithContext([{}])", parts.join(",")) diff --git a/nix-js/src/context.rs b/nix-js/src/context.rs index 0b216b9..bfce895 100644 --- a/nix-js/src/context.rs +++ b/nix-js/src/context.rs @@ -4,12 +4,17 @@ use std::ptr::NonNull; use hashbrown::{HashMap, HashSet}; use itertools::Itertools as _; use petgraph::graphmap::DiGraphMap; +use rnix::TextRange; use string_interner::DefaultStringInterner; use crate::codegen::{CodegenContext, compile}; use crate::error::{Error, Result}; -use crate::ir::{ArgId, Builtin, Downgrade as _, DowngradeContext, ExprId, Ir, SymId, ToIr as _}; +use crate::ir::{ + Arg, ArgId, Bool, Builtin, Downgrade as _, DowngradeContext, ExprId, ExprRef, Ir, Null, SymId, + ToIr as _, synthetic_span, +}; use crate::runtime::{Runtime, RuntimeContext}; +use crate::sourcemap::NixSourceMapBuilder; use crate::store::{StoreBackend, StoreConfig}; use crate::value::Value; use std::sync::Arc; @@ -43,6 +48,9 @@ mod private { fn compile_code(&mut self, expr: &str) -> Result { self.as_mut().compile_code(expr) } + fn get_current_source(&self) -> Option> { + self.as_ref().get_current_source() + } } } use private::CtxPtr; @@ -67,7 +75,11 @@ impl Context { let config = StoreConfig::from_env(); let store = Arc::new(StoreBackend::new(config)?); - Ok(Self { ctx, runtime, store }) + Ok(Self { + ctx, + runtime, + store, + }) } pub fn eval_code(&mut self, expr: &str) -> Result { @@ -83,10 +95,7 @@ impl Context { tracing::debug!("Compiling code"); let code = self.compile_code(expr)?; - self.runtime - .op_state() - .borrow_mut() - .put(self.store.clone()); + self.runtime.op_state().borrow_mut().put(self.store.clone()); tracing::debug!("Executing JavaScript"); self.runtime @@ -112,6 +121,9 @@ pub(crate) struct Ctx { symbols: DefaultStringInterner, global: NonNull>, current_file: Option, + source_map: HashMap>, + current_source: Option>, + js_source_maps: HashMap, } impl Default for Ctx { @@ -122,7 +134,12 @@ impl Default for Ctx { let mut irs = Vec::new(); let mut global = HashMap::new(); - irs.push(Builtins.to_ir()); + irs.push( + Builtins { + span: synthetic_span(), + } + .to_ir(), + ); let builtins_expr = ExprId(0); let builtins_sym = symbols.get_or_intern("builtins"); @@ -150,15 +167,41 @@ impl Default for Ctx { "toString", ]; let consts = [ - ("true", Ir::Bool(true)), - ("false", Ir::Bool(false)), - ("null", Ir::Null(())), + ( + "true", + Bool { + inner: true, + span: synthetic_span(), + } + .to_ir(), + ), + ( + "false", + Bool { + inner: false, + span: synthetic_span(), + } + .to_ir(), + ), + ( + "null", + Null { + span: synthetic_span(), + } + .to_ir(), + ), ]; for name in free_globals { let name_sym = symbols.get_or_intern(name); let id = ExprId(irs.len()); - irs.push(Builtin(name_sym).to_ir()); + irs.push( + Builtin { + inner: name_sym, + span: synthetic_span(), + } + .to_ir(), + ); global.insert(name_sym, id); } for (name, value) in consts { @@ -173,6 +216,9 @@ impl Default for Ctx { irs, global: unsafe { NonNull::new_unchecked(Box::leak(Box::new(global))) }, current_file: None, + source_map: HashMap::new(), + current_source: None, + js_source_maps: HashMap::new(), } } } @@ -195,11 +241,27 @@ impl Ctx { .expect("current_file doesn't have a parent dir") } + pub(crate) fn get_current_source(&self) -> Option> { + self.current_source.clone() + } + fn compile_code(&mut self, expr: &str) -> Result { tracing::debug!("Parsing Nix expression"); + + // Store source text for error reporting + let source: Arc = Arc::from(expr); + self.current_source = Some(source.clone()); + + // Store source in source_map if we have a current_file + if let Some(ref file) = self.current_file { + self.source_map.insert(file.clone(), source.clone()); + } + let root = rnix::Root::parse(expr); if !root.errors().is_empty() { - return Err(Error::parse_error(root.errors().iter().join("; "))); + let error_msg = root.errors().iter().join("; "); + let err = Error::parse_error(error_msg).with_source(source); + return Err(err); } #[allow(clippy::unwrap_used)] @@ -283,8 +345,14 @@ impl DowngradeContext for DowngradeCtx<'_> { ExprId(self.ctx.irs.len() + self.irs.len() - 1) } - fn new_arg(&mut self) -> ExprId { - self.irs.push(Some(Ir::Arg(ArgId(self.arg_id)))); + fn new_arg(&mut self, span: TextRange) -> ExprId { + self.irs.push(Some( + Arg { + inner: ArgId(self.arg_id), + span, + } + .to_ir(), + )); self.arg_id += 1; ExprId(self.ctx.irs.len() + self.irs.len() - 1) } @@ -297,7 +365,7 @@ impl DowngradeContext for DowngradeCtx<'_> { self.ctx.get_sym(id) } - fn lookup(&mut self, sym: SymId) -> Result { + fn lookup(&mut self, sym: SymId, span: TextRange) -> Result { for scope in self.scopes.iter().rev() { match scope { &Scope::Global(global_scope) => { @@ -345,7 +413,7 @@ impl DowngradeContext for DowngradeCtx<'_> { } } - return Ok(self.new_expr(Ir::ExprRef(expr))); + return Ok(self.new_expr(ExprRef { inner: expr, span }.to_ir())); } } &Scope::Param(param_sym, expr) => { @@ -375,10 +443,15 @@ impl DowngradeContext for DowngradeCtx<'_> { expr: namespace, attrpath: vec![Attr::Str(sym)], default: result, // Link to outer With or None + span, }; result = Some(self.new_expr(select.to_ir())); } - result.ok_or_else(|| Error::downgrade_error(format!("'{}' not found", self.get_sym(sym)))) + result.ok_or_else(|| { + Error::downgrade_error(format!("'{}' not found", self.get_sym(sym))) + .with_span(span) + .with_source(self.get_current_source().unwrap_or_else(|| Arc::from(""))) + }) } fn extract_expr(&mut self, id: ExprId) -> Ir { @@ -399,6 +472,19 @@ impl DowngradeContext for DowngradeCtx<'_> { .insert(expr); } + fn get_span(&self, id: ExprId) -> rnix::TextRange { + dbg!(id); + if id.0 >= self.ctx.irs.len() { + return self.ctx.irs.get(id.0).unwrap().span() + } + let local_id = id.0 - self.ctx.irs.len(); + self.irs.get(local_id).unwrap().as_ref().unwrap().span() + } + + fn get_current_source(&self) -> Option> { + self.ctx.current_source.clone() + } + #[allow(refining_impl_trait)] fn reserve_slots(&mut self, slots: usize) -> impl Iterator + Clone + use<> { let start = self.ctx.irs.len() + self.irs.len(); diff --git a/nix-js/src/error.rs b/nix-js/src/error.rs index ab89205..dcbb07c 100644 --- a/nix-js/src/error.rs +++ b/nix-js/src/error.rs @@ -1,135 +1,256 @@ +use miette::{Diagnostic, LabeledSpan, SourceSpan}; use std::sync::Arc; use thiserror::Error; pub type Result = core::result::Result; -#[derive(Error, Debug)] -pub enum ErrorKind { - #[error("error occurred during parse stage: {0}")] - ParseError(String), - #[error("error occurred during downgrade stage: {0}")] - DowngradeError(String), - #[error( - "error occurred during evaluation stage: {msg}{}", - backtrace.as_ref().map_or("".into(), |backtrace| format!("\nBacktrace: {backtrace}")) - )] - EvalError { - msg: String, - backtrace: Option, +#[derive(Error, Debug, Diagnostic)] +pub enum Error { + #[error("Parse error: {message}")] + #[diagnostic(code(nix::parse))] + ParseError { + #[source_code] + src: Option>, + #[label("error occurred here")] + span: Option, + message: String, }, - #[error("internal error occurred: {0}")] - InternalError(String), - #[error("{0}")] - Catchable(String), - #[error("an unknown or unexpected error occurred")] + + #[error("Downgrade error: {message}")] + #[diagnostic(code(nix::downgrade))] + DowngradeError { + #[source_code] + src: Option>, + #[label("{message}")] + span: Option, + message: String, + // #[related] + // related: Vec, + }, + + #[error("Evaluation error: {message}")] + #[diagnostic(code(nix::eval))] + EvalError { + #[source_code] + src: Option>, + #[label("error occurred here")] + span: Option, + message: String, + // #[help] + js_backtrace: Option, + }, + + #[error("Internal error: {message}")] + #[diagnostic(code(nix::internal))] + InternalError { message: String }, + + #[error("{message}")] + #[diagnostic(code(nix::catchable))] + Catchable { message: String }, + + #[error("Unknown error")] + #[diagnostic(code(nix::unknown))] Unknown, } -#[derive(Debug)] -pub struct Error { - pub kind: ErrorKind, - pub span: Option, - pub source: Option>, -} - -impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - // Basic display - write!(f, "{}", self.kind)?; - - // If we have source and span, print context - if let (Some(source), Some(span)) = (&self.source, self.span) { - let start_byte = usize::from(span.start()); - let end_byte = usize::from(span.end()); - - if start_byte > source.len() || end_byte > source.len() { - return Ok(()); // Span is out of bounds - } - - let mut start_line = 1; - let mut start_col = 1usize; - let mut line_start_byte = 0; - for (i, c) in source.char_indices() { - if i >= start_byte { - break; - } - if c == '\n' { - start_line += 1; - start_col = 1; - line_start_byte = i + 1; - } else { - start_col += 1; - } - } - - let line_end_byte = source[line_start_byte..] - .find('\n') - .map(|i| line_start_byte + i) - .unwrap_or(source.len()); - - let line_str = &source[line_start_byte..line_end_byte]; - - let underline_len = if end_byte > start_byte { - end_byte - start_byte - } else { - 1 - }; - - write!(f, "\n --> {}:{}", start_line, start_col)?; - write!(f, "\n |\n")?; - writeln!(f, "{:4} | {}", start_line, line_str)?; - write!( - f, - " | {}{}", - " ".repeat(start_col.saturating_sub(1)), - "^".repeat(underline_len) - )?; - } - Ok(()) - } -} - -impl std::error::Error for Error { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - Some(&self.kind) - } -} - impl Error { - pub fn new(kind: ErrorKind) -> Self { - Self { - kind, + pub fn parse_error(msg: String) -> Self { + Error::ParseError { + src: None, span: None, - source: None, + message: msg, } } - pub fn with_span(mut self, span: rnix::TextRange) -> Self { - self.span = Some(span); - self - } - - pub fn with_source(mut self, source: Arc) -> Self { - self.source = Some(source); - self - } - - pub fn parse_error(msg: String) -> Self { - Self::new(ErrorKind::ParseError(msg)) - } pub fn downgrade_error(msg: String) -> Self { - Self::new(ErrorKind::DowngradeError(msg)) + Error::DowngradeError { + src: None, + span: None, + message: msg, + // related: Vec::new(), + } } + + // pub fn downgrade_error_with_related(msg: String, related: Vec) -> Self { + // Error::DowngradeError { + // src: None, + // span: None, + // message: msg, + // related, + // } + // } + pub fn eval_error(msg: String, backtrace: Option) -> Self { - Self::new(ErrorKind::EvalError { msg, backtrace }) + Error::EvalError { + src: None, + span: None, + message: msg, + js_backtrace: backtrace, + } } + pub fn internal(msg: String) -> Self { - Self::new(ErrorKind::InternalError(msg)) + Error::InternalError { message: msg } } + pub fn catchable(msg: String) -> Self { - Self::new(ErrorKind::Catchable(msg)) + Error::Catchable { message: msg } } + pub fn unknown() -> Self { - Self::new(ErrorKind::Unknown) + Error::Unknown + } + + pub fn with_span(self, span: rnix::TextRange) -> Self { + let source_span = Some(text_range_to_source_span(span)); + match self { + Error::ParseError { src, message, .. } => Error::ParseError { + src, + span: source_span, + message, + }, + Error::DowngradeError { + src, + message, + // related, + .. + } => Error::DowngradeError { + src, + span: source_span, + message, + // related, + }, + Error::EvalError { + src, + message, + js_backtrace, + .. + } => Error::EvalError { + src, + span: source_span, + message, + js_backtrace, + }, + other => other, + } + } + + pub fn with_source(self, source: Arc) -> Self { + let src = Some(source); + match self { + Error::ParseError { span, message, .. } => Error::ParseError { src, span, message }, + Error::DowngradeError { + span, + message, + // related, + .. + } => Error::DowngradeError { + src, + span, + message, + // related, + }, + Error::EvalError { + span, + message, + js_backtrace, + .. + } => Error::EvalError { + src, + span, + message, + js_backtrace, + }, + other => other, + } } } + +pub fn text_range_to_source_span(range: rnix::TextRange) -> SourceSpan { + let start = usize::from(range.start()); + let len = usize::from(range.end()) - start; + SourceSpan::new(start.into(), len) +} + +/// Stack frame types from Nix evaluation +#[derive(Debug, Clone)] +pub(crate) struct NixStackFrame { + pub span: rnix::TextRange, + pub message: String, +} + +/// Parse Nix stack trace from V8 Error.stack +/// Returns vector of stack frames (in order from oldest to newest) +pub(crate) fn parse_nix_stack(stack: &str) -> Vec { + let mut frames = Vec::new(); + + for line in stack.lines() { + if !line.starts_with("NIX_STACK_FRAME:") { + continue; + } + + // Format: NIX_STACK_FRAME:type:start:end[:extra_data] + let rest = line.strip_prefix("NIX_STACK_FRAME:").unwrap(); + let parts: Vec<&str> = rest.splitn(4, ':').collect(); + + if parts.len() < 3 { + continue; + } + + let frame_type = parts[0]; + let start: u32 = match parts[1].parse() { + Ok(v) => v, + Err(_) => continue, + }; + let end: u32 = match parts[2].parse() { + Ok(v) => v, + Err(_) => continue, + }; + + let span = rnix::TextRange::new( + rnix::TextSize::from(start), + rnix::TextSize::from(end) + ); + + // Convert all frame types to context frames with descriptive messages + let message = match frame_type { + "call" => "from call site".to_string(), + "select" => { + let path = if parts.len() >= 4 { parts[3] } else { "" }; + if path.is_empty() { + "while selecting attribute".to_string() + } else { + format!("while selecting attribute [{}]", path) + } + } + "context" => { + if parts.len() >= 4 { + parts[3].to_string() + } else { + String::new() + } + } + _ => continue, + }; + + frames.push(NixStackFrame { span, message }); + } + + // Deduplicate consecutive identical frames + frames.dedup_by(|a, b| a.span == b.span && a.message == b.message); + + frames +} + +/// Format stack trace for display (reversed order, newest at bottom) +pub(crate) fn format_stack_trace(frames: &[NixStackFrame]) -> Vec { + let mut lines = Vec::new(); + + // Reverse order: oldest first, newest last + for frame in frames.iter().rev() { + lines.push(format!("{} at {}:{}", + frame.message, usize::from(frame.span.start()), usize::from(frame.span.end()))); + } + + lines +} diff --git a/nix-js/src/fetcher.rs b/nix-js/src/fetcher.rs index 38299a8..0592755 100644 --- a/nix-js/src/fetcher.rs +++ b/nix-js/src/fetcher.rs @@ -1,5 +1,5 @@ -use deno_core::op2; use deno_core::OpState; +use deno_core::op2; use serde::Serialize; use tracing::{debug, info, warn}; @@ -14,8 +14,8 @@ pub use cache::FetcherCache; pub use download::Downloader; pub use metadata_cache::MetadataCache; -use crate::runtime::NixError; use crate::nar; +use crate::runtime::NixError; #[derive(Serialize)] pub struct FetchUrlResult { @@ -69,8 +69,7 @@ pub fn op_fetch_url( let file_name = name.unwrap_or_else(|| url.rsplit('/').next().unwrap_or("download").to_string()); - let metadata_cache = - MetadataCache::new(3600).map_err(|e| NixError::from(e.to_string()))?; + let metadata_cache = MetadataCache::new(3600).map_err(|e| NixError::from(e.to_string()))?; let input = serde_json::json!({ "type": "file", @@ -156,10 +155,7 @@ pub fn op_fetch_url( .add(&input, &info, &store_path, true) .map_err(|e| NixError::from(e.to_string()))?; - Ok(FetchUrlResult { - store_path, - hash, - }) + Ok(FetchUrlResult { store_path, hash }) } #[op2] @@ -178,8 +174,7 @@ pub fn op_fetch_tarball( info!("fetchTarball started"); let dir_name = name.unwrap_or_else(|| "source".to_string()); - let metadata_cache = - MetadataCache::new(3600).map_err(|e| NixError::from(e.to_string()))?; + let metadata_cache = MetadataCache::new(3600).map_err(|e| NixError::from(e.to_string()))?; let input = serde_json::json!({ "type": "tarball", diff --git a/nix-js/src/fetcher/cache.rs b/nix-js/src/fetcher/cache.rs index aadc067..85c5e11 100644 --- a/nix-js/src/fetcher/cache.rs +++ b/nix-js/src/fetcher/cache.rs @@ -78,7 +78,10 @@ impl FetcherCache { self.hg_cache_dir().join(key) } - pub fn extract_tarball_to_temp(&self, data: &[u8]) -> Result<(PathBuf, tempfile::TempDir), CacheError> { + pub fn extract_tarball_to_temp( + &self, + data: &[u8], + ) -> Result<(PathBuf, tempfile::TempDir), CacheError> { let temp_dir = tempfile::tempdir()?; let extracted_path = super::archive::extract_archive(data, temp_dir.path())?; Ok((extracted_path, temp_dir)) diff --git a/nix-js/src/fetcher/git.rs b/nix-js/src/fetcher/git.rs index 8e55887..5982947 100644 --- a/nix-js/src/fetcher/git.rs +++ b/nix-js/src/fetcher/git.rs @@ -34,7 +34,8 @@ pub fn fetch_git( let nar_hash = crate::nar::compute_nar_hash(&checkout_dir) .map_err(|e| GitError::NarHashError(e.to_string()))?; - let store_path = store.add_to_store_from_path(name, &checkout_dir, vec![]) + let store_path = store + .add_to_store_from_path(name, &checkout_dir, vec![]) .map_err(|e| GitError::StoreError(e.to_string()))?; let rev_count = get_rev_count(&bare_repo, &target_rev)?; diff --git a/nix-js/src/fetcher/metadata_cache.rs b/nix-js/src/fetcher/metadata_cache.rs index e56eb5a..bf80473 100644 --- a/nix-js/src/fetcher/metadata_cache.rs +++ b/nix-js/src/fetcher/metadata_cache.rs @@ -1,4 +1,4 @@ -use rusqlite::{params, Connection, OptionalExtension}; +use rusqlite::{Connection, OptionalExtension, params}; use serde::{Deserialize, Serialize}; use serde_json; use std::path::PathBuf; @@ -72,8 +72,9 @@ impl MetadataCache { .unwrap_or_else(|| PathBuf::from("/tmp")) .join("nix-js"); - std::fs::create_dir_all(&cache_dir) - .map_err(|e| CacheError::Database(rusqlite::Error::ToSqlConversionFailure(Box::new(e))))?; + std::fs::create_dir_all(&cache_dir).map_err(|e| { + CacheError::Database(rusqlite::Error::ToSqlConversionFailure(Box::new(e))) + })?; let db_path = cache_dir.join("fetcher-cache.sqlite"); let conn = Connection::open(db_path)?; @@ -156,15 +157,15 @@ impl MetadataCache { .optional()?; match entry { - Some((input_json, info_json, store_path, immutable, timestamp)) => Ok(Some( - CacheEntry { + Some((input_json, info_json, store_path, immutable, timestamp)) => { + Ok(Some(CacheEntry { input: serde_json::from_str(&input_json)?, info: serde_json::from_str(&info_json)?, store_path, immutable: immutable != 0, timestamp: timestamp as u64, - }, - )), + })) + } None => Ok(None), } } diff --git a/nix-js/src/ir.rs b/nix-js/src/ir.rs index e75cb12..2338329 100644 --- a/nix-js/src/ir.rs +++ b/nix-js/src/ir.rs @@ -1,6 +1,7 @@ use derive_more::{IsVariant, TryUnwrap, Unwrap}; use hashbrown::{HashMap, HashSet}; -use rnix::ast; +use rnix::{TextRange, ast}; +use std::sync::Arc; use string_interner::symbol::SymbolU32; use crate::context::SccInfo; @@ -9,24 +10,29 @@ use crate::value::format_symbol; use nix_js_macros::ir; mod downgrade; +mod span_utils; mod utils; + use utils::*; pub use downgrade::Downgrade; +pub(crate) use span_utils::*; pub trait DowngradeContext { fn downgrade(self, expr: rnix::ast::Expr) -> Result; fn new_expr(&mut self, expr: Ir) -> ExprId; - fn new_arg(&mut self) -> ExprId; + fn new_arg(&mut self, span: TextRange) -> ExprId; fn new_sym(&mut self, sym: String) -> SymId; fn get_sym(&self, id: SymId) -> &str; - fn lookup(&mut self, sym: SymId) -> Result; + fn lookup(&mut self, sym: SymId, span: TextRange) -> Result; fn extract_expr(&mut self, id: ExprId) -> Ir; fn replace_expr(&mut self, id: ExprId, expr: Ir); fn reserve_slots(&mut self, slots: usize) -> impl Iterator + Clone + use; + fn get_span(&self, id: ExprId) -> TextRange; + fn get_current_source(&self) -> Option>; fn with_param_scope(&mut self, param: SymId, arg: ExprId, f: F) -> R where @@ -51,27 +57,57 @@ ir! { Int(i64), Float(f64), Bool(bool), - Null(()), - Str, - AttrSet, - List, + Null, + Str { pub val: String }, + AttrSet { pub stcs: HashMap, pub dyns: Vec<(ExprId, ExprId)> }, + List { pub items: Vec }, - HasAttr, - BinOp, - UnOp, - Select, - If, - Call, - Assert, - ConcatStrings, - Path, - Func, - Let, + HasAttr { pub lhs: ExprId, pub rhs: Vec }, + BinOp { pub lhs: ExprId, pub rhs: ExprId, pub kind: BinOpKind }, + UnOp { pub rhs: ExprId, pub kind: UnOpKind }, + Select { pub expr: ExprId, pub attrpath: Vec, pub default: Option }, + If { pub cond: ExprId, pub consq: ExprId, pub alter: ExprId }, + Call { pub func: ExprId, pub arg: ExprId }, + Assert { pub assertion: ExprId, pub expr: ExprId, pub assertion_raw: String }, + ConcatStrings { pub parts: Vec }, + Path { pub expr: ExprId }, + Func { pub body: ExprId, pub param: Param, pub arg: ExprId }, + Let { pub binding_sccs: SccInfo, pub body: ExprId }, Arg(ArgId), ExprRef(ExprId), Thunk(ExprId), Builtins, - Builtin, + Builtin(SymId), +} + +impl Ir { + pub fn span(&self) -> TextRange { + match self { + Ir::Int(i) => i.span, + Ir::Float(f) => f.span, + Ir::Bool(b) => b.span, + Ir::Null(n) => n.span, + Ir::Str(s) => s.span, + Ir::AttrSet(a) => a.span, + Ir::List(l) => l.span, + Ir::HasAttr(h) => h.span, + Ir::BinOp(b) => b.span, + Ir::UnOp(u) => u.span, + Ir::Select(s) => s.span, + Ir::If(i) => i.span, + Ir::Call(c) => c.span, + Ir::Assert(a) => a.span, + Ir::ConcatStrings(c) => c.span, + Ir::Path(p) => p.span, + Ir::Func(f) => f.span, + Ir::Let(l) => l.span, + Ir::Arg(a) => a.span, + Ir::ExprRef(e) => e.span, + Ir::Thunk(t) => t.span, + Ir::Builtins(b) => b.span, + Ir::Builtin(b) => b.span, + } + } } impl AttrSet { @@ -105,7 +141,12 @@ impl AttrSet { result?; } else { // Create a new sub-attrset because this path doesn't exist yet. - let mut attrs = AttrSet::default(); + // FIXME: span + let mut attrs = AttrSet { + stcs: Default::default(), + dyns: Default::default(), + span: synthetic_span(), + }; attrs._insert(path, name, value, ctx)?; let attrs = ctx.new_expr(attrs.to_ir()); self.stcs.insert(ident, attrs); @@ -115,7 +156,12 @@ impl AttrSet { Attr::Dynamic(dynamic) => { // If the next attribute is a dynamic expression, we must create a new sub-attrset. // We cannot merge with existing dynamic attributes at this stage. - let mut attrs = AttrSet::default(); + // FIXME: span + let mut attrs = AttrSet { + stcs: Default::default(), + dyns: Default::default(), + span: synthetic_span(), + }; attrs._insert(path, name, value, ctx)?; self.dyns.push((dynamic, ctx.new_expr(attrs.to_ir()))); Ok(()) @@ -165,15 +211,6 @@ pub type SymId = SymbolU32; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct ArgId(pub usize); -/// Represents a Nix attribute set. -#[derive(Debug, Default)] -pub struct AttrSet { - /// Statically known attributes (key is a string). - pub stcs: HashMap, - /// Dynamically computed attributes, where both the key and value are expressions. - pub dyns: Vec<(ExprId, ExprId)>, -} - /// Represents a key in an attribute path. #[derive(Debug, TryUnwrap)] pub enum Attr { @@ -185,30 +222,6 @@ pub enum Attr { Str(SymId), } -/// Represents a Nix list. -#[derive(Debug)] -pub struct List { - /// The expressions that are elements of the list. - pub items: Vec, -} - -/// Represents a "has attribute" check (`?` operator). -#[derive(Debug)] -pub struct HasAttr { - /// The expression to check for the attribute (the left-hand side). - pub lhs: ExprId, - /// The attribute path to look for (the right-hand side). - pub rhs: Vec, -} - -/// Represents a binary operation. -#[derive(Debug)] -pub struct BinOp { - pub lhs: ExprId, - pub rhs: ExprId, - pub kind: BinOpKind, -} - /// The kinds of binary operations supported in Nix. #[derive(Clone, Debug)] pub enum BinOpKind { @@ -266,13 +279,6 @@ impl From for BinOpKind { } } -/// Represents a unary operation. -#[derive(Debug)] -pub struct UnOp { - pub rhs: ExprId, - pub kind: UnOpKind, -} - /// The kinds of unary operations. #[derive(Clone, Debug)] pub enum UnOpKind { @@ -289,45 +295,6 @@ impl From for UnOpKind { } } -/// Represents an attribute selection from an attribute set. -#[derive(Debug)] -pub struct Select { - /// The expression that should evaluate to an attribute set. - pub expr: ExprId, - /// The path of attributes to select. - pub attrpath: Vec, - /// An optional default value to return if the selection fails. - pub default: Option, -} - -/// Represents an `if-then-else` expression. -#[derive(Debug)] -pub struct If { - pub cond: ExprId, - pub consq: ExprId, // Consequence (then branch) - pub alter: ExprId, // Alternative (else branch) -} - -/// Represents a function value (a lambda). -#[derive(Debug)] -pub struct Func { - /// The body of the function - pub body: ExprId, - /// The parameter specification for the function. - pub param: Param, - - pub arg: ExprId, -} - -/// Represents a `let ... in ...` expression. -#[derive(Debug)] -pub struct Let { - /// The bindings in the `let` expression, group in SCCs - pub binding_sccs: SccInfo, - /// The body expression evaluated in the scope of the bindings. - pub body: ExprId, -} - /// Describes the parameters of a function. #[derive(Debug)] pub struct Param { @@ -337,50 +304,3 @@ pub struct Param { /// If `None`, any attribute is allowed (ellipsis `...` is present). pub allowed: Option>, } - -/// Represents a function call. -#[derive(Debug)] -pub struct Call { - /// The expression that evaluates to the function to be called. - pub func: ExprId, - pub arg: ExprId, -} - -/// Represents an `assert` expression. -#[derive(Debug)] -pub struct Assert { - /// The condition to assert. - pub assertion: ExprId, - /// The expression to return if the assertion is true. - pub expr: ExprId, - pub assertion_raw: String, -} - -/// Represents the concatenation of multiple string expressions. -/// This is typically the result of downgrading an interpolated string. -#[derive(Debug)] -pub struct ConcatStrings { - pub parts: Vec, -} - -/// Represents a simple, non-interpolated string literal. -#[derive(Debug)] -pub struct Str { - pub val: String, -} - -/// Represents a path literal. -#[derive(Debug)] -pub struct Path { - /// The expression that evaluates to the string content of the path. - /// This can be a simple `Str` or a `ConcatStrings` for interpolated paths. - pub expr: ExprId, -} - -/// Represents the special `builtins` global object. -#[derive(Debug)] -pub struct Builtins; - -/// Represents an attribute in `builtins`. -#[derive(Debug)] -pub struct Builtin(pub SymId); diff --git a/nix-js/src/ir/downgrade.rs b/nix-js/src/ir/downgrade.rs index 1830feb..8e0fe86 100644 --- a/nix-js/src/ir/downgrade.rs +++ b/nix-js/src/ir/downgrade.rs @@ -1,7 +1,10 @@ // Assume no parse error #![allow(clippy::unwrap_used)] -use rnix::ast::{self, Expr, HasEntry}; +use std::sync::Arc; + +use rnix::ast::{self, AstToken, Expr, HasEntry}; +use rowan::ast::AstNode; use super::*; use crate::error::{Error, Result}; @@ -16,7 +19,12 @@ impl Downgrade for Expr { match self { Apply(apply) => apply.downgrade(ctx), Assert(assert) => assert.downgrade(ctx), - Error(error) => Err(self::Error::downgrade_error(error.to_string())), + Error(error) => { + let span = error.syntax().text_range(); + Err(self::Error::downgrade_error(error.to_string()) + .with_span(span) + .with_source(ctx.get_current_source().unwrap_or_else(|| Arc::from("")))) + } IfElse(ifelse) => ifelse.downgrade(ctx), Select(select) => select.downgrade(ctx), Str(str) => str.downgrade(ctx), @@ -44,11 +52,13 @@ impl Downgrade for ast::Assert { let assertion_raw = assertion.to_string(); let assertion = assertion.downgrade(ctx)?; let expr = self.body().unwrap().downgrade(ctx)?; + let span = self.syntax().text_range(); Ok(ctx.new_expr( Assert { assertion, expr, assertion_raw, + span, } .to_ir(), )) @@ -60,18 +70,29 @@ impl Downgrade for ast::IfElse { let cond = self.condition().unwrap().downgrade(ctx)?; let consq = self.body().unwrap().downgrade(ctx)?; let alter = self.else_body().unwrap().downgrade(ctx)?; - Ok(ctx.new_expr(If { cond, consq, alter }.to_ir())) + let span = self.syntax().text_range(); + Ok(ctx.new_expr( + If { + cond, + consq, + alter, + span, + } + .to_ir(), + )) } } impl Downgrade for ast::Path { fn downgrade(self, ctx: &mut Ctx) -> Result { + let span = self.syntax().text_range(); let parts = self .parts() .map(|part| match part { ast::InterpolPart::Literal(lit) => Ok(ctx.new_expr( Str { val: lit.to_string(), + span: lit.syntax().text_range(), } .to_ir(), )), @@ -84,14 +105,15 @@ impl Downgrade for ast::Path { let expr = if parts.len() == 1 { parts.into_iter().next().unwrap() } else { - ctx.new_expr(ConcatStrings { parts }.to_ir()) + ctx.new_expr(ConcatStrings { parts, span }.to_ir()) }; - Ok(ctx.new_expr(Path { expr }.to_ir())) + Ok(ctx.new_expr(Path { expr, span }.to_ir())) } } impl Downgrade for ast::Str { fn downgrade(self, ctx: &mut Ctx) -> Result { + let span = self.syntax().text_range(); let normalized = self.normalized_parts(); let is_single_literal = normalized.len() == 1 && matches!(normalized.first(), Some(ast::InterpolPart::Literal(_))); @@ -99,7 +121,7 @@ impl Downgrade for ast::Str { let parts = normalized .into_iter() .map(|part| match part { - ast::InterpolPart::Literal(lit) => Ok(ctx.new_expr(Str { val: lit }.to_ir())), + ast::InterpolPart::Literal(lit) => Ok(ctx.new_expr(Str { val: lit, span }.to_ir())), ast::InterpolPart::Interpolation(interpol) => { interpol.expr().unwrap().downgrade(ctx) } @@ -109,18 +131,28 @@ impl Downgrade for ast::Str { Ok(if is_single_literal { parts.into_iter().next().unwrap() } else { - ctx.new_expr(ConcatStrings { parts }.to_ir()) + ctx.new_expr(ConcatStrings { parts, span }.to_ir()) }) } } impl Downgrade for ast::Literal { fn downgrade(self, ctx: &mut Ctx) -> Result { + let span = self.syntax().text_range(); Ok(ctx.new_expr(match self.kind() { - ast::LiteralKind::Integer(int) => Ir::Int(int.value().unwrap()), - ast::LiteralKind::Float(float) => Ir::Float(float.value().unwrap()), + ast::LiteralKind::Integer(int) => Int { + inner: int.value().unwrap(), + span, + } + .to_ir(), + ast::LiteralKind::Float(float) => Float { + inner: float.value().unwrap(), + span, + } + .to_ir(), ast::LiteralKind::Uri(uri) => Str { val: uri.to_string(), + span, } .to_ir(), })) @@ -131,13 +163,14 @@ impl Downgrade for ast::Ident { fn downgrade(self, ctx: &mut Ctx) -> Result { let sym = self.ident_token().unwrap().to_string(); let sym = ctx.new_sym(sym); - ctx.lookup(sym) + ctx.lookup(sym, self.syntax().text_range()) } } impl Downgrade for ast::AttrSet { fn downgrade(self, ctx: &mut Ctx) -> Result { let rec = self.rec_token().is_some(); + let span = self.syntax().text_range(); if !rec { let attrs = downgrade_attrs(self, ctx)?; @@ -151,17 +184,26 @@ impl Downgrade for ast::AttrSet { let mut attrs = AttrSet { stcs: HashMap::new(), dyns: Vec::new(), + span, }; for sym in binding_keys { - let expr = ctx.lookup(*sym)?; + // FIXME: span + let expr = ctx.lookup(*sym, synthetic_span())?; attrs.stcs.insert(*sym, expr); } Ok(ctx.new_expr(attrs.to_ir())) })?; - Ok(ctx.new_expr(Let { body, binding_sccs }.to_ir())) + Ok(ctx.new_expr( + Let { + body, + binding_sccs, + span, + } + .to_ir(), + )) } } @@ -172,7 +214,8 @@ impl Downgrade for ast::List { .items() .map(|item| maybe_thunk(item, ctx)) .collect::>()?; - Ok(ctx.new_expr(List { items }.to_ir())) + let span = self.syntax().text_range(); + Ok(ctx.new_expr(List { items, span }.to_ir())) } } @@ -182,7 +225,16 @@ impl Downgrade for ast::BinOp { let lhs = self.lhs().unwrap().downgrade(ctx)?; let rhs = self.rhs().unwrap().downgrade(ctx)?; let kind = self.operator().unwrap().into(); - Ok(ctx.new_expr(BinOp { lhs, rhs, kind }.to_ir())) + let span = self.syntax().text_range(); + Ok(ctx.new_expr( + BinOp { + lhs, + rhs, + kind, + span, + } + .to_ir(), + )) } } @@ -191,7 +243,8 @@ impl Downgrade for ast::HasAttr { fn downgrade(self, ctx: &mut Ctx) -> Result { let lhs = self.expr().unwrap().downgrade(ctx)?; let rhs = downgrade_attrpath(self.attrpath().unwrap(), ctx)?; - Ok(ctx.new_expr(HasAttr { lhs, rhs }.to_ir())) + let span = self.syntax().text_range(); + Ok(ctx.new_expr(HasAttr { lhs, rhs, span }.to_ir())) } } @@ -200,7 +253,8 @@ impl Downgrade for ast::UnaryOp { fn downgrade(self, ctx: &mut Ctx) -> Result { let rhs = self.expr().unwrap().downgrade(ctx)?; let kind = self.operator().unwrap().into(); - Ok(ctx.new_expr(UnOp { rhs, kind }.to_ir())) + let span = self.syntax().text_range(); + Ok(ctx.new_expr(UnOp { rhs, kind, span }.to_ir())) } } @@ -210,16 +264,27 @@ impl Downgrade for ast::Select { let expr = self.expr().unwrap().downgrade(ctx)?; let attrpath = downgrade_attrpath(self.attrpath().unwrap(), ctx)?; let default = if let Some(default) = self.default_expr() { + let span = default.syntax().text_range(); let default_expr = default.downgrade(ctx)?; - Some(ctx.new_expr(Ir::Thunk(default_expr))) + Some( + ctx.new_expr( + Thunk { + inner: default_expr, + span, + } + .to_ir(), + ), + ) } else { None }; + let span = self.syntax().text_range(); Ok(ctx.new_expr( Select { expr, attrpath, default, + span, } .to_ir(), )) @@ -230,6 +295,7 @@ impl Downgrade for ast::Select { /// The body of the `let` is accessed via `let.body`. impl Downgrade for ast::LegacyLet { fn downgrade(self, ctx: &mut Ctx) -> Result { + let span = self.syntax().text_range(); let bindings = downgrade_static_attrs(self, ctx)?; let binding_keys: Vec<_> = bindings.keys().copied().collect(); @@ -237,10 +303,12 @@ impl Downgrade for ast::LegacyLet { let mut attrs = AttrSet { stcs: HashMap::new(), dyns: Vec::new(), + span, }; for sym in binding_keys { - let expr = ctx.lookup(sym)?; + // FIXME: span + let expr = ctx.lookup(sym, synthetic_span())?; attrs.stcs.insert(sym, expr); } @@ -252,6 +320,7 @@ impl Downgrade for ast::LegacyLet { expr: attrset_expr, attrpath: vec![Attr::Str(body_sym)], default: None, + span, }; Ok(ctx.new_expr(select.to_ir())) @@ -263,11 +332,19 @@ impl Downgrade for ast::LetIn { fn downgrade(self, ctx: &mut Ctx) -> Result { let entries: Vec<_> = self.entries().collect(); let body_expr = self.body().unwrap(); + let span = self.syntax().text_range(); let (binding_sccs, body) = downgrade_let_bindings(entries, ctx, |ctx, _binding_keys| body_expr.downgrade(ctx))?; - Ok(ctx.new_expr(Let { body, binding_sccs }.to_ir())) + Ok(ctx.new_expr( + Let { + body, + binding_sccs, + span, + } + .to_ir(), + )) } } @@ -288,13 +365,15 @@ impl Downgrade for ast::With { /// This involves desugaring pattern-matching arguments into `let` bindings. impl Downgrade for ast::Lambda { fn downgrade(self, ctx: &mut Ctx) -> Result { - let arg = ctx.new_arg(); + let param = self.param().unwrap(); + let arg = ctx.new_arg(param.syntax().text_range()); let required; let allowed; let body; + let span = self.body().unwrap().syntax().text_range(); - match self.param().unwrap() { + match param { ast::Param::IdentParam(id) => { // Simple case: `x: body` let param_sym = ctx.new_sym(id.to_string()); @@ -334,6 +413,7 @@ impl Downgrade for ast::Lambda { Let { body: inner_body, binding_sccs: scc_info, + span, } .to_ir(), ); @@ -341,8 +421,17 @@ impl Downgrade for ast::Lambda { } let param = Param { required, allowed }; + let span = self.syntax().text_range(); // The function's body and parameters are now stored directly in the `Func` node. - Ok(ctx.new_expr(Func { body, param, arg }.to_ir())) + Ok(ctx.new_expr( + Func { + body, + param, + arg, + span, + } + .to_ir(), + )) } } @@ -353,6 +442,7 @@ impl Downgrade for ast::Apply { fn downgrade(self, ctx: &mut Ctx) -> Result { let func = self.lambda().unwrap().downgrade(ctx)?; let arg = maybe_thunk(self.argument().unwrap(), ctx)?; - Ok(ctx.new_expr(Call { func, arg }.to_ir())) + let span = self.syntax().text_range(); + Ok(ctx.new_expr(Call { func, arg, span }.to_ir())) } } diff --git a/nix-js/src/ir/span_utils.rs b/nix-js/src/ir/span_utils.rs new file mode 100644 index 0000000..d7ed2fe --- /dev/null +++ b/nix-js/src/ir/span_utils.rs @@ -0,0 +1,20 @@ +use rnix::TextRange; + +pub fn merge_spans(spans: impl IntoIterator) -> TextRange { + let mut spans = spans.into_iter(); + let first = spans.next().unwrap_or_else(|| synthetic_span()); + + spans.fold(first, |acc, span| { + let start = acc.start().min(span.start()); + let end = acc.end().max(span.end()); + TextRange::new(start, end) + }) +} + +pub fn point_span() -> TextRange { + TextRange::new(0.into(), 0.into()) +} + +pub fn synthetic_span() -> TextRange { + TextRange::new(0.into(), 0.into()) +} diff --git a/nix-js/src/ir/utils.rs b/nix-js/src/ir/utils.rs index f2654bd..a1287da 100644 --- a/nix-js/src/ir/utils.rs +++ b/nix-js/src/ir/utils.rs @@ -1,9 +1,12 @@ // Assume no parse error #![allow(clippy::unwrap_used)] +use std::sync::Arc; + use hashbrown::hash_map::Entry; use hashbrown::{HashMap, HashSet}; use rnix::ast; +use rowan::ast::AstNode; use crate::error::{Error, Result}; use crate::ir::{Attr, AttrSet, ConcatStrings, ExprId, Ir, Select, Str, SymId}; @@ -21,7 +24,12 @@ pub fn maybe_thunk(mut expr: ast::Expr, ctx: &mut impl DowngradeContext) -> Resu } }; match expr { - Error(error) => return Err(self::Error::downgrade_error(error.to_string())), + Error(error) => { + let span = error.syntax().text_range(); + return Err(self::Error::downgrade_error(error.to_string()) + .with_span(span) + .with_source(ctx.get_current_source().unwrap_or_else(|| Arc::from("")))); + } Ident(ident) => return ident.downgrade(ctx), Literal(lit) => return lit.downgrade(ctx), Str(str) => return str.downgrade(ctx), @@ -46,19 +54,28 @@ pub fn maybe_thunk(mut expr: ast::Expr, ctx: &mut impl DowngradeContext) -> Resu _ => unreachable!(), }?; - Ok(ctx.new_expr(Ir::Thunk(id))) + Ok(ctx.new_expr( + Thunk { + inner: id, + // span: ctx.get_span(id), + // FIXME: span + span: synthetic_span() + } + .to_ir(), + )) } /// Downgrades the entries of an attribute set. /// This handles `inherit` and `attrpath = value;` entries. pub fn downgrade_attrs( - attrs: impl ast::HasEntry, + attrs: impl ast::HasEntry + AstNode, ctx: &mut impl DowngradeContext, ) -> Result { let entries = attrs.entries(); let mut attrs = AttrSet { stcs: HashMap::new(), dyns: Vec::new(), + span: attrs.syntax().text_range(), }; for entry in entries { @@ -75,13 +92,14 @@ pub fn downgrade_attrs( /// This is a stricter version of `downgrade_attrs` that disallows dynamic attributes, /// as `let` bindings must be statically known. pub fn downgrade_static_attrs( - attrs: impl ast::HasEntry, + attrs: impl ast::HasEntry + AstNode, ctx: &mut impl DowngradeContext, ) -> Result> { let entries = attrs.entries(); let mut attrs = AttrSet { stcs: HashMap::new(), dyns: Vec::new(), + span: attrs.syntax().text_range(), }; for entry in entries { @@ -111,13 +129,16 @@ pub fn downgrade_inherit( None }; for attr in inherit.attrs() { + let span = attr.syntax().text_range(); let ident = match downgrade_attr(attr, ctx)? { Attr::Str(ident) => ident, _ => { // `inherit` does not allow dynamic attributes. return Err(Error::downgrade_error( "dynamic attributes not allowed in inherit".to_string(), - )); + ) + .with_span(span) + .with_source(ctx.get_current_source().unwrap_or_else(|| Arc::from("")))); } }; let expr = if let Some(expr) = from { @@ -126,19 +147,28 @@ pub fn downgrade_inherit( expr, attrpath: vec![Attr::Str(ident)], default: None, + span, } .to_ir(), ); - ctx.new_expr(Ir::Thunk(select_expr)) + ctx.new_expr( + Thunk { + inner: select_expr, + span, + } + .to_ir(), + ) } else { - ctx.lookup(ident)? + ctx.lookup(ident, span)? }; match stcs.entry(ident) { Entry::Occupied(occupied) => { return Err(Error::downgrade_error(format!( "attribute '{}' already defined", format_symbol(ctx.get_sym(*occupied.key())) - ))); + )) + .with_span(span) + .with_source(ctx.get_current_source().unwrap_or_else(|| Arc::from("")))); } Entry::Vacant(vacant) => vacant.insert(expr), }; @@ -151,6 +181,7 @@ pub fn downgrade_inherit( pub fn downgrade_attr(attr: ast::Attr, ctx: &mut impl DowngradeContext) -> Result { use ast::Attr::*; use ast::InterpolPart::*; + let span = attr.syntax().text_range(); match attr { Ident(ident) => Ok(Attr::Str(ctx.new_sym(ident.to_string()))), Str(string) => { @@ -170,11 +201,13 @@ pub fn downgrade_attr(attr: ast::Attr, ctx: &mut impl DowngradeContext) -> Resul let parts = parts .into_iter() .map(|part| match part { - Literal(lit) => Ok(ctx.new_expr(self::Str { val: lit }.to_ir())), + Literal(lit) => Ok(ctx.new_expr(self::Str { val: lit, span }.to_ir())), Interpolation(interpol) => interpol.expr().unwrap().downgrade(ctx), }) .collect::>>()?; - Ok(Attr::Dynamic(ctx.new_expr(ConcatStrings { parts }.to_ir()))) + Ok(Attr::Dynamic( + ctx.new_expr(ConcatStrings { parts, span }.to_ir()), + )) } } Dynamic(dynamic) => Ok(Attr::Dynamic(dynamic.expr().unwrap().downgrade(ctx)?)), @@ -210,11 +243,14 @@ pub fn downgrade_static_attrpathvalue( attrs: &mut AttrSet, ctx: &mut impl DowngradeContext, ) -> Result<()> { - let path = downgrade_attrpath(value.attrpath().unwrap(), ctx)?; + let attrpath_node = value.attrpath().unwrap(); + let path = downgrade_attrpath(attrpath_node.clone(), ctx)?; if path.iter().any(|attr| matches!(attr, Attr::Dynamic(_))) { return Err(Error::downgrade_error( "dynamic attributes not allowed in let bindings".to_string(), - )); + ) + .with_span(attrpath_node.syntax().text_range()) + .with_source(ctx.get_current_source().unwrap_or_else(|| Arc::from("")))); } let value = value.value().unwrap().downgrade(ctx)?; attrs.insert(path, value, ctx) @@ -249,21 +285,26 @@ where { let mut param_syms = Vec::new(); let mut param_defaults = Vec::new(); + let mut param_spans = Vec::new(); let mut seen_params = HashSet::new(); for entry in pat_entries { let sym = ctx.new_sym(entry.ident().unwrap().to_string()); + let span = entry.ident().unwrap().syntax().text_range(); if !seen_params.insert(sym) { return Err(Error::downgrade_error(format!( "duplicate parameter '{}'", format_symbol(ctx.get_sym(sym)) - ))); + )) + .with_span(span) + .with_source(ctx.get_current_source().unwrap_or_else(|| Arc::from("")))); } let default_ast = entry.default(); param_syms.push(sym); param_defaults.push(default_ast); + param_spans.push(span); } let mut binding_keys: Vec = param_syms.clone(); @@ -292,7 +333,11 @@ where |ctx, sym_to_slot| { let mut bindings = HashMap::new(); - for (sym, default_ast) in param_syms.iter().zip(param_defaults.iter()) { + for ((sym, default_ast), span) in param_syms + .iter() + .zip(param_defaults.iter()) + .zip(param_spans.iter()) + { let slot = *sym_to_slot.get(sym).unwrap(); ctx.set_current_binding(Some(slot)); @@ -307,6 +352,7 @@ where expr: arg, attrpath: vec![Attr::Str(*sym)], default, + span: *span, } .to_ir(), ); @@ -387,12 +433,23 @@ where for (sym, slot) in binding_keys.iter().copied().zip(slots.iter()) { if let Some(&expr) = bindings.get(&sym) { - ctx.replace_expr(*slot, Ir::Thunk(expr)); + ctx.replace_expr( + *slot, + Thunk { + inner: expr, + // span: ctx.get_span(expr), + // FIXME: span + span: synthetic_span() + } + .to_ir(), + ); } else { return Err(Error::downgrade_error(format!( "binding '{}' not found", format_symbol(ctx.get_sym(sym)) - ))); + )) + .with_span(synthetic_span()) + .with_source(ctx.get_current_source().unwrap_or_else(|| Arc::from("")))); } } @@ -430,7 +487,9 @@ where return Err(Error::downgrade_error(format!( "attribute '{}' already defined", format_symbol(ctx.get_sym(sym)) - ))); + )) + .with_span(ident.syntax().text_range()) + .with_source(ctx.get_current_source().unwrap_or_else(|| Arc::from("")))); } } } @@ -448,7 +507,9 @@ where return Err(Error::downgrade_error(format!( "attribute '{}' already defined", format_symbol(ctx.get_sym(sym)) - ))); + )) + .with_span(ident.syntax().text_range()) + .with_source(ctx.get_current_source().unwrap_or_else(|| Arc::from("")))); } } } else if attrs_vec.len() > 1 { @@ -471,6 +532,7 @@ where let mut temp_attrs = AttrSet { stcs: HashMap::new(), dyns: Vec::new(), + span: synthetic_span() }; for entry in entries { diff --git a/nix-js/src/lib.rs b/nix-js/src/lib.rs index 146f64a..27ea6c8 100644 --- a/nix-js/src/lib.rs +++ b/nix-js/src/lib.rs @@ -8,9 +8,10 @@ pub mod value; mod codegen; mod fetcher; mod ir; -mod nix_hash; mod nar; +mod nix_hash; mod runtime; +mod sourcemap; mod store; #[global_allocator] diff --git a/nix-js/src/logging.rs b/nix-js/src/logging.rs index 10d244e..78b147c 100644 --- a/nix-js/src/logging.rs +++ b/nix-js/src/logging.rs @@ -1,6 +1,6 @@ use std::env; use std::io::IsTerminal; -use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer}; +use tracing_subscriber::{EnvFilter, Layer, fmt, layer::SubscriberExt, util::SubscriberInitExt}; pub fn init_logging() { let is_terminal = std::io::stderr().is_terminal(); @@ -19,9 +19,7 @@ pub fn init_logging() { .with_level(true); let fmt_layer = if show_time { - fmt_layer - .with_timer(fmt::time::uptime()) - .boxed() + fmt_layer.with_timer(fmt::time::uptime()).boxed() } else { fmt_layer.without_time().boxed() }; @@ -30,34 +28,20 @@ pub fn init_logging() { .with(filter) .with(fmt_layer) .init(); + + init_miette_handler(); } -#[macro_export] -macro_rules! trace_span { - ($name:expr) => { - tracing::trace_span!($name) - }; - ($name:expr, $($field:tt)*) => { - tracing::trace_span!($name, $($field)*) - }; -} - -#[macro_export] -macro_rules! debug_span { - ($name:expr) => { - tracing::debug_span!($name) - }; - ($name:expr, $($field:tt)*) => { - tracing::debug_span!($name, $($field)*) - }; -} - -#[macro_export] -macro_rules! info_span { - ($name:expr) => { - tracing::info_span!($name) - }; - ($name:expr, $($field:tt)*) => { - tracing::info_span!($name, $($field)*) - }; +fn init_miette_handler() { + let is_terminal = std::io::stderr().is_terminal(); + miette::set_hook(Box::new(move |_| { + Box::new( + miette::MietteHandlerOpts::new() + .terminal_links(is_terminal) + .unicode(is_terminal) + .color(is_terminal) + .build(), + ) + })) + .ok(); } diff --git a/nix-js/src/runtime.rs b/nix-js/src/runtime.rs index 9844c3d..24e47be 100644 --- a/nix-js/src/runtime.rs +++ b/nix-js/src/runtime.rs @@ -1,7 +1,7 @@ use std::borrow::Cow; use std::marker::PhantomData; use std::path::{Component, Path, PathBuf}; -use std::sync::Once; +use std::sync::{Arc, Once}; use deno_core::{Extension, ExtensionFileSource, JsRuntime, OpState, RuntimeOptions, v8}; use deno_error::JsErrorClass; @@ -17,6 +17,7 @@ pub(crate) trait RuntimeContext: 'static { fn get_current_dir(&self) -> &Path; fn set_current_file(&mut self, path: PathBuf); fn compile_code(&mut self, code: &str) -> Result; + fn get_current_source(&self) -> Option>; } fn runtime_extension() -> Extension { @@ -134,8 +135,7 @@ fn op_resolve_path( dir.push(path); dir } else { - let mut dir = std::env::home_dir() - .ok_or("home dir not defined")?; + let mut dir = std::env::home_dir().ok_or("home dir not defined")?; dir.push(&path[2..]); dir }; @@ -434,7 +434,49 @@ impl Runtime { let global_value = self .js_runtime .execute_script("", script) - .map_err(|e| Error::eval_error(format!("{}", e.get_message()), e.stack))?; + .map_err(|e| { + let msg = format!("{}", e.get_message()); + let stack_str = e.stack.as_ref().map(|s| s.to_string()); + + let mut error = Error::eval_error(msg.clone(), None); + + // Parse Nix stack trace frames + if let Some(ref stack) = stack_str { + let frames = crate::error::parse_nix_stack(stack); + + if !frames.is_empty() { + // Get the last frame (where error occurred) for span + if let Some(last_frame) = frames.last() { + let span = last_frame.span; + error = error.with_span(span); + } + + // Format stack trace (reversed, newest at bottom) + let trace_lines = crate::error::format_stack_trace(&frames); + if !trace_lines.is_empty() { + let formatted_trace = trace_lines.join("\n"); + error = Error::eval_error(msg, Some(formatted_trace)); + + // Re-apply span after recreating error + if let Some(last_frame) = frames.last() { + let span = last_frame.span; + error = error.with_span(span); + } + } + + // Get current source from Context + let op_state = self.js_runtime.op_state(); + let op_state_borrow = op_state.borrow(); + if let Some(ctx) = op_state_borrow.try_borrow::() { + if let Some(source) = ctx.get_current_source() { + error = error.with_source(source); + } + } + } + } + + error + })?; // Retrieve scope from JsRuntime deno_core::scope!(scope, self.js_runtime); diff --git a/nix-js/src/sourcemap.rs b/nix-js/src/sourcemap.rs new file mode 100644 index 0000000..03c6896 --- /dev/null +++ b/nix-js/src/sourcemap.rs @@ -0,0 +1,110 @@ +use base64::{engine::general_purpose::STANDARD, Engine}; +use rnix::TextRange; +use sourcemap::{SourceMap, SourceMapBuilder}; +use std::sync::Arc; + +pub struct NixSourceMapBuilder { + builder: SourceMapBuilder, + source_name: String, + source_content: Arc, + generated_code: String, +} + +impl NixSourceMapBuilder { + pub fn new(source_name: impl Into, source_content: Arc) -> Self { + let mut builder = SourceMapBuilder::new(None); + let source_name = source_name.into(); + builder.add_source(&source_name); + builder.set_source_contents(0, Some(&source_content)); + + Self { + builder, + source_name, + source_content, + generated_code: String::new(), + } + } + + pub fn add_mapping(&mut self, js_offset: usize, nix_span: TextRange) { + let (js_line, js_col) = byte_to_line_col(&self.generated_code, js_offset); + let (nix_line, nix_col) = byte_to_line_col(&self.source_content, nix_span.start().into()); + + self.builder.add_raw( + js_line, + js_col, + nix_line, + nix_col, + Some(0), + None, + false, + ); + } + + pub fn set_generated_code(&mut self, code: String) { + self.generated_code = code; + } + + pub fn build(self) -> Result<(SourceMap, String), sourcemap::Error> { + let sourcemap = self.builder.into_sourcemap(); + let mut buf = Vec::new(); + sourcemap.to_writer(&mut buf)?; + + let encoded = STANDARD.encode(&buf); + let data_url = format!( + "data:application/json;charset=utf-8;base64,{}", + encoded + ); + + Ok((sourcemap, data_url)) + } +} + +fn byte_to_line_col(text: &str, byte_offset: usize) -> (u32, u32) { + let mut line = 0; + let mut col = 0; + let mut current_offset = 0; + + for ch in text.chars() { + if current_offset >= byte_offset { + break; + } + + if ch == '\n' { + line += 1; + col = 0; + } else { + col += 1; + } + + current_offset += ch.len_utf8(); + } + + (line, col) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_byte_to_line_col() { + let text = "line1\nline2\nline3"; + + assert_eq!(byte_to_line_col(text, 0), (0, 0)); + assert_eq!(byte_to_line_col(text, 5), (0, 5)); + assert_eq!(byte_to_line_col(text, 6), (1, 0)); + assert_eq!(byte_to_line_col(text, 12), (2, 0)); + } + + #[test] + fn test_sourcemap_builder() { + let source = Arc::::from("let x = 1; in x"); + let mut builder = NixSourceMapBuilder::new("test.nix", source); + + let span = TextRange::new(4.into(), 5.into()); + builder.add_mapping(0, span); + + let result = builder.build(); + assert!(result.is_ok()); + } +} diff --git a/nix-js/src/store.rs b/nix-js/src/store.rs index 5ad4442..4e78f7f 100644 --- a/nix-js/src/store.rs +++ b/nix-js/src/store.rs @@ -48,7 +48,7 @@ pub trait Store: Send + Sync { pub enum StoreBackend { Simulated(SimulatedStore), #[cfg(feature = "daemon")] - Daemon(DaemonStore), + Daemon(Box), } impl StoreBackend { @@ -56,12 +56,14 @@ impl StoreBackend { match config.mode { #[cfg(feature = "daemon")] StoreMode::Daemon => { - let daemon = DaemonStore::connect(&config.daemon_socket)?; + let daemon = Box::new(DaemonStore::connect(&config.daemon_socket)?); Ok(StoreBackend::Daemon(daemon)) } #[cfg(not(feature = "daemon"))] StoreMode::Daemon => { - tracing::warn!("Daemon mode not available (nix-js not compiled with 'daemon' feature), falling back to simulated store"); + tracing::warn!( + "Daemon mode not available (nix-js not compiled with 'daemon' feature), falling back to simulated store" + ); let simulated = SimulatedStore::new()?; Ok(StoreBackend::Simulated(simulated)) } @@ -72,17 +74,11 @@ impl StoreBackend { #[cfg(feature = "daemon")] StoreMode::Auto => match DaemonStore::connect(&config.daemon_socket) { Ok(daemon) => { - tracing::debug!( - "Using nix-daemon at {}", - config.daemon_socket.display() - ); - Ok(StoreBackend::Daemon(daemon)) + tracing::debug!("Using nix-daemon at {}", config.daemon_socket.display()); + Ok(StoreBackend::Daemon(Box::new(daemon))) } Err(e) => { - tracing::warn!( - "Daemon unavailable ({}), using simulated store", - e - ); + tracing::warn!("Daemon unavailable ({}), using simulated store", e); let simulated = SimulatedStore::new()?; Ok(StoreBackend::Simulated(simulated)) } @@ -99,7 +95,7 @@ impl StoreBackend { match self { StoreBackend::Simulated(s) => s, #[cfg(feature = "daemon")] - StoreBackend::Daemon(d) => d, + StoreBackend::Daemon(d) => d.as_ref(), } } } diff --git a/nix-js/src/store/config.rs b/nix-js/src/store/config.rs index 1767d16..e4f674f 100644 --- a/nix-js/src/store/config.rs +++ b/nix-js/src/store/config.rs @@ -24,10 +24,7 @@ impl StoreConfig { Ok("simulated") => StoreMode::Simulated, Ok("auto") | Err(_) => StoreMode::Auto, Ok(other) => { - tracing::warn!( - "Invalid NIX_JS_STORE_MODE '{}', using 'auto'", - other - ); + tracing::warn!("Invalid NIX_JS_STORE_MODE '{}', using 'auto'", other); StoreMode::Auto } }; diff --git a/nix-js/src/store/daemon.rs b/nix-js/src/store/daemon.rs index 50ac565..e78c499 100644 --- a/nix-js/src/store/daemon.rs +++ b/nix-js/src/store/daemon.rs @@ -1,3 +1,5 @@ +#![allow(dead_code)] + use std::io::{Error as IoError, ErrorKind as IoErrorKind, Result as IoResult}; use std::path::Path; @@ -180,18 +182,12 @@ impl Store for DaemonStore { let nar_data = crate::nar::pack_nar(source_path)?; - let nar_hash_hex = { + let nar_hash: [u8; 32] = { let mut hasher = Sha256::new(); hasher.update(&nar_data); - hex::encode(hasher.finalize()) + hasher.finalize().into() }; - - let nar_hash_bytes = hex::decode(&nar_hash_hex) - .map_err(|e| Error::internal(format!("Invalid nar hash: {}", e)))?; - let mut nar_hash_arr = [0u8; 32]; - nar_hash_arr.copy_from_slice(&nar_hash_bytes); - - let ca_hash = CAHash::Nar(NixHash::Sha256(nar_hash_arr)); + let ca_hash = CAHash::Nar(NixHash::Sha256(nar_hash)); let ref_store_paths: std::result::Result>, _> = references .iter() @@ -215,7 +211,7 @@ impl Store for DaemonStore { deriver: None, nar_hash: unsafe { std::mem::transmute::<[u8; 32], nix_compat::nix_daemon::types::NarHash>( - nar_hash_arr, + nar_hash, ) }, references: ref_store_paths, @@ -258,17 +254,12 @@ impl Store for DaemonStore { let nar_data = crate::nar::pack_nar(temp_file.path())?; - let nar_hash_hex = { + let nar_hash: [u8; 32] = { let mut hasher = Sha256::new(); hasher.update(&nar_data); - hex::encode(hasher.finalize()) + hasher.finalize().into() }; - let nar_hash_bytes = hex::decode(&nar_hash_hex) - .map_err(|e| Error::internal(format!("Invalid nar hash: {}", e)))?; - let mut nar_hash_arr = [0u8; 32]; - nar_hash_arr.copy_from_slice(&nar_hash_bytes); - let content_hash = { let mut hasher = Sha256::new(); hasher.update(content.as_bytes()); @@ -296,7 +287,7 @@ impl Store for DaemonStore { deriver: None, nar_hash: unsafe { std::mem::transmute::<[u8; 32], nix_compat::nix_daemon::types::NarHash>( - nar_hash_arr, + nar_hash, ) }, references: ref_store_paths, diff --git a/nix-js/src/store/simulated.rs b/nix-js/src/store/simulated.rs index f267b56..8c2cfb5 100644 --- a/nix-js/src/store/simulated.rs +++ b/nix-js/src/store/simulated.rs @@ -24,10 +24,6 @@ impl SimulatedStore { Ok(Self { cache, store_dir }) } - - pub fn cache(&self) -> &FetcherCache { - &self.cache - } } impl Store for SimulatedStore { diff --git a/nix-js/src/store/validation.rs b/nix-js/src/store/validation.rs index c1704c5..a2d3b73 100644 --- a/nix-js/src/store/validation.rs +++ b/nix-js/src/store/validation.rs @@ -87,7 +87,7 @@ mod tests { let valid_paths = vec![ "/nix/store/0123456789abcdfghijklmnpqrsvwxyz-hello", "/nix/store/abcdfghijklmnpqrsvwxyz0123456789-hello-1.0", - "/nix/store/00000000000000000000000000000000-test_+-.?=" + "/nix/store/00000000000000000000000000000000-test_+-.?=", ]; for path in valid_paths { @@ -109,15 +109,36 @@ mod tests { ("/nix/store/tooshort-name", "hash too short"), ( "/nix/store/abc123defghijklmnopqrstuvwxyz123-name", - "hash too long" + "hash too long", + ), + ( + "/nix/store/abcd1234abcd1234abcd1234abcd123e-name", + "e in hash", + ), + ( + "/nix/store/abcd1234abcd1234abcd1234abcd123o-name", + "o in hash", + ), + ( + "/nix/store/abcd1234abcd1234abcd1234abcd123u-name", + "u in hash", + ), + ( + "/nix/store/abcd1234abcd1234abcd1234abcd123t-name", + "t in hash", + ), + ( + "/nix/store/abcd1234abcd1234abcd1234abcd1234-.name", + "name starts with dot", + ), + ( + "/nix/store/abcd1234abcd1234abcd1234abcd1234-na/me", + "slash in name", + ), + ( + "/nix/store/abcd1234abcd1234abcd1234abcd1234", + "missing name", ), - ("/nix/store/abcd1234abcd1234abcd1234abcd123e-name", "e in hash"), - ("/nix/store/abcd1234abcd1234abcd1234abcd123o-name", "o in hash"), - ("/nix/store/abcd1234abcd1234abcd1234abcd123u-name", "u in hash"), - ("/nix/store/abcd1234abcd1234abcd1234abcd123t-name", "t in hash"), - ("/nix/store/abcd1234abcd1234abcd1234abcd1234-.name", "name starts with dot"), - ("/nix/store/abcd1234abcd1234abcd1234abcd1234-na/me", "slash in name"), - ("/nix/store/abcd1234abcd1234abcd1234abcd1234", "missing name"), ]; for (path, reason) in invalid_paths { diff --git a/nix-js/tests/builtins_store.rs b/nix-js/tests/builtins_store.rs index a4fe364..0857774 100644 --- a/nix-js/tests/builtins_store.rs +++ b/nix-js/tests/builtins_store.rs @@ -9,9 +9,13 @@ fn init() { static INIT: Once = Once::new(); INIT.call_once(|| { #[cfg(not(feature = "daemon"))] - unsafe { std::env::set_var("NIX_JS_STORE_MODE", "simulated") }; + unsafe { + std::env::set_var("NIX_JS_STORE_MODE", "simulated") + }; #[cfg(feature = "daemon")] - unsafe { std::env::set_var("NIX_JS_STORE_MODE", "daemon") }; + unsafe { + std::env::set_var("NIX_JS_STORE_MODE", "daemon") + }; }); }