feat(error): stack trace

This commit is contained in:
2026-01-25 00:48:53 +08:00
parent 4d68fb26d9
commit 3186cfe6e4
8 changed files with 195 additions and 79 deletions

View File

@@ -373,23 +373,27 @@ impl<Ctx: CodegenContext> Compile<Ctx> for AttrSet {
} }
if !self.dyns.is_empty() { if !self.dyns.is_empty() {
let (keys, vals, dyn_spans) = self.dyns.iter().map(|(key, val, attr_span)| { let (keys, vals, dyn_spans) = self
let key = ctx.get_ir(*key).compile(ctx); .dyns
let val_expr = ctx.get_ir(*val); .iter()
let val = val_expr.compile(ctx); .map(|(key, val, attr_span)| {
let span = val_expr.span(); let key = ctx.get_ir(*key).compile(ctx);
let val = if stack_trace_enabled { let val_expr = ctx.get_ir(*val);
let span = encode_span(span, ctx); let val = val_expr.compile(ctx);
format!( let span = val_expr.span();
"Nix.withContext(\"while evaluating a dynamic attribute\",{},()=>({}))", let val = if stack_trace_enabled {
span, val let span = encode_span(span, ctx);
) format!(
} else { "Nix.withContext(\"while evaluating a dynamic attribute\",{},()=>({}))",
val span, val
}; )
let dyn_span_str = encode_span(*attr_span, ctx); } else {
(key, val, dyn_span_str) val
}).multiunzip::<(Vec<_>, Vec<_>, Vec<_>)>(); };
let dyn_span_str = encode_span(*attr_span, ctx);
(key, val, dyn_span_str)
})
.multiunzip::<(Vec<_>, Vec<_>, Vec<_>)>();
format!( format!(
"Nix.mkAttrsWithPos({{{}}},{{{}}},{{dynKeys:[{}],dynVals:[{}],dynSpans:[{}]}})", "Nix.mkAttrsWithPos({{{}}},{{{}}},{{dynKeys:[{}],dynVals:[{}],dynSpans:[{}]}})",
attrs.join(","), attrs.join(","),
@@ -399,11 +403,14 @@ impl<Ctx: CodegenContext> Compile<Ctx> for AttrSet {
dyn_spans.join(",") dyn_spans.join(",")
) )
} else if !attr_positions.is_empty() { } else if !attr_positions.is_empty() {
format!("Nix.mkAttrsWithPos({{{}}},{{{}}})", attrs.join(","), attr_positions.join(",")) format!(
"Nix.mkAttrsWithPos({{{}}},{{{}}})",
attrs.join(","),
attr_positions.join(",")
)
} else { } else {
format!("{{{}}}", attrs.join(",")) format!("{{{}}}", attrs.join(","))
} }
} }
} }

View File

