Show KCL backtraces (#7033)

* Add backtrace to errors

* Add display of backtraces with hints

* Change pane badge to only show count of errors

* Fix property name to not collide with Error superclass

* Increase min stack again

* Add e2e test that checks that the diagnostics are created in CodeMirror

* Remove unneeded code

* Change to the new hotness
This commit is contained in:
Jonathan Tran
2025-05-19 14:13:10 -04:00
committed by GitHub
parent bfa2f67393
commit ddb034b14d
53 changed files with 1543 additions and 1322 deletions

View File

@ -131,10 +131,10 @@ impl ExecutorContext {
match statement {
BodyItem::ImportStatement(import_stmt) => {
if !matches!(body_type, BodyType::Root) {
return Err(KclError::Semantic(KclErrorDetails {
message: "Imports are only supported at the top-level of a file.".to_owned(),
source_ranges: vec![import_stmt.into()],
}));
return Err(KclError::Semantic(KclErrorDetails::new(
"Imports are only supported at the top-level of a file.".to_owned(),
vec![import_stmt.into()],
)));
}
let source_range = SourceRange::from(import_stmt);
@ -157,28 +157,25 @@ impl ExecutorContext {
let mut ty = mem.get_from(&ty_name, env_ref, import_item.into(), 0).cloned();
if value.is_err() && ty.is_err() {
return Err(KclError::UndefinedValue(KclErrorDetails {
message: format!("{} is not defined in module", import_item.name.name),
source_ranges: vec![SourceRange::from(&import_item.name)],
}));
return Err(KclError::UndefinedValue(KclErrorDetails::new(
format!("{} is not defined in module", import_item.name.name),
vec![SourceRange::from(&import_item.name)],
)));
}
// Check that the item is allowed to be imported (in at least one namespace).
if value.is_ok() && !module_exports.contains(&import_item.name.name) {
value = Err(KclError::Semantic(KclErrorDetails {
message: format!(
value = Err(KclError::Semantic(KclErrorDetails::new(
format!(
"Cannot import \"{}\" from module because it is not exported. Add \"export\" before the definition to export it.",
import_item.name.name
),
source_ranges: vec![SourceRange::from(&import_item.name)],
}));
vec![SourceRange::from(&import_item.name)],
)));
}
if ty.is_ok() && !module_exports.contains(&ty_name) {
ty = Err(KclError::Semantic(KclErrorDetails {
message: String::new(),
source_ranges: vec![],
}));
ty = Err(KclError::Semantic(KclErrorDetails::new(String::new(), vec![])));
}
if value.is_err() && ty.is_err() {
@ -225,10 +222,10 @@ impl ExecutorContext {
.memory
.get_from(name, env_ref, source_range, 0)
.map_err(|_err| {
KclError::Internal(KclErrorDetails {
message: format!("{} is not defined in module (but was exported?)", name),
source_ranges: vec![source_range],
})
KclError::Internal(KclErrorDetails::new(
format!("{} is not defined in module (but was exported?)", name),
vec![source_range],
))
})?
.clone();
exec_state.mut_stack().add(name.to_owned(), item, source_range)?;
@ -297,10 +294,10 @@ impl ExecutorContext {
let std_path = match &exec_state.mod_local.std_path {
Some(p) => p,
None => {
return Err(KclError::Semantic(KclErrorDetails {
message: "User-defined types are not yet supported.".to_owned(),
source_ranges: vec![metadata.source_range],
}));
return Err(KclError::Semantic(KclErrorDetails::new(
"User-defined types are not yet supported.".to_owned(),
vec![metadata.source_range],
)));
}
};
let (t, props) = crate::std::std_ty(std_path, &ty.name.name);
@ -313,10 +310,10 @@ impl ExecutorContext {
.mut_stack()
.add(name_in_mem.clone(), value, metadata.source_range)
.map_err(|_| {
KclError::Semantic(KclErrorDetails {
message: format!("Redefinition of type {}.", ty.name.name),
source_ranges: vec![metadata.source_range],
})
KclError::Semantic(KclErrorDetails::new(
format!("Redefinition of type {}.", ty.name.name),
vec![metadata.source_range],
))
})?;
if let ItemVisibility::Export = ty.visibility {
@ -343,10 +340,10 @@ impl ExecutorContext {
.mut_stack()
.add(name_in_mem.clone(), value, metadata.source_range)
.map_err(|_| {
KclError::Semantic(KclErrorDetails {
message: format!("Redefinition of type {}.", ty.name.name),
source_ranges: vec![metadata.source_range],
})
KclError::Semantic(KclErrorDetails::new(
format!("Redefinition of type {}.", ty.name.name),
vec![metadata.source_range],
))
})?;
if let ItemVisibility::Export = ty.visibility {
@ -354,10 +351,10 @@ impl ExecutorContext {
}
}
None => {
return Err(KclError::Semantic(KclErrorDetails {
message: "User-defined types are not yet supported.".to_owned(),
source_ranges: vec![metadata.source_range],
}))
return Err(KclError::Semantic(KclErrorDetails::new(
"User-defined types are not yet supported.".to_owned(),
vec![metadata.source_range],
)))
}
},
}
@ -368,10 +365,10 @@ impl ExecutorContext {
let metadata = Metadata::from(return_statement);
if matches!(body_type, BodyType::Root) {
return Err(KclError::Semantic(KclErrorDetails {
message: "Cannot return from outside a function.".to_owned(),
source_ranges: vec![metadata.source_range],
}));
return Err(KclError::Semantic(KclErrorDetails::new(
"Cannot return from outside a function.".to_owned(),
vec![metadata.source_range],
)));
}
let value = self
@ -387,10 +384,10 @@ impl ExecutorContext {
.mut_stack()
.add(memory::RETURN_NAME.to_owned(), value, metadata.source_range)
.map_err(|_| {
KclError::Semantic(KclErrorDetails {
message: "Multiple returns from a single function.".to_owned(),
source_ranges: vec![metadata.source_range],
})
KclError::Semantic(KclErrorDetails::new(
"Multiple returns from a single function.".to_owned(),
vec![metadata.source_range],
))
})?;
last_expr = None;
}
@ -493,10 +490,10 @@ impl ExecutorContext {
*cache = Some((val, er, items.clone()));
(er, items)
}),
ModuleRepr::Foreign(geom, _) => Err(KclError::Semantic(KclErrorDetails {
message: "Cannot import items from foreign modules".to_owned(),
source_ranges: vec![geom.source_range],
})),
ModuleRepr::Foreign(geom, _) => Err(KclError::Semantic(KclErrorDetails::new(
"Cannot import items from foreign modules".to_owned(),
vec![geom.source_range],
))),
ModuleRepr::Dummy => unreachable!("Looking up {}, but it is still being interpreted", path),
};
@ -572,13 +569,13 @@ impl ExecutorContext {
err.override_source_ranges(vec![source_range])
} else {
// TODO would be great to have line/column for the underlying error here
KclError::Semantic(KclErrorDetails {
message: format!(
KclError::Semantic(KclErrorDetails::new(
format!(
"Error loading imported file ({path}). Open it to view more details.\n {}",
err.message()
),
source_ranges: vec![source_range],
})
vec![source_range],
))
}
})
}
@ -639,11 +636,10 @@ impl ExecutorContext {
meta: vec![metadata.to_owned()],
}
} else {
return Err(KclError::Semantic(KclErrorDetails {
message: "Rust implementation of functions is restricted to the standard library"
.to_owned(),
source_ranges: vec![metadata.source_range],
}));
return Err(KclError::Semantic(KclErrorDetails::new(
"Rust implementation of functions is restricted to the standard library".to_owned(),
vec![metadata.source_range],
)));
}
} else {
// Snapshotting memory here is crucial for semantics so that we close
@ -667,18 +663,18 @@ impl ExecutorContext {
"you cannot declare variable {name} as %, because % can only be used in function calls"
);
return Err(KclError::Semantic(KclErrorDetails {
return Err(KclError::Semantic(KclErrorDetails::new(
message,
source_ranges: vec![pipe_substitution.into()],
}));
vec![pipe_substitution.into()],
)));
}
StatementKind::Expression => match exec_state.mod_local.pipe_value.clone() {
Some(x) => x,
None => {
return Err(KclError::Semantic(KclErrorDetails {
message: "cannot use % outside a pipe expression".to_owned(),
source_ranges: vec![pipe_substitution.into()],
}));
return Err(KclError::Semantic(KclErrorDetails::new(
"cannot use % outside a pipe expression".to_owned(),
vec![pipe_substitution.into()],
)));
}
},
},
@ -750,13 +746,13 @@ fn apply_ascription(
} else {
""
};
KclError::Semantic(KclErrorDetails {
message: format!(
KclError::Semantic(KclErrorDetails::new(
format!(
"could not coerce {} value to type {ty}{suggestion}",
value.human_friendly_type()
),
source_ranges: vec![source_range],
})
vec![source_range],
))
})
}
@ -783,10 +779,10 @@ impl Node<Name> {
ctx: &ExecutorContext,
) -> Result<&'a KclValue, KclError> {
if self.abs_path {
return Err(KclError::Semantic(KclErrorDetails {
message: "Absolute paths (names beginning with `::` are not yet supported)".to_owned(),
source_ranges: self.as_source_ranges(),
}));
return Err(KclError::Semantic(KclErrorDetails::new(
"Absolute paths (names beginning with `::` are not yet supported)".to_owned(),
self.as_source_ranges(),
)));
}
if self.path.is_empty() {
@ -798,10 +794,10 @@ impl Node<Name> {
let value = match mem_spec {
Some((env, exports)) => {
if !exports.contains(&p.name) {
return Err(KclError::Semantic(KclErrorDetails {
message: format!("Item {} not found in module's exported items", p.name),
source_ranges: p.as_source_ranges(),
}));
return Err(KclError::Semantic(KclErrorDetails::new(
format!("Item {} not found in module's exported items", p.name),
p.as_source_ranges(),
)));
}
exec_state
@ -813,13 +809,13 @@ impl Node<Name> {
};
let KclValue::Module { value: module_id, .. } = value else {
return Err(KclError::Semantic(KclErrorDetails {
message: format!(
return Err(KclError::Semantic(KclErrorDetails::new(
format!(
"Identifier in path must refer to a module, found {}",
value.human_friendly_type()
),
source_ranges: p.as_source_ranges(),
}));
p.as_source_ranges(),
)));
};
mem_spec = Some(
@ -830,10 +826,10 @@ impl Node<Name> {
let (env, exports) = mem_spec.unwrap();
if !exports.contains(&self.name.name) {
return Err(KclError::Semantic(KclErrorDetails {
message: format!("Item {} not found in module's exported items", self.name.name),
source_ranges: self.name.as_source_ranges(),
}));
return Err(KclError::Semantic(KclErrorDetails::new(
format!("Item {} not found in module's exported items", self.name.name),
self.name.as_source_ranges(),
)));
}
exec_state
@ -861,46 +857,44 @@ impl Node<MemberExpression> {
if let Some(value) = map.get(&property) {
Ok(value.to_owned())
} else {
Err(KclError::UndefinedValue(KclErrorDetails {
message: format!("Property '{property}' not found in object"),
source_ranges: vec![self.clone().into()],
}))
Err(KclError::UndefinedValue(KclErrorDetails::new(
format!("Property '{property}' not found in object"),
vec![self.clone().into()],
)))
}
}
(KclValue::Object { .. }, Property::String(property), true) => Err(KclError::Semantic(KclErrorDetails {
message: format!("Cannot index object with string; use dot notation instead, e.g. `obj.{property}`"),
source_ranges: vec![self.clone().into()],
})),
(KclValue::Object { .. }, Property::String(property), true) => {
Err(KclError::Semantic(KclErrorDetails::new(
format!("Cannot index object with string; use dot notation instead, e.g. `obj.{property}`"),
vec![self.clone().into()],
)))
}
(KclValue::Object { .. }, p, _) => {
let t = p.type_name();
let article = article_for(t);
Err(KclError::Semantic(KclErrorDetails {
message: format!(
"Only strings can be used as the property of an object, but you're using {article} {t}",
),
source_ranges: vec![self.clone().into()],
}))
Err(KclError::Semantic(KclErrorDetails::new(
format!("Only strings can be used as the property of an object, but you're using {article} {t}",),
vec![self.clone().into()],
)))
}
(KclValue::HomArray { value: arr, .. }, Property::UInt(index), _) => {
let value_of_arr = arr.get(index);
if let Some(value) = value_of_arr {
Ok(value.to_owned())
} else {
Err(KclError::UndefinedValue(KclErrorDetails {
message: format!("The array doesn't have any item at index {index}"),
source_ranges: vec![self.clone().into()],
}))
Err(KclError::UndefinedValue(KclErrorDetails::new(
format!("The array doesn't have any item at index {index}"),
vec![self.clone().into()],
)))
}
}
(KclValue::HomArray { .. }, p, _) => {
let t = p.type_name();
let article = article_for(t);
Err(KclError::Semantic(KclErrorDetails {
message: format!(
"Only integers >= 0 can be used as the index of an array, but you're using {article} {t}",
),
source_ranges: vec![self.clone().into()],
}))
Err(KclError::Semantic(KclErrorDetails::new(
format!("Only integers >= 0 can be used as the index of an array, but you're using {article} {t}",),
vec![self.clone().into()],
)))
}
(KclValue::Solid { value }, Property::String(prop), false) if prop == "sketch" => Ok(KclValue::Sketch {
value: Box::new(value.sketch),
@ -918,10 +912,10 @@ impl Node<MemberExpression> {
(being_indexed, _, _) => {
let t = being_indexed.human_friendly_type();
let article = article_for(&t);
Err(KclError::Semantic(KclErrorDetails {
message: format!("Only arrays can be indexed, but you're trying to index {article} {t}"),
source_ranges: vec![self.clone().into()],
}))
Err(KclError::Semantic(KclErrorDetails::new(
format!("Only arrays can be indexed, but you're trying to index {article} {t}"),
vec![self.clone().into()],
)))
}
}
}
@ -996,26 +990,26 @@ impl Node<BinaryExpression> {
meta: _,
} = left_value
else {
return Err(KclError::Semantic(KclErrorDetails {
message: format!(
return Err(KclError::Semantic(KclErrorDetails::new(
format!(
"Cannot apply logical operator to non-boolean value: {}",
left_value.human_friendly_type()
),
source_ranges: vec![self.left.clone().into()],
}));
vec![self.left.clone().into()],
)));
};
let KclValue::Bool {
value: right_value,
meta: _,
} = right_value
else {
return Err(KclError::Semantic(KclErrorDetails {
message: format!(
return Err(KclError::Semantic(KclErrorDetails::new(
format!(
"Cannot apply logical operator to non-boolean value: {}",
right_value.human_friendly_type()
),
source_ranges: vec![self.right.clone().into()],
}));
vec![self.right.clone().into()],
)));
};
let raw_value = match self.operator {
BinaryOperator::Or => left_value || right_value,
@ -1115,13 +1109,13 @@ impl Node<UnaryExpression> {
meta: _,
} = value
else {
return Err(KclError::Semantic(KclErrorDetails {
message: format!(
return Err(KclError::Semantic(KclErrorDetails::new(
format!(
"Cannot apply unary operator ! to non-boolean value: {}",
value.human_friendly_type()
),
source_ranges: vec![self.into()],
}));
vec![self.into()],
)));
};
let meta = vec![Metadata {
source_range: self.into(),
@ -1136,13 +1130,13 @@ impl Node<UnaryExpression> {
let value = &self.argument.get_result(exec_state, ctx).await?;
let err = || {
KclError::Semantic(KclErrorDetails {
message: format!(
KclError::Semantic(KclErrorDetails::new(
format!(
"You can only negate numbers, planes, or lines, but this is a {}",
value.human_friendly_type()
),
source_ranges: vec![self.into()],
})
vec![self.into()],
))
};
match value {
KclValue::Number { value, ty, .. } => {
@ -1239,10 +1233,10 @@ pub(crate) async fn execute_pipe_body(
ctx: &ExecutorContext,
) -> Result<KclValue, KclError> {
let Some((first, body)) = body.split_first() else {
return Err(KclError::Semantic(KclErrorDetails {
message: "Pipe expressions cannot be empty".to_owned(),
source_ranges: vec![source_range],
}));
return Err(KclError::Semantic(KclErrorDetails::new(
"Pipe expressions cannot be empty".to_owned(),
vec![source_range],
)));
};
// Evaluate the first element in the pipeline.
// They use the pipe_value from some AST node above this, so that if pipe expression is nested in a larger pipe expression,
@ -1277,10 +1271,10 @@ async fn inner_execute_pipe_body(
) -> Result<KclValue, KclError> {
for expression in body {
if let Expr::TagDeclarator(_) = expression {
return Err(KclError::Semantic(KclErrorDetails {
message: format!("This cannot be in a PipeExpression: {:?}", expression),
source_ranges: vec![expression.into()],
}));
return Err(KclError::Semantic(KclErrorDetails::new(
format!("This cannot be in a PipeExpression: {:?}", expression),
vec![expression.into()],
)));
}
let metadata = Metadata {
source_range: SourceRange::from(expression),
@ -1349,35 +1343,37 @@ impl Node<ArrayRangeExpression> {
StatementKind::Expression,
)
.await?;
let (start, start_ty) = start_val.as_int_with_ty().ok_or(KclError::Semantic(KclErrorDetails {
source_ranges: vec![self.into()],
message: format!("Expected int but found {}", start_val.human_friendly_type()),
}))?;
let (start, start_ty) = start_val
.as_int_with_ty()
.ok_or(KclError::Semantic(KclErrorDetails::new(
format!("Expected int but found {}", start_val.human_friendly_type()),
vec![self.into()],
)))?;
let metadata = Metadata::from(&self.end_element);
let end_val = ctx
.execute_expr(&self.end_element, exec_state, &metadata, &[], StatementKind::Expression)
.await?;
let (end, end_ty) = end_val.as_int_with_ty().ok_or(KclError::Semantic(KclErrorDetails {
source_ranges: vec![self.into()],
message: format!("Expected int but found {}", end_val.human_friendly_type()),
}))?;
let (end, end_ty) = end_val.as_int_with_ty().ok_or(KclError::Semantic(KclErrorDetails::new(
format!("Expected int but found {}", end_val.human_friendly_type()),
vec![self.into()],
)))?;
if start_ty != end_ty {
let start = start_val.as_ty_f64().unwrap_or(TyF64 { n: 0.0, ty: start_ty });
let start = fmt::human_display_number(start.n, start.ty);
let end = end_val.as_ty_f64().unwrap_or(TyF64 { n: 0.0, ty: end_ty });
let end = fmt::human_display_number(end.n, end.ty);
return Err(KclError::Semantic(KclErrorDetails {
source_ranges: vec![self.into()],
message: format!("Range start and end must be of the same type, but found {start} and {end}"),
}));
return Err(KclError::Semantic(KclErrorDetails::new(
format!("Range start and end must be of the same type, but found {start} and {end}"),
vec![self.into()],
)));
}
if end < start {
return Err(KclError::Semantic(KclErrorDetails {
source_ranges: vec![self.into()],
message: format!("Range start is greater than range end: {start} .. {end}"),
}));
return Err(KclError::Semantic(KclErrorDetails::new(
format!("Range start is greater than range end: {start} .. {end}"),
vec![self.into()],
)));
}
let range: Vec<_> = if self.end_inclusive {
@ -1438,10 +1434,10 @@ fn article_for<S: AsRef<str>>(s: S) -> &'static str {
fn number_as_f64(v: &KclValue, source_range: SourceRange) -> Result<TyF64, KclError> {
v.as_ty_f64().ok_or_else(|| {
let actual_type = v.human_friendly_type();
KclError::Semantic(KclErrorDetails {
source_ranges: vec![source_range],
message: format!("Expected a number, but found {actual_type}",),
})
KclError::Semantic(KclErrorDetails::new(
format!("Expected a number, but found {actual_type}",),
vec![source_range],
))
})
}
@ -1530,16 +1526,16 @@ impl Property {
if let Some(x) = crate::try_f64_to_usize(value) {
Ok(Property::UInt(x))
} else {
Err(KclError::Semantic(KclErrorDetails {
source_ranges: property_sr,
message: format!("{value} is not a valid index, indices must be whole numbers >= 0"),
}))
Err(KclError::Semantic(KclErrorDetails::new(
format!("{value} is not a valid index, indices must be whole numbers >= 0"),
property_sr,
)))
}
}
_ => Err(KclError::Semantic(KclErrorDetails {
source_ranges: vec![sr],
message: "Only numbers (>= 0) can be indexes".to_owned(),
})),
_ => Err(KclError::Semantic(KclErrorDetails::new(
"Only numbers (>= 0) can be indexes".to_owned(),
vec![sr],
))),
}
}
}
@ -1547,12 +1543,7 @@ impl Property {
}
fn jvalue_to_prop(value: &KclValue, property_sr: Vec<SourceRange>, name: &str) -> Result<Property, KclError> {
let make_err = |message: String| {
Err::<Property, _>(KclError::Semantic(KclErrorDetails {
source_ranges: property_sr,
message,
}))
};
let make_err = |message: String| Err::<Property, _>(KclError::Semantic(KclErrorDetails::new(message, property_sr)));
match value {
KclValue::Number{value: num, .. } => {
let num = *num;
@ -1795,10 +1786,10 @@ d = b + c
crate::engine::conn_mock::EngineConnection::new()
.await
.map_err(|err| {
KclError::Internal(crate::errors::KclErrorDetails {
message: format!("Failed to create mock engine connection: {}", err),
source_ranges: vec![SourceRange::default()],
})
KclError::Internal(KclErrorDetails::new(
format!("Failed to create mock engine connection: {}", err),
vec![SourceRange::default()],
))
})
.unwrap(),
)),