diff --git a/Justfile b/Justfile index 8831a48..b501a0e 100644 --- a/Justfile +++ b/Justfile @@ -8,8 +8,8 @@ [no-exit-message] @replr: - cargo run --bin repl --release + RUST_LOG=info cargo run --bin repl --release [no-exit-message] @evalr expr: - cargo run --bin eval --release -- '{{expr}}' + RUST_LOG=info cargo run --bin eval --release -- '{{expr}}' diff --git a/flake.lock b/flake.lock index 27e2ee0..f6c5731 100644 --- a/flake.lock +++ b/flake.lock @@ -8,11 +8,11 @@ "rust-analyzer-src": "rust-analyzer-src" }, "locked": { - "lastModified": 1767250179, - "narHash": "sha256-PnQdWvPZqHp+7yaHWDFX3NYSKaOy0fjkwpR+rIQC7AY=", + "lastModified": 1768892055, + "narHash": "sha256-zatCoDgFd0C8YEOztMeBcom6cka0GqJGfc0aAXvpktc=", "owner": "nix-community", "repo": "fenix", - "rev": "a3eaf682db8800962943a77ab77c0aae966f9825", + "rev": "81d6a7547e090f7e760b95b9cc534461f6045e43", "type": "github" }, "original": { @@ -37,11 +37,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1767116409, - "narHash": "sha256-5vKw92l1GyTnjoLzEagJy5V5mDFck72LiQWZSOnSicw=", + "lastModified": 1768886240, + "narHash": "sha256-C2TjvwYZ2VDxYWeqvvJ5XPPp6U7H66zeJlRaErJKoEM=", "owner": "nixos", "repo": "nixpkgs", - "rev": "cad22e7d996aea55ecab064e84834289143e44a0", + "rev": "80e4adbcf8992d3fd27ad4964fbb84907f9478b0", "type": "github" }, "original": { @@ -61,11 +61,11 @@ "rust-analyzer-src": { "flake": false, "locked": { - "lastModified": 1767191410, - "narHash": "sha256-cCZGjubgDWmstvFkS6eAw2qk2ihgWkycw55u2dtLd70=", + "lastModified": 1768816483, + "narHash": "sha256-bXeWgVkvxN76QEw12OaWFbRhO1yt+5QETz/BxBX4dk0=", "owner": "rust-lang", "repo": "rust-analyzer", - "rev": "a9026e6d5068172bf5a0d52a260bb290961d1cb4", + "rev": "1b8952b49fa10cae9020f0e46d0b8938563a6b64", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index fafa76d..7dcfafd 100644 --- a/flake.nix +++ b/flake.nix @@ -37,6 +37,8 @@ biome claude-code + codex + opencode ]; }; } diff --git a/nix-js/runtime-ts/src/builtins/functional.ts b/nix-js/runtime-ts/src/builtins/functional.ts index 48590cb..d92f5e2 100644 --- a/nix-js/runtime-ts/src/builtins/functional.ts +++ b/nix-js/runtime-ts/src/builtins/functional.ts @@ -37,10 +37,12 @@ export const throwFunc = (s: NixValue): never => { throw new CatchableError(coerceToString(s, StringCoercionMode.Base)); }; -export const trace = (e1: NixValue, e2: NixValue): NixValue => { - console.log(`trace: ${force(e1)}`); - return e2; -}; +export const trace = + (e1: NixValue) => + (e2: NixValue): NixValue => { + console.log(`trace: ${coerceToString(e1, StringCoercionMode.Base)}`); + return e2; + }; export const warn = (e1: NixValue) => diff --git a/nix-js/runtime-ts/src/helpers.ts b/nix-js/runtime-ts/src/helpers.ts index 7918050..c48e31e 100644 --- a/nix-js/runtime-ts/src/helpers.ts +++ b/nix-js/runtime-ts/src/helpers.ts @@ -29,11 +29,11 @@ function enrichError(error: unknown): Error { } const nixStackLines = callStack.map((frame) => { - return `NIX_STACK_FRAME:context:${frame.span}:${frame.message}`; + return `NIX_STACK_FRAME:${frame.span}:${frame.message}`; }); // Prepend stack frames to error stack - err.stack = `${nixStackLines.join('\n')}\n${err.stack || ''}`; + err.stack = `${nixStackLines.join("\n")}\n${err.stack || ""}`; return err; } @@ -176,9 +176,9 @@ export const resolvePath = (currentDir: string, path: NixValue): NixPath => { 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'; + 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(); @@ -215,11 +215,16 @@ function select_impl(obj: NixValue, attrpath: NixValue[]): NixValue { return attrs[last]; } -export const selectWithDefault = (obj: NixValue, attrpath: NixValue[], default_val: NixValue, span?: string): NixValue => { +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'; + 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(); @@ -324,7 +329,7 @@ export const call = (func: NixValue, arg: NixValue, span?: string): NixValue => if (callStack.length >= MAX_STACK_DEPTH) { callStack.shift(); } - callStack.push({ span, message: 'from call site' }); + callStack.push({ span, message: "from call site" }); try { return call_impl(func, arg); } catch (error) { diff --git a/nix-js/src/bin/eval.rs b/nix-js/src/bin/eval.rs index 508dc68..4835091 100644 --- a/nix-js/src/bin/eval.rs +++ b/nix-js/src/bin/eval.rs @@ -19,7 +19,7 @@ fn main() -> Result<()> { Ok(()) } Err(err) => { - eprintln!("{:?}", miette::Report::new(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 d95dbd4..61a554e 100644 --- a/nix-js/src/bin/repl.rs +++ b/nix-js/src/bin/repl.rs @@ -35,7 +35,7 @@ fn main() -> Result<()> { let src = Source::new_repl(line)?; match context.eval_code(src) { Ok(value) => println!("{value}"), - Err(err) => eprintln!("{:?}", miette::Report::new(err)), + Err(err) => eprintln!("{:?}", miette::Report::new(*err)), } } } diff --git a/nix-js/src/codegen.rs b/nix-js/src/codegen.rs index d01f904..b5cdc7e 100644 --- a/nix-js/src/codegen.rs +++ b/nix-js/src/codegen.rs @@ -35,6 +35,7 @@ pub(crate) trait CodegenContext { fn get_ir(&self, id: ExprId) -> &Ir; fn get_sym(&self, id: SymId) -> &str; fn get_current_dir(&self) -> &Path; + fn get_current_source_id(&self) -> usize; } trait EscapeQuote { @@ -60,9 +61,10 @@ impl EscapeQuote for str { } } -fn encode_span(span: rnix::TextRange) -> String { +fn encode_span(span: rnix::TextRange, ctx: &impl CodegenContext) -> String { format!( - "\"{}:{}\"", + "\"{}:{}:{}\"", + ctx.get_current_source_id(), usize::from(span.start()), usize::from(span.end()) ) @@ -93,7 +95,7 @@ impl Compile for Ir { // 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()); + let cond_span = encode_span(ctx.get_ir(cond).span(), ctx); format!( "(Nix.withContext(\"while evaluating a branch condition\",{},()=>({})))?({}):({})", cond_span, cond_code, consq, alter @@ -135,8 +137,8 @@ impl Compile for Ir { // 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()); - let span = encode_span(span); + let assertion_span = encode_span(ctx.get_ir(assertion).span(), ctx); + let span = encode_span(span, ctx); format!( "Nix.assert(Nix.withContext(\"while evaluating the condition of the assert statement\",{},()=>({})),{},{},{})", assertion_span, @@ -171,7 +173,7 @@ impl Compile for BinOp { // 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); + let span = encode_span(self.span, ctx); format!( "Nix.withContext(\"while evaluating the {} operator\",{},()=>({}))", op_name, span, op_call @@ -274,7 +276,7 @@ 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); - let span_str = encode_span(self.span); + let span_str = encode_span(self.span, ctx); format!("Nix.call({func},{arg},{span_str})") } } @@ -338,7 +340,7 @@ impl Compile for Select { Attr::Dynamic(expr_id) => ctx.get_ir(*expr_id).compile(ctx), }) .join(","); - let span_str = encode_span(self.span); + let span_str = encode_span(self.span, ctx); if let Some(default) = self.default { format!( "Nix.selectWithDefault({lhs},[{attrpath}],{},{span_str})", @@ -360,7 +362,7 @@ impl Compile for AttrSet { 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()); + let value_span = encode_span(ctx.get_ir(expr).span(), ctx); format!( "Nix.withContext(\"while evaluating the attribute '{}'\",{},()=>({}))", key, value_span, value_code @@ -377,7 +379,7 @@ impl Compile for AttrSet { 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()); + let value_span = encode_span(ctx.get_ir(*value_expr).span(), ctx); format!( "Nix.withContext(\"while evaluating a dynamic attribute\",{},()=>({}))", value_span, value_code @@ -403,7 +405,7 @@ impl Compile for List { .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()); + let item_span = encode_span(ctx.get_ir(*item).span(), ctx); format!( "Nix.withContext(\"while evaluating list element {}\",{},()=>({}))", idx, item_span, item_code @@ -427,7 +429,7 @@ impl Compile for ConcatStrings { .map(|part| { let part_code = ctx.get_ir(*part).compile(ctx); if stack_trace_enabled { - let part_span = encode_span(ctx.get_ir(*part).span()); + let part_span = encode_span(ctx.get_ir(*part).span(), ctx); format!( "Nix.withContext(\"while evaluating a path segment\",{},()=>({}))", part_span, part_code diff --git a/nix-js/src/context.rs b/nix-js/src/context.rs index 798d60a..34adc1e 100644 --- a/nix-js/src/context.rs +++ b/nix-js/src/context.rs @@ -41,15 +41,18 @@ mod private { fn get_current_dir(&self) -> &Path { self.as_ref().get_current_dir() } - fn set_current_file(&mut self, source: Source) { - self.as_mut().current_file = Some(source); + fn add_source(&mut self, source: Source) { + self.as_mut().sources.push(source); } fn compile_code(&mut self, source: Source) -> Result { self.as_mut().compile_code(source) } - fn get_current_source(&self) -> Option { + fn get_current_source(&self) -> Source { self.as_ref().get_current_source() } + fn get_source(&self, id: usize) -> Source { + self.as_ref().get_source(id) + } } } use private::CtxPtr; @@ -83,7 +86,6 @@ impl Context { pub fn eval_code(&mut self, source: Source) -> Result { tracing::info!("Starting evaluation"); - self.ctx.current_file = Some(source.clone()); tracing::debug!("Compiling code"); let code = self.compile_code(source)?; @@ -113,8 +115,7 @@ pub(crate) struct Ctx { irs: Vec, symbols: DefaultStringInterner, global: NonNull>, - current_file: Option, - current_source: Option, + sources: Vec, } impl Default for Ctx { @@ -206,8 +207,7 @@ impl Default for Ctx { symbols, irs, global: unsafe { NonNull::new_unchecked(Box::leak(Box::new(global))) }, - current_file: None, - current_source: None, + sources: Vec::new(), } } } @@ -223,20 +223,28 @@ impl Ctx { } pub(crate) fn get_current_dir(&self) -> &Path { - self.current_file + self.sources + .last() .as_ref() - .expect("current_file is not set") + .expect("current_source is not set") .get_dir() } - pub(crate) fn get_current_source(&self) -> Option { - self.current_source.clone() + pub(crate) fn get_current_source(&self) -> Source { + self.sources + .last() + .expect("current_source is not set") + .clone() + } + + pub(crate) fn get_source(&self, id: usize) -> Source { + self.sources.get(id).expect("source not found").clone() } fn compile_code(&mut self, source: Source) -> Result { tracing::debug!("Parsing Nix expression"); - self.current_source = Some(source.clone()); + self.sources.push(source.clone()); let root = rnix::Root::parse(&source.src); if !root.errors().is_empty() { @@ -267,6 +275,12 @@ impl CodegenContext for Ctx { fn get_current_dir(&self) -> &std::path::Path { self.get_current_dir() } + fn get_current_source_id(&self) -> usize { + self.sources + .len() + .checked_sub(1) + .expect("current_source not set") + } } struct DependencyTracker { @@ -431,7 +445,7 @@ impl DowngradeContext for DowngradeCtx<'_> { result.ok_or_else(|| { Error::downgrade_error(format!("'{}' not found", self.get_sym(sym))) .with_span(span) - .with_source(self.get_current_source().expect("no source set")) + .with_source(self.get_current_source()) }) } @@ -453,17 +467,8 @@ 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() + fn get_current_source(&self) -> Source { + self.ctx.get_current_source() } #[allow(refining_impl_trait)] diff --git a/nix-js/src/error.rs b/nix-js/src/error.rs index e6029fe..573a7fc 100644 --- a/nix-js/src/error.rs +++ b/nix-js/src/error.rs @@ -5,7 +5,9 @@ use std::{ }; use thiserror::Error; -pub type Result = core::result::Result; +use crate::{context::Ctx, runtime::RuntimeContext}; + +pub type Result = core::result::Result>; #[derive(Clone, Debug)] pub enum SourceType { @@ -24,7 +26,7 @@ pub struct Source { } impl TryFrom<&str> for Source { - type Error = Error; + type Error = Box; fn try_from(value: &str) -> Result { Source::new_eval(value.into()) } @@ -66,7 +68,7 @@ impl Source { use SourceType::*; match &self.ty { Eval(dir) | Repl(dir) => dir.as_ref(), - File(file) => file.as_path().parent().unwrap(), + File(file) => file.as_path().parent().expect("source file must have a parent dir"), } } } @@ -101,7 +103,7 @@ pub enum Error { #[label("error occurred here")] span: Option, message: String, - // #[help] + #[help] js_backtrace: Option, }, @@ -119,91 +121,64 @@ pub enum Error { } impl Error { - pub fn parse_error(msg: String) -> Self { + pub fn parse_error(msg: String) -> Box { Error::ParseError { src: None, span: None, message: msg, } + .into() } - pub fn downgrade_error(msg: String) -> Self { + pub fn downgrade_error(msg: String) -> Box { Error::DowngradeError { src: None, span: None, message: msg, } + .into() } - pub fn eval_error(msg: String, backtrace: Option) -> Self { + pub fn eval_error(msg: String, backtrace: Option) -> Box { Error::EvalError { src: None, span: None, message: msg, js_backtrace: backtrace, } + .into() } - pub fn internal(msg: String) -> Self { - Error::InternalError { message: msg } + pub fn internal(msg: String) -> Box { + Error::InternalError { message: msg }.into() } - pub fn catchable(msg: String) -> Self { - Error::Catchable { message: msg } + pub fn catchable(msg: String) -> Box { + Error::Catchable { message: msg }.into() } - pub fn unknown() -> Self { - Error::Unknown - } - - pub fn with_span(self, span: rnix::TextRange) -> Self { + pub fn with_span(mut self: Box, span: rnix::TextRange) -> Box { + use Error::*; 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, .. } => Error::DowngradeError { - src, - span: source_span, - message, - }, - Error::EvalError { - src, - message, - js_backtrace, - .. - } => Error::EvalError { - src, - span: source_span, - message, - js_backtrace, - }, - other => other, - } + let (ParseError { span, .. } | DowngradeError { span, .. } | EvalError { span, .. }) = + self.as_mut() + else { + return self; + }; + *span = source_span; + self } - pub fn with_source(self, source: Source) -> Self { - let src = Some(source.into()); - match self { - Error::ParseError { span, message, .. } => Error::ParseError { src, span, message }, - Error::DowngradeError { span, message, .. } => { - Error::DowngradeError { src, span, message } - } - Error::EvalError { - span, - message, - js_backtrace, - .. - } => Error::EvalError { - src, - span, - message, - js_backtrace, - }, - other => other, - } + pub fn with_source(mut self: Box, source: Source) -> Box { + use Error::*; + let new_src = Some(source.into()); + let (ParseError { src, .. } | DowngradeError { src, .. } | EvalError { src, .. }) = + self.as_mut() + else { + return self; + }; + *src = new_src; + self } } @@ -218,27 +193,27 @@ pub fn text_range_to_source_span(range: rnix::TextRange) -> SourceSpan { pub(crate) struct NixStackFrame { pub span: rnix::TextRange, pub message: String, + pub source: Source, } -/// 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 { +pub(crate) fn parse_nix_stack(stack: &str, ctx: &impl RuntimeContext) -> Vec { let mut frames = Vec::new(); for line in stack.lines() { - if !line.starts_with("NIX_STACK_FRAME:") { + // Format: NIX_STACK_FRAME:start:end[:extra_data] + let Some(rest) = line.strip_prefix("NIX_STACK_FRAME:") else { 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 source = match parts[0].parse() { + Ok(id) => ctx.get_source(id), + Err(_) => continue, + }; let start: u32 = match parts[1].parse() { Ok(v) => v, Err(_) => continue, @@ -250,28 +225,15 @@ pub(crate) fn parse_nix_stack(stack: &str) -> Vec { 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) - } + let message = { + if parts.len() == 4 { + parts[3].to_string() + } else { + String::new() } - "context" => { - if parts.len() >= 4 { - parts[3].to_string() - } else { - String::new() - } - } - _ => continue, }; - frames.push(NixStackFrame { span, message }); + frames.push(NixStackFrame { span, message, source }); } // Deduplicate consecutive identical frames @@ -279,20 +241,3 @@ pub(crate) fn parse_nix_stack(stack: &str) -> Vec { 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/metadata_cache.rs b/nix-js/src/fetcher/metadata_cache.rs index c9b7144..9eb70ad 100644 --- a/nix-js/src/fetcher/metadata_cache.rs +++ b/nix-js/src/fetcher/metadata_cache.rs @@ -53,7 +53,7 @@ impl CacheEntry { let now = SystemTime::now() .duration_since(UNIX_EPOCH) - .unwrap() + .expect("Clock may have gone backwards") .as_secs(); now > self.timestamp + ttl_seconds @@ -180,7 +180,7 @@ impl MetadataCache { let info_str = serde_json::to_string(info)?; let timestamp = SystemTime::now() .duration_since(UNIX_EPOCH) - .unwrap() + .expect("Clock may have gone backwards") .as_secs(); self.conn.execute( @@ -202,7 +202,7 @@ impl MetadataCache { let input_str = serde_json::to_string(input)?; let timestamp = SystemTime::now() .duration_since(UNIX_EPOCH) - .unwrap() + .expect("Clock may have gone backwards") .as_secs(); self.conn.execute( diff --git a/nix-js/src/ir.rs b/nix-js/src/ir.rs index e451fba..553e9a2 100644 --- a/nix-js/src/ir.rs +++ b/nix-js/src/ir.rs @@ -30,8 +30,7 @@ pub trait DowngradeContext { 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 get_current_source(&self) -> Source; fn with_param_scope(&mut self, param: SymId, arg: ExprId, f: F) -> R where diff --git a/nix-js/src/ir/downgrade.rs b/nix-js/src/ir/downgrade.rs index 7dee934..6027e1a 100644 --- a/nix-js/src/ir/downgrade.rs +++ b/nix-js/src/ir/downgrade.rs @@ -21,7 +21,7 @@ impl Downgrade for Expr { let span = error.syntax().text_range(); Err(self::Error::downgrade_error(error.to_string()) .with_span(span) - .with_source(ctx.get_current_source().expect("no source set"))) + .with_source(ctx.get_current_source())) } IfElse(ifelse) => ifelse.downgrade(ctx), Select(select) => select.downgrade(ctx), @@ -310,7 +310,7 @@ impl Downgrade for ast::LegacyLet { attrs.stcs.insert(sym, expr); } - Ok(ctx.new_expr(attrs.to_ir())) + Result::Ok(ctx.new_expr(attrs.to_ir())) })?; let body_sym = ctx.new_sym("body".to_string()); diff --git a/nix-js/src/ir/utils.rs b/nix-js/src/ir/utils.rs index 34a96be..314aeec 100644 --- a/nix-js/src/ir/utils.rs +++ b/nix-js/src/ir/utils.rs @@ -26,7 +26,7 @@ pub fn maybe_thunk(mut expr: ast::Expr, ctx: &mut impl DowngradeContext) -> Resu let span = error.syntax().text_range(); return Err(self::Error::downgrade_error(error.to_string()) .with_span(span) - .with_source(ctx.get_current_source().expect("no source set"))); + .with_source(ctx.get_current_source())); } Ident(ident) => return ident.downgrade(ctx), Literal(lit) => return lit.downgrade(ctx), @@ -136,7 +136,7 @@ pub fn downgrade_inherit( "dynamic attributes not allowed in inherit".to_string(), ) .with_span(span) - .with_source(ctx.get_current_source().expect("no source set"))); + .with_source(ctx.get_current_source())); } }; let expr = if let Some(expr) = from { @@ -166,7 +166,7 @@ pub fn downgrade_inherit( format_symbol(ctx.get_sym(*occupied.key())) )) .with_span(span) - .with_source(ctx.get_current_source().expect("no source set"))); + .with_source(ctx.get_current_source())); } Entry::Vacant(vacant) => vacant.insert(expr), }; @@ -248,7 +248,7 @@ pub fn downgrade_static_attrpathvalue( "dynamic attributes not allowed in let bindings".to_string(), ) .with_span(attrpath_node.syntax().text_range()) - .with_source(ctx.get_current_source().expect("no source set"))); + .with_source(ctx.get_current_source())); } let value = value.value().unwrap().downgrade(ctx)?; attrs.insert(path, value, ctx) @@ -296,7 +296,7 @@ where format_symbol(ctx.get_sym(sym)) )) .with_span(span) - .with_source(ctx.get_current_source().expect("no source set"))); + .with_source(ctx.get_current_source())); } let default_ast = entry.default(); @@ -447,7 +447,7 @@ where format_symbol(ctx.get_sym(sym)) )) .with_span(synthetic_span()) - .with_source(ctx.get_current_source().expect("no source set"))); + .with_source(ctx.get_current_source())); } } @@ -487,7 +487,7 @@ where format_symbol(ctx.get_sym(sym)) )) .with_span(ident.syntax().text_range()) - .with_source(ctx.get_current_source().expect("no source set"))); + .with_source(ctx.get_current_source())); } } } @@ -507,7 +507,7 @@ where format_symbol(ctx.get_sym(sym)) )) .with_span(ident.syntax().text_range()) - .with_source(ctx.get_current_source().expect("no source set"))); + .with_source(ctx.get_current_source())); } } } else if attrs_vec.len() > 1 { diff --git a/nix-js/src/runtime.rs b/nix-js/src/runtime.rs index 5d997ea..a0f1612 100644 --- a/nix-js/src/runtime.rs +++ b/nix-js/src/runtime.rs @@ -5,6 +5,7 @@ use std::sync::Once; use deno_core::{Extension, ExtensionFileSource, JsRuntime, OpState, RuntimeOptions, v8}; use deno_error::JsErrorClass; +use itertools::Itertools as _; use crate::error::{Error, Result, Source}; use crate::value::{AttrSet, List, Symbol, Value}; @@ -15,9 +16,10 @@ type LocalSymbol<'a> = v8::Local<'a, v8::Symbol>; pub(crate) trait RuntimeContext: 'static { fn get_current_dir(&self) -> &Path; - fn set_current_file(&mut self, path: Source); + fn add_source(&mut self, path: Source); fn compile_code(&mut self, source: Source) -> Result; - fn get_current_source(&self) -> Option; + fn get_current_source(&self) -> Source; + fn get_source(&self, id: usize) -> Source; } fn runtime_extension() -> Extension { @@ -104,7 +106,7 @@ fn op_import( }; tracing::debug!("Compiling file"); - ctx.set_current_file(source.clone()); + ctx.add_source(source.clone()); Ok(ctx.compile_code(source).map_err(|err| err.to_string())?) } @@ -439,46 +441,39 @@ impl Runtime { .js_runtime .execute_script("", script) .map_err(|e| { - let msg = format!("{}", e.get_message()); - let stack_str = e.stack.as_ref().map(|s| s.to_string()); + // Get current source from Context + let op_state = self.js_runtime.op_state(); + let op_state_borrow = op_state.borrow(); + let ctx = op_state_borrow.borrow::(); - let mut error = Error::eval_error(msg.clone(), None); + let msg = e.get_message().to_string(); + let mut span = None; + let mut source = None; // Parse Nix stack trace frames - if let Some(ref stack) = stack_str { - let frames = crate::error::parse_nix_stack(stack); + if let Some(stack) = &e.stack { + let frames = crate::error::parse_nix_stack(stack, ctx); - 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::() - && let Some(source) = ctx.get_current_source() - { - error = error.with_source(source); - } + if let Some(last_frame) = frames.last() { + span = Some(last_frame.span); + source = Some(last_frame.source.clone()) } } + let js_backtrace = e.stack.map(|stack| { + stack + .lines() + .filter(|line| !line.starts_with("NIX_STACK_FRAME:")) + .join("\n") + }); + let mut error = Error::eval_error(msg.clone(), js_backtrace); + if let Some(span) = span { + error = error.with_span(span); + } + if let Some(source) = source { + error = error.with_source(source); + } + error })?; @@ -532,7 +527,7 @@ impl Runtime { "failed to convert {symbol} Value to Symbol ({err})" )) })?; - Ok(v8::Global::new(scope, sym)) + Result::Ok(v8::Global::new(scope, sym)) }; let is_thunk = get_symbol("IS_THUNK")?; diff --git a/nix-js/tests/string_context.rs b/nix-js/tests/string_context.rs index 10d6ae6..3ae7131 100644 --- a/nix-js/tests/string_context.rs +++ b/nix-js/tests/string_context.rs @@ -5,7 +5,6 @@ use nix_js::value::Value; use utils::eval_result; fn eval(expr: &str) -> Value { - let mut ctx = Context::new().unwrap(); eval_result(expr).unwrap_or_else(|e| panic!("{}", e)) } diff --git a/nix-js/tests/utils.rs b/nix-js/tests/utils.rs index 554c4ed..1501a22 100644 --- a/nix-js/tests/utils.rs +++ b/nix-js/tests/utils.rs @@ -1,7 +1,7 @@ #![allow(dead_code)] use nix_js::context::Context; -use nix_js::error::Source; +use nix_js::error::{Result, Source}; use nix_js::value::Value; pub fn eval(expr: &str) -> Value { @@ -11,7 +11,7 @@ pub fn eval(expr: &str) -> Value { .unwrap() } -pub fn eval_result(expr: &str) -> Result { +pub fn eval_result(expr: &str) -> Result { Context::new() .unwrap() .eval_code(Source::new_eval(expr.into()).unwrap())