@@ -3,6 +3,9 @@
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::Arc; use std::sync::Arc;
use deno_core::error::JsError;
use deno_error::JsErrorClass as _;
use itertools::Itertools as _;
use miette::{Diagnostic, NamedSource, SourceSpan}; use miette::{Diagnostic, NamedSource, SourceSpan};
use thiserror::Error; use thiserror::Error;
@@ -113,6 +116,8 @@ pub enum Error {
message: String, message: String,
#[help] #[help]
js_backtrace: Option<String>, js_backtrace: Option<String>,
#[related]
stack_trace: Vec<StackFrame>,
}, },
#[error("Internal error: {message}")] #[error("Internal error: {message}")]
@@ -153,6 +158,7 @@ impl Error {
span: None, span: None,
message: msg, message: msg,
js_backtrace: backtrace, js_backtrace: backtrace,
stack_trace: Vec::new(),
} }
.into() .into()
} }
@@ -197,14 +203,72 @@ pub fn text_range_to_source_span(range: rnix::TextRange) -> SourceSpan {
} }
/// Stack frame types from Nix evaluation /// Stack frame types from Nix evaluation
#[derive(Debug, Clone)] #[derive(Debug, Clone, Error, Diagnostic)]
pub(crate) struct NixStackFrame { #[error("{message}")]
pub span: rnix::TextRange, pub struct StackFrame {
#[label]
pub span: SourceSpan,
#[help]
pub message: String, pub message: String,
pub source: Source, #[source_code]
pub src: NamedSource<Arc<str>>,
} }
pub(crate) fn parse_nix_stack(stack: &str, ctx: &impl RuntimeContext) -> Vec<NixStackFrame> { const MAX_STACK_FRAMES: usize = 10;
const FRAMES_AT_START: usize = 5;
const FRAMES_AT_END: usize = 5;
pub(crate) fn parse_js_error(error: Box<JsError>, ctx: &impl RuntimeContext) -> Error {
let (span, src, frames) = if let Some(stack) = &error.stack {
let mut frames = parse_frames(stack, ctx);
if let Some(last_frame) = frames.pop() {
(
Some(text_range_to_source_span(last_frame.span)),
Some(last_frame.src.into()),
frames,
)
} else {
(None, None, frames)
}
} else {
(None, None, Vec::new())
};
let stack_trace = truncate_stack_trace(frames);
let message = error.get_message().to_string();
let js_backtrace = error.stack.map(|stack| {
stack
.lines()
.filter(|line| !line.starts_with("NIX_STACK_FRAME:"))
.join("\n")
});
Error::EvalError {
src,
span,
message,
js_backtrace,
stack_trace,
}
}
struct NixStackFrame {
span: rnix::TextRange,
message: String,
src: Source,
}
impl From<NixStackFrame> for StackFrame {
fn from(NixStackFrame { span, message, src }: NixStackFrame) -> Self {
StackFrame {
span: text_range_to_source_span(span),
message,
src: src.into(),
}
}
}
fn parse_frames(stack: &str, ctx: &impl RuntimeContext) -> Vec<NixStackFrame> {
let mut frames = Vec::new(); let mut frames = Vec::new();
for line in stack.lines() { for line in stack.lines() {
@@ -218,7 +282,7 @@ pub(crate) fn parse_nix_stack(stack: &str, ctx: &impl RuntimeContext) -> Vec<Nix
continue; continue;
} }
let source = match parts[0].parse() { let src = match parts[0].parse() {
Ok(id) => ctx.get_source(id), Ok(id) => ctx.get_source(id),
Err(_) => continue, Err(_) => continue,
}; };
@@ -241,11 +305,7 @@ pub(crate) fn parse_nix_stack(stack: &str, ctx: &impl RuntimeContext) -> Vec<Nix
} }
}; };
frames.push(NixStackFrame { frames.push(NixStackFrame { span, message, src });
span,
message,
source,
});
} }
// Deduplicate consecutive identical frames // Deduplicate consecutive identical frames
@@ -253,3 +313,34 @@ pub(crate) fn parse_nix_stack(stack: &str, ctx: &impl RuntimeContext) -> Vec<Nix
frames frames
} }
fn truncate_stack_trace(frames: Vec<NixStackFrame>) -> Vec<StackFrame> {
let reversed: Vec<_> = frames.into_iter().rev().collect();
let total = reversed.len();
if total <= MAX_STACK_FRAMES {
return reversed.into_iter().map(Into::into).collect();
}
let omitted_count = total - FRAMES_AT_START - FRAMES_AT_END;
reversed
.into_iter()
.enumerate()
.filter_map(|(i, frame)| {
if i < FRAMES_AT_START {
Some(frame.into())
} else if i == FRAMES_AT_START {
Some(StackFrame {
span: text_range_to_source_span(frame.span),
message: format!("... ({} more frames omitted)", omitted_count),
src: frame.src.into(),
})
} else if i >= total - FRAMES_AT_END {
Some(frame.into())
} else {
None
}
})
.collect()
}

View File

