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

View File

@@ -3,6 +3,9 @@
use std::path::{Path, PathBuf};
use std::sync::Arc;
use deno_core::error::JsError;
use deno_error::JsErrorClass as _;
use itertools::Itertools as _;
use miette::{Diagnostic, NamedSource, SourceSpan};
use thiserror::Error;
@@ -113,6 +116,8 @@ pub enum Error {
message: String,
#[help]
js_backtrace: Option<String>,
#[related]
stack_trace: Vec<StackFrame>,
},
#[error("Internal error: {message}")]
@@ -153,6 +158,7 @@ impl Error {
span: None,
message: msg,
js_backtrace: backtrace,
stack_trace: Vec::new(),
}
.into()
}
@@ -197,14 +203,72 @@ pub fn text_range_to_source_span(range: rnix::TextRange) -> SourceSpan {
}
/// Stack frame types from Nix evaluation
#[derive(Debug, Clone)]
pub(crate) struct NixStackFrame {
pub span: rnix::TextRange,
#[derive(Debug, Clone, Error, Diagnostic)]
#[error("{message}")]
pub struct StackFrame {
#[label]
pub span: SourceSpan,
#[help]
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();
for line in stack.lines() {
@@ -218,7 +282,7 @@ pub(crate) fn parse_nix_stack(stack: &str, ctx: &impl RuntimeContext) -> Vec<Nix
continue;
}
let source = match parts[0].parse() {
let src = match parts[0].parse() {
Ok(id) => ctx.get_source(id),
Err(_) => continue,
};
@@ -241,11 +305,7 @@ pub(crate) fn parse_nix_stack(stack: &str, ctx: &impl RuntimeContext) -> Vec<Nix
}
};
frames.push(NixStackFrame {
span,
message,
source,
});
frames.push(NixStackFrame { span, message, src });
}
// Deduplicate consecutive identical frames
@@ -253,3 +313,34 @@ pub(crate) fn parse_nix_stack(stack: &str, ctx: &impl RuntimeContext) -> Vec<Nix
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 part = parts.into_iter().next().unwrap();
if let &Ir::Str(Str { ref val, span }) = ctx.get_ir(part)
&& let Some(path) = val.strip_prefix("<").map(|path| &path[..path.len() - 1]) {
ctx.replace_ir(part, Str { val: path.to_string(), span }.to_ir());
&& let Some(path) = val.strip_prefix("<").map(|path| &path[..path.len() - 1])
{
ctx.replace_ir(
part,
Str {
val: path.to_string(),
span,
}
.to_ir(),
);
let sym = ctx.new_sym("findFile".into());
let find_file = ctx.new_expr(Builtin { inner: sym, span }.to_ir());
let sym = ctx.new_sym("nixPath".into());
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());
return Ok(ctx.new_expr(Call { func: call, arg: part, span }.to_ir()));
let call = ctx.new_expr(
Call {
func: find_file,
arg: nix_path,
span,
}
.to_ir(),
);
return Ok(ctx.new_expr(
Call {
func: call,
arg: part,
span,
}
.to_ir(),
));
} else {
part
}

View File

@@ -414,9 +414,10 @@ where
.to_ir(),
);
} else {
return Err(Error::internal(
format!("binding '{}' not found", format_symbol(ctx.get_sym(sym))),
));
return Err(Error::internal(format!(
"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,
)

View File

@@ -4,8 +4,6 @@ use std::path::{Component, Path, PathBuf};
use std::sync::{Arc, Once};
use deno_core::{Extension, ExtensionFileSource, JsRuntime, OpState, RuntimeOptions, v8};
use deno_error::JsErrorClass;
use itertools::Itertools as _;
use rust_embed::Embed;
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]
#[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);
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 file_name = entry.file_name().to_string_lossy().to_string();
let file_type = entry.file_type()
.map_err(|e| format!("Failed to read file type for {}: {}", entry.path().display(), e))?;
let file_type = entry.file_type().map_err(|e| {
format!(
"Failed to read file type for {}: {}",
entry.path().display(),
e
)
})?;
let type_str = if file_type.is_dir() {
"directory"
@@ -584,41 +589,13 @@ impl<Ctx: RuntimeContext> Runtime<Ctx> {
let global_value = self
.js_runtime
.execute_script("<eval>", script)
.map_err(|e| {
.map_err(|error| {
// Get current source from Context
let op_state = self.js_runtime.op_state();
let op_state_borrow = op_state.borrow();
let ctx: &Ctx = op_state_borrow.get_ctx();
let msg = e.get_message().to_string();
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
crate::error::parse_js_error(error, ctx)
})?;
// Retrieve scope from JsRuntime

View File

@@ -320,5 +320,8 @@ fn builtins_function_args() {
fn builtins_parse_drv_name() {
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("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"
);
} 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") {

View File

@@ -313,9 +313,18 @@ fn read_dir_basic() {
let result = eval(&expr);
if let Value::AttrSet(attrs) = result {
assert_eq!(attrs.get("file1.txt"), 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.get("file1.txt"),
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);
} else {
panic!("Expected AttrSet, got {:?}", result);
@@ -359,4 +368,3 @@ fn read_dir_on_file_fails() {
let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("not a directory"));
}