@@ -105,14 +105,36 @@ impl<Ctx: DowngradeContext> Downgrade<Ctx> for ast::Path {
let expr = if parts.len() == 1 { let expr = if parts.len() == 1 {
let part = parts.into_iter().next().unwrap(); let part = parts.into_iter().next().unwrap();
if let &Ir::Str(Str { ref val, span }) = ctx.get_ir(part) if let &Ir::Str(Str { ref val, span }) = ctx.get_ir(part)
&& let Some(path) = val.strip_prefix("<").map(|path| &path[..path.len() - 1]) { && let Some(path) = val.strip_prefix("<").map(|path| &path[..path.len() - 1])
ctx.replace_ir(part, Str { val: path.to_string(), span }.to_ir()); {
ctx.replace_ir(
part,
Str {
val: path.to_string(),
span,
}
.to_ir(),
);
let sym = ctx.new_sym("findFile".into()); let sym = ctx.new_sym("findFile".into());
let find_file = ctx.new_expr(Builtin { inner: sym, span }.to_ir()); let find_file = ctx.new_expr(Builtin { inner: sym, span }.to_ir());
let sym = ctx.new_sym("nixPath".into()); let sym = ctx.new_sym("nixPath".into());
let nix_path = ctx.new_expr(Builtin { inner: sym, span }.to_ir()); let nix_path = ctx.new_expr(Builtin { inner: sym, span }.to_ir());
let call = ctx.new_expr(Call { func: find_file, arg: nix_path, span }.to_ir()); let call = ctx.new_expr(
return Ok(ctx.new_expr(Call { func: call, arg: part, span }.to_ir())); Call {
func: find_file,
arg: nix_path,
span,
}
.to_ir(),
);
return Ok(ctx.new_expr(
Call {
func: call,
arg: part,
span,
}
.to_ir(),
));
} else { } else {
part part
} }

View File

@@ -414,9 +414,10 @@ where
.to_ir(), .to_ir(),
); );
} else { } else {
return Err(Error::internal( return Err(Error::internal(format!(
format!("binding '{}' not found", format_symbol(ctx.get_sym(sym))), "binding '{}' not found",
)); format_symbol(ctx.get_sym(sym))
)));
} }
} }
@@ -534,7 +535,11 @@ where
} }
} }
Ok(temp_attrs.stcs.into_iter().map(|(k, (v, _))| (k, v)).collect()) Ok(temp_attrs
.stcs
.into_iter()
.map(|(k, (v, _))| (k, v))
.collect())
}, },
body_fn, body_fn,
) )

View File

@@ -4,8 +4,6 @@ use std::path::{Component, Path, PathBuf};
use std::sync::{Arc, Once}; use std::sync::{Arc, Once};
use deno_core::{Extension, ExtensionFileSource, JsRuntime, OpState, RuntimeOptions, v8}; use deno_core::{Extension, ExtensionFileSource, JsRuntime, OpState, RuntimeOptions, v8};
use deno_error::JsErrorClass;
use itertools::Itertools as _;
use rust_embed::Embed; use rust_embed::Embed;
use crate::error::{Error, Result, Source}; use crate::error::{Error, Result, Source};
@@ -189,7 +187,9 @@ fn op_read_file_type(#[string] path: String) -> std::result::Result<String, NixE
#[deno_core::op2] #[deno_core::op2]
#[serde] #[serde]
fn op_read_dir(#[string] path: String) -> std::result::Result<std::collections::HashMap<String, String>, NixError> { fn op_read_dir(
#[string] path: String,
) -> std::result::Result<std::collections::HashMap<String, String>, NixError> {
let path = Path::new(&path); let path = Path::new(&path);
if !path.is_dir() { if !path.is_dir() {
@@ -205,8 +205,13 @@ fn op_read_dir(#[string] path: String) -> std::result::Result<std::collections::
let entry = entry.map_err(|e| format!("Failed to read directory entry: {}", e))?; let entry = entry.map_err(|e| format!("Failed to read directory entry: {}", e))?;
let file_name = entry.file_name().to_string_lossy().to_string(); let file_name = entry.file_name().to_string_lossy().to_string();
let file_type = entry.file_type() let file_type = entry.file_type().map_err(|e| {
.map_err(|e| format!("Failed to read file type for {}: {}", entry.path().display(), e))?; format!(
"Failed to read file type for {}: {}",
entry.path().display(),
e
)
})?;
let type_str = if file_type.is_dir() { let type_str = if file_type.is_dir() {
"directory" "directory"
@@ -584,41 +589,13 @@ impl<Ctx: RuntimeContext> Runtime<Ctx> {
let global_value = self let global_value = self
.js_runtime .js_runtime
.execute_script("<eval>", script) .execute_script("<eval>", script)
.map_err(|e| { .map_err(|error| {
// Get current source from Context // Get current source from Context
let op_state = self.js_runtime.op_state(); let op_state = self.js_runtime.op_state();
let op_state_borrow = op_state.borrow(); let op_state_borrow = op_state.borrow();
let ctx: &Ctx = op_state_borrow.get_ctx(); let ctx: &Ctx = op_state_borrow.get_ctx();
let msg = e.get_message().to_string(); crate::error::parse_js_error(error, ctx)
let mut span = None;
let mut source = None;
// Parse Nix stack trace frames
if let Some(stack) = &e.stack {
let frames = crate::error::parse_nix_stack(stack, ctx);
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
})?; })?;
// Retrieve scope from JsRuntime // Retrieve scope from JsRuntime

View File

@@ -320,5 +320,8 @@ fn builtins_function_args() {
fn builtins_parse_drv_name() { fn builtins_parse_drv_name() {
let result = eval(r#"builtins.parseDrvName "nix-js-0.1.0pre""#).unwrap_attr_set(); let result = eval(r#"builtins.parseDrvName "nix-js-0.1.0pre""#).unwrap_attr_set();
assert_eq!(result.get("name"), Some(&Value::String("nix-js".into()))); assert_eq!(result.get("name"), Some(&Value::String("nix-js".into())));
assert_eq!(result.get("version"), Some(&Value::String("0.1.0pre".into()))); assert_eq!(
result.get("version"),
Some(&Value::String("0.1.0pre".into()))
);
} }

View File

@@ -212,7 +212,10 @@ fn multi_output_two_outputs() {
"/nix/store/vmyjryfipkn9ss3ya23hk8p3m58l6dsl-multi.drv" "/nix/store/vmyjryfipkn9ss3ya23hk8p3m58l6dsl-multi.drv"
); );
} else { } else {
panic!("drvPath should be a string, got: {:?}", attrs.get("drvPath")); panic!(
"drvPath should be a string, got: {:?}",
attrs.get("drvPath")
);
} }
if let Some(Value::String(out_path)) = attrs.get("outPath") { if let Some(Value::String(out_path)) = attrs.get("outPath") {

View File

@@ -313,9 +313,18 @@ fn read_dir_basic() {
let result = eval(&expr); let result = eval(&expr);
if let Value::AttrSet(attrs) = result { if let Value::AttrSet(attrs) = result {
assert_eq!(attrs.get("file1.txt"), Some(&Value::String("regular".to_string()))); assert_eq!(
assert_eq!(attrs.get("file2.txt"), Some(&Value::String("regular".to_string()))); attrs.get("file1.txt"),
assert_eq!(attrs.get("subdir"), Some(&Value::String("directory".to_string()))); Some(&Value::String("regular".to_string()))
);
assert_eq!(
attrs.get("file2.txt"),
Some(&Value::String("regular".to_string()))
);
assert_eq!(
attrs.get("subdir"),
Some(&Value::String("directory".to_string()))
);
assert_eq!(attrs.len(), 3); assert_eq!(attrs.len(), 3);
} else { } else {
panic!("Expected AttrSet, got {:?}", result); panic!("Expected AttrSet, got {:?}", result);
@@ -359,4 +368,3 @@ fn read_dir_on_file_fails() {
let err_msg = result.unwrap_err().to_string(); let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("not a directory")); assert!(err_msg.contains("not a directory"));
} }