Support multiple errors and warnings in the parser (#4534)

Signed-off-by: Nick Cameron <nrc@ncameron.org>
This commit is contained in:
Nick Cameron
2024-11-22 13:25:14 +13:00
committed by GitHub
parent 676ac201bb
commit 455fb49fb6
13 changed files with 436 additions and 232 deletions

View File

@ -185,7 +185,9 @@ pub async fn modify_ast_for_sketch(
let recasted = program.ast.recast(&FormatOptions::default(), 0); let recasted = program.ast.recast(&FormatOptions::default(), 0);
// Re-parse the ast so we get the correct source ranges. // Re-parse the ast so we get the correct source ranges.
*program = crate::parser::parse_str(&recasted, module_id)?.into(); *program = crate::parser::parse_str(&recasted, module_id)
.parse_errs_as_err()?
.into();
Ok(recasted) Ok(recasted)
} }

View File

@ -3491,36 +3491,6 @@ const cylinder = startSketchOn('-XZ')
assert_eq!(l.raw, "false"); assert_eq!(l.raw, "false");
} }
#[tokio::test(flavor = "multi_thread")]
async fn test_parse_tag_named_std_lib() {
let some_program_string = r#"startSketchOn('XY')
|> startProfileAt([0, 0], %)
|> line([5, 5], %, $xLine)
"#;
let result = crate::parser::top_level_parse(some_program_string);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().to_string(),
r#"syntax: KclErrorDetails { source_ranges: [SourceRange([76, 82, 0])], message: "Cannot assign a tag to a reserved keyword: xLine" }"#
);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_parse_empty_tag() {
let some_program_string = r#"startSketchOn('XY')
|> startProfileAt([0, 0], %)
|> line([5, 5], %, $)
"#;
let result = crate::parser::top_level_parse(some_program_string);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().to_string(),
r#"syntax: KclErrorDetails { source_ranges: [SourceRange([57, 59, 0])], message: "Unexpected token: |>" }"#
);
}
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn test_parse_digest() { async fn test_parse_digest() {
let prog1_string = r#"startSketchOn('XY') let prog1_string = r#"startSketchOn('XY')

View File

@ -44,6 +44,13 @@ pub struct KclErrorDetails {
} }
impl KclError { impl KclError {
pub fn internal(message: String) -> KclError {
KclError::Internal(KclErrorDetails {
source_ranges: Default::default(),
message,
})
}
/// Get the error message. /// Get the error message.
pub fn get_message(&self) -> String { pub fn get_message(&self) -> String {
format!("{}: {}", self.error_type(), self.message()) format!("{}: {}", self.error_type(), self.message())

View File

@ -1954,7 +1954,8 @@ impl ExecutorContext {
} }
let module_id = exec_state.add_module(resolved_path.clone()); let module_id = exec_state.add_module(resolved_path.clone());
let source = self.fs.read_to_string(&resolved_path, source_range).await?; let source = self.fs.read_to_string(&resolved_path, source_range).await?;
let program = crate::parser::parse_str(&source, module_id)?; // TODO handle parsing errors properly
let program = crate::parser::parse_str(&source, module_id).parse_errs_as_err()?;
let (module_memory, module_exports) = { let (module_memory, module_exports) = {
exec_state.import_stack.push(resolved_path.clone()); exec_state.import_stack.push(resolved_path.clone());
let original_execution = self.engine.replace_execution_kind(ExecutionKind::Isolated); let original_execution = self.engine.replace_execution_kind(ExecutionKind::Isolated);

View File

@ -89,7 +89,8 @@ impl Program {
pub fn parse(input: &str) -> Result<Program, KclError> { pub fn parse(input: &str) -> Result<Program, KclError> {
let module_id = ModuleId::default(); let module_id = ModuleId::default();
let tokens = token::lexer(input, module_id)?; let tokens = token::lexer(input, module_id)?;
let ast = parser::parse_tokens(tokens)?; // TODO handle parsing errors properly
let ast = parser::parse_tokens(tokens).parse_errs_as_err()?;
Ok(Program { ast }) Ok(Program { ast })
} }

View File

@ -299,7 +299,8 @@ impl crate::lsp::backend::Backend for Backend {
// Lets update the ast. // Lets update the ast.
let result = crate::parser::parse_tokens(tokens.clone()); let result = crate::parser::parse_tokens(tokens.clone());
let mut ast = match result { // TODO handle parse errors properly
let mut ast = match result.parse_errs_as_err() {
Ok(ast) => ast, Ok(ast) => ast,
Err(err) => { Err(err) => {
self.add_to_diagnostics(&params, &[err], true).await; self.add_to_diagnostics(&params, &[err], true).await;
@ -1301,7 +1302,7 @@ impl LanguageServer for Backend {
// I don't know if we need to do this again since it should be updated in the context. // I don't know if we need to do this again since it should be updated in the context.
// But I figure better safe than sorry since this will write back out to the file. // But I figure better safe than sorry since this will write back out to the file.
let module_id = ModuleId::default(); let module_id = ModuleId::default();
let Ok(ast) = crate::parser::parse_str(current_code, module_id) else { let Ok(ast) = crate::parser::parse_str(current_code, module_id).parse_errs_as_err() else {
return Ok(None); return Ok(None);
}; };
// Now recast it. // Now recast it.
@ -1335,7 +1336,7 @@ impl LanguageServer for Backend {
// I don't know if we need to do this again since it should be updated in the context. // I don't know if we need to do this again since it should be updated in the context.
// But I figure better safe than sorry since this will write back out to the file. // But I figure better safe than sorry since this will write back out to the file.
let module_id = ModuleId::default(); let module_id = ModuleId::default();
let Ok(mut ast) = crate::parser::parse_str(current_code, module_id) else { let Ok(mut ast) = crate::parser::parse_str(current_code, module_id).parse_errs_as_err() else {
return Ok(None); return Ok(None);
}; };

View File

@ -1,3 +1,5 @@
use parser_impl::ParseContext;
use crate::{ use crate::{
ast::types::{ModuleId, Node, Program}, ast::types::{ModuleId, Node, Program},
errors::{KclError, KclErrorDetails}, errors::{KclError, KclErrorDetails},
@ -12,20 +14,31 @@ pub(crate) mod parser_impl;
pub const PIPE_SUBSTITUTION_OPERATOR: &str = "%"; pub const PIPE_SUBSTITUTION_OPERATOR: &str = "%";
pub const PIPE_OPERATOR: &str = "|>"; pub const PIPE_OPERATOR: &str = "|>";
// `?` like behavior for `Result`s to return a ParseResult if there is an error.
macro_rules! pr_try {
($e: expr) => {
match $e {
Ok(a) => a,
Err(e) => return e.into(),
}
};
}
#[cfg(test)] #[cfg(test)]
/// Parse the given KCL code into an AST. This is the top-level. /// Parse the given KCL code into an AST. This is the top-level.
pub fn top_level_parse(code: &str) -> Result<Node<Program>, KclError> { pub fn top_level_parse(code: &str) -> ParseResult {
let module_id = ModuleId::default(); let module_id = ModuleId::default();
parse_str(code, module_id) parse_str(code, module_id)
} }
/// Parse the given KCL code into an AST. /// Parse the given KCL code into an AST.
pub fn parse_str(code: &str, module_id: ModuleId) -> Result<Node<Program>, KclError> { pub fn parse_str(code: &str, module_id: ModuleId) -> ParseResult {
let tokens = crate::token::lexer(code, module_id)?; let tokens = pr_try!(crate::token::lexer(code, module_id));
parse_tokens(tokens) parse_tokens(tokens)
} }
pub fn parse_tokens(tokens: Vec<Token>) -> Result<Node<Program>, KclError> { /// Parse the supplied tokens into an AST.
pub fn parse_tokens(tokens: Vec<Token>) -> ParseResult {
let (tokens, unknown_tokens): (Vec<Token>, Vec<Token>) = tokens let (tokens, unknown_tokens): (Vec<Token>, Vec<Token>) = tokens
.into_iter() .into_iter()
.partition(|token| token.token_type != TokenType::Unknown); .partition(|token| token.token_type != TokenType::Unknown);
@ -38,13 +51,13 @@ pub fn parse_tokens(tokens: Vec<Token>) -> Result<Node<Program>, KclError> {
} else { } else {
format!("found unknown tokens [{}]", token_list.join(", ")) format!("found unknown tokens [{}]", token_list.join(", "))
}; };
return Err(KclError::Lexical(KclErrorDetails { source_ranges, message })); return KclError::Lexical(KclErrorDetails { source_ranges, message }).into();
} }
// Important, to not call this before the unknown tokens check. // Important, to not call this before the unknown tokens check.
if tokens.is_empty() { if tokens.is_empty() {
// Empty file should just do nothing. // Empty file should just do nothing.
return Ok(Node::<Program>::default()); return Node::<Program>::default().into();
} }
// Check all the tokens are whitespace or comments. // Check all the tokens are whitespace or comments.
@ -52,8 +65,78 @@ pub fn parse_tokens(tokens: Vec<Token>) -> Result<Node<Program>, KclError> {
.iter() .iter()
.all(|t| t.token_type.is_whitespace() || t.token_type.is_comment()) .all(|t| t.token_type.is_whitespace() || t.token_type.is_comment())
{ {
return Ok(Node::<Program>::default()); return Node::<Program>::default().into();
} }
parser_impl::run_parser(&mut tokens.as_slice()) parser_impl::run_parser(&mut tokens.as_slice())
} }
/// Result of parsing.
///
/// Will be a KclError if there was a lexing error or some unexpected error during parsing.
/// TODO - lexing errors should be included with the parse errors.
/// Will be Ok otherwise, including if there were parsing errors. Any errors or warnings will
/// be in the ParseContext. If an AST was produced, then that will be in the Option.
///
/// Invariants:
/// - if there are no errors, then the Option will be Some
/// - if the Option is None, then there will be at least one error in the ParseContext.
pub(crate) struct ParseResult(pub Result<(Option<Node<Program>>, ParseContext), KclError>);
impl ParseResult {
#[cfg(test)]
pub fn unwrap(self) -> Node<Program> {
self.0.unwrap().0.unwrap()
}
#[cfg(test)]
pub fn is_ok(&self) -> bool {
match &self.0 {
Ok((p, pc)) => p.is_some() && pc.errors.is_empty(),
Err(_) => false,
}
}
#[cfg(test)]
#[track_caller]
pub fn unwrap_errs(&self) -> &[parser_impl::error::ParseError] {
&self.0.as_ref().unwrap().1.errors
}
/// Treat parsing errors as an Error.
pub fn parse_errs_as_err(self) -> Result<Node<Program>, KclError> {
let (p, errs) = self.0?;
if !errs.errors.is_empty() {
// TODO could summarise all errors rather than just the first one.
return Err(errs.errors.into_iter().next().unwrap().into());
}
match p {
Some(p) => Ok(p),
None => Err(KclError::internal("Unknown parsing error".to_owned())),
}
}
}
impl From<Result<(Option<Node<Program>>, ParseContext), KclError>> for ParseResult {
fn from(r: Result<(Option<Node<Program>>, ParseContext), KclError>) -> ParseResult {
ParseResult(r)
}
}
impl From<(Option<Node<Program>>, ParseContext)> for ParseResult {
fn from(p: (Option<Node<Program>>, ParseContext)) -> ParseResult {
ParseResult(Ok(p))
}
}
impl From<Node<Program>> for ParseResult {
fn from(p: Node<Program>) -> ParseResult {
ParseResult(Ok((Some(p), ParseContext::default())))
}
}
impl From<KclError> for ParseResult {
fn from(e: KclError) -> ParseResult {
ParseResult(Err(e))
}
}

View File

@ -1,4 +1,4 @@
use std::{collections::HashMap, str::FromStr}; use std::{cell::RefCell, collections::HashMap, str::FromStr};
use winnow::{ use winnow::{
combinator::{alt, delimited, opt, peek, preceded, repeat, separated, terminated}, combinator::{alt, delimited, opt, peek, preceded, repeat, separated, terminated},
@ -8,6 +8,7 @@ use winnow::{
token::{any, one_of, take_till}, token::{any, one_of, take_till},
}; };
use self::error::ParseError;
use crate::{ use crate::{
ast::types::{ ast::types::{
ArrayExpression, ArrayRangeExpression, BinaryExpression, BinaryOperator, BinaryPart, BodyItem, BoxNode, ArrayExpression, ArrayRangeExpression, BinaryExpression, BinaryOperator, BinaryPart, BodyItem, BoxNode,
@ -25,14 +26,93 @@ use crate::{
token::{Token, TokenType}, token::{Token, TokenType},
}; };
mod error; pub(crate) mod error;
thread_local! {
/// The current `ParseContext`. `None` if parsing is not currently happening on this thread.
static CTXT: RefCell<Option<ParseContext>> = const { RefCell::new(None) };
}
pub type TokenSlice<'slice, 'input> = &'slice mut &'input [Token];
pub fn run_parser(i: TokenSlice) -> super::ParseResult {
ParseContext::init();
let result = program.parse(i).save_err();
let ctxt = ParseContext::take();
result.map(|o| (o, ctxt)).into()
}
/// Context built up while parsing a program.
///
/// When returned from parsing contains the errors and warnings from the current parse.
#[derive(Debug, Clone, Default)]
pub(crate) struct ParseContext {
pub errors: Vec<ParseError>,
#[allow(dead_code)]
pub warnings: Vec<ParseError>,
}
impl ParseContext {
fn new() -> Self {
ParseContext {
errors: Vec::new(),
warnings: Vec::new(),
}
}
/// Set a new `ParseContext` in thread-local storage. Panics if one already exists.
fn init() {
assert!(CTXT.with_borrow(|ctxt| ctxt.is_none()));
CTXT.with_borrow_mut(|ctxt| *ctxt = Some(ParseContext::new()));
}
/// Take the current `ParseContext` from thread-local storage, leaving `None`. Panics if a `ParseContext`
/// is not present.
fn take() -> ParseContext {
CTXT.with_borrow_mut(|ctxt| ctxt.take()).unwrap()
}
/// Add an error to the current `ParseContext`, panics if there is none.
fn err(e: ParseError) {
CTXT.with_borrow_mut(|ctxt| ctxt.as_mut().unwrap().errors.push(e));
}
/// Add a warning to the current `ParseContext`, panics if there is none.
#[allow(dead_code)]
fn warn(mut e: ParseError) {
e.severity = error::Severity::Warning;
CTXT.with_borrow_mut(|ctxt| ctxt.as_mut().unwrap().warnings.push(e));
}
}
type PResult<O, E = error::ContextError> = winnow::prelude::PResult<O, E>; type PResult<O, E = error::ContextError> = winnow::prelude::PResult<O, E>;
type TokenSlice<'slice, 'input> = &'slice mut &'input [Token]; /// Helper trait for dealing with PResults and the `ParseContext`.
trait PResultEx {
type O;
pub fn run_parser(i: TokenSlice) -> Result<Node<Program>, KclError> { /// If self is Ok, then returns it wrapped in `Ok(Some())`.
program.parse(i).map_err(KclError::from) /// If self is a parsing error, saves it to the current `ParseContext` and returns `Ok(None)`.
/// If self is some other kind of error, then returns it.
fn save_err(self) -> Result<Option<Self::O>, KclError>;
}
impl<O, E: Into<error::ErrorKind>> PResultEx for Result<O, E> {
type O = O;
fn save_err(self) -> Result<Option<O>, KclError> {
match self {
Ok(o) => Ok(Some(o)),
Err(e) => match e.into() {
error::ErrorKind::Parse(e) => {
ParseContext::err(e);
Ok(None)
}
error::ErrorKind::Internal(e) => Err(e),
},
}
}
} }
fn expected(what: &'static str) -> StrContext { fn expected(what: &'static str) -> StrContext {
@ -41,7 +121,7 @@ fn expected(what: &'static str) -> StrContext {
fn program(i: TokenSlice) -> PResult<Node<Program>> { fn program(i: TokenSlice) -> PResult<Node<Program>> {
let shebang = opt(shebang).parse_next(i)?; let shebang = opt(shebang).parse_next(i)?;
let mut out = function_body.parse_next(i)?; let mut out: Node<Program> = function_body.parse_next(i)?;
// Add the shebang to the non-code meta. // Add the shebang to the non-code meta.
if let Some(shebang) = shebang { if let Some(shebang) = shebang {
@ -2069,16 +2149,16 @@ mod tests {
// Try to use it as a variable name. // Try to use it as a variable name.
let code = format!(r#"{} = 0"#, word); let code = format!(r#"{} = 0"#, word);
let result = crate::parser::top_level_parse(code.as_str()); let result = crate::parser::top_level_parse(code.as_str());
let err = result.unwrap_err(); let err = &result.unwrap_errs()[0];
// Which token causes the error may change. In "return = 0", for // Which token causes the error may change. In "return = 0", for
// example, "return" is the problem. // example, "return" is the problem.
assert!( assert!(
err.message().starts_with("Unexpected token: ") err.message.starts_with("Unexpected token: ")
|| err || err
.message() .message
.starts_with("Cannot assign a variable to a reserved keyword: "), .starts_with("Cannot assign a variable to a reserved keyword: "),
"Error message is: {}", "Error message is: {}",
err.message(), err.message,
); );
} }
@ -2108,19 +2188,21 @@ mod tests {
fn weird_program_unclosed_paren() { fn weird_program_unclosed_paren() {
let tokens = crate::token::lexer("fn firstPrime=(", ModuleId::default()).unwrap(); let tokens = crate::token::lexer("fn firstPrime=(", ModuleId::default()).unwrap();
let last = tokens.last().unwrap(); let last = tokens.last().unwrap();
let err: KclError = program.parse(&tokens).unwrap_err().into(); let err: super::error::ErrorKind = program.parse(&tokens).unwrap_err().into();
assert_eq!(err.source_ranges(), last.as_source_ranges()); let err = err.unwrap_parse_error();
assert_eq!(vec![err.source_range], last.as_source_ranges());
// TODO: Better comment. This should explain the compiler expected ) because the user had started declaring the function's parameters. // TODO: Better comment. This should explain the compiler expected ) because the user had started declaring the function's parameters.
// Part of https://github.com/KittyCAD/modeling-app/issues/784 // Part of https://github.com/KittyCAD/modeling-app/issues/784
assert_eq!(err.message(), "Unexpected end of file. The compiler expected )"); assert_eq!(err.message, "Unexpected end of file. The compiler expected )");
} }
#[test] #[test]
fn weird_program_just_a_pipe() { fn weird_program_just_a_pipe() {
let tokens = crate::token::lexer("|", ModuleId::default()).unwrap(); let tokens = crate::token::lexer("|", ModuleId::default()).unwrap();
let err: KclError = program.parse(&tokens).unwrap_err().into(); let err: super::error::ErrorKind = program.parse(&tokens).unwrap_err().into();
assert_eq!(err.source_ranges(), vec![SourceRange([0, 1, 0])]); let err = err.unwrap_parse_error();
assert_eq!(err.message(), "Unexpected token: |"); assert_eq!(vec![err.source_range], vec![SourceRange([0, 1, 0])]);
assert_eq!(err.message, "Unexpected token: |");
} }
#[test] #[test]
@ -3048,15 +3130,29 @@ const mySk1 = startSketchAt([0, 0])"#;
assert!(result.is_ok()); assert!(result.is_ok());
} }
#[track_caller]
fn assert_err(p: &str, msg: &str, src: [usize; 2]) {
let result = crate::parser::top_level_parse(p);
let err = &result.unwrap_errs()[0];
assert_eq!(err.message, msg);
assert_eq!(&err.source_range.0[..2], &src);
}
#[track_caller]
fn assert_err_contains(p: &str, expected: &str) {
let result = crate::parser::top_level_parse(p);
let err = &result.unwrap_errs()[0].message;
assert!(err.contains(expected), "actual='{err}'");
}
#[test] #[test]
fn test_parse_half_pipe_small() { fn test_parse_half_pipe_small() {
let code = "const secondExtrude = startSketchOn('XY') assert_err_contains(
"const secondExtrude = startSketchOn('XY')
|> startProfileAt([0,0], %) |> startProfileAt([0,0], %)
|"; |",
let result = crate::parser::top_level_parse(code); "Unexpected token: |",
assert!(result.is_err()); );
let actual = result.err().unwrap().to_string();
assert!(actual.contains("Unexpected token: |"), "actual={actual:?}");
} }
#[test] #[test]
@ -3130,41 +3226,29 @@ const firstExtrude = startSketchOn('XY')
const secondExtrude = startSketchOn('XY') const secondExtrude = startSketchOn('XY')
|> startProfileAt([0,0], %) |> startProfileAt([0,0], %)
|"; |";
let result = crate::parser::top_level_parse(code); assert_err_contains(code, "Unexpected token: |");
assert!(result.is_err());
assert!(result.err().unwrap().to_string().contains("Unexpected token: |"));
} }
#[test] #[test]
fn test_parse_greater_bang() { fn test_parse_greater_bang() {
let module_id = ModuleId::default(); assert_err(">!", "Unexpected token: >", [0, 1]);
let err = crate::parser::parse_str(">!", module_id).unwrap_err();
assert_eq!(
err.to_string(),
r#"syntax: KclErrorDetails { source_ranges: [SourceRange([0, 1, 0])], message: "Unexpected token: >" }"#
);
} }
#[test] #[test]
fn test_parse_z_percent_parens() { fn test_parse_z_percent_parens() {
let module_id = ModuleId::default(); assert_err("z%)", "Unexpected token: %", [1, 2]);
let err = crate::parser::parse_str("z%)", module_id).unwrap_err();
assert_eq!(
err.to_string(),
r#"syntax: KclErrorDetails { source_ranges: [SourceRange([1, 2, 0])], message: "Unexpected token: %" }"#
);
} }
#[test] #[test]
fn test_parse_parens_unicode() { fn test_parse_parens_unicode() {
let module_id = ModuleId::default(); let result = crate::parser::top_level_parse("");
let err = crate::parser::parse_str("", module_id).unwrap_err(); let KclError::Lexical(details) = result.0.unwrap_err() else {
panic!();
};
// TODO: Better errors when program cannot tokenize. // TODO: Better errors when program cannot tokenize.
// https://github.com/KittyCAD/modeling-app/issues/696 // https://github.com/KittyCAD/modeling-app/issues/696
assert_eq!( assert_eq!(details.message, "found unknown token 'ޜ'");
err.to_string(), assert_eq!(&details.source_ranges[0].0[..2], &[1, 2]);
r#"lexical: KclErrorDetails { source_ranges: [SourceRange([1, 2, 0])], message: "found unknown token 'ޜ'" }"#
);
} }
#[test] #[test]
@ -3183,61 +3267,46 @@ const bracket = [-leg2 + thickness, 0]
r#" r#"
z(-[["#, z(-[["#,
) )
.unwrap_err(); .unwrap_errs();
} }
#[test] #[test]
fn test_parse_weird_new_line_function() { fn test_parse_weird_new_line_function() {
let err = crate::parser::top_level_parse( assert_err(
r#"z r#"z
(--#"#, (--#"#,
) "Unexpected token: (",
.unwrap_err(); [2, 3],
assert_eq!(
err.to_string(),
r#"syntax: KclErrorDetails { source_ranges: [SourceRange([3, 4, 0])], message: "Unexpected token: (" }"#
); );
} }
#[test] #[test]
fn test_parse_weird_lots_of_fancy_brackets() { fn test_parse_weird_lots_of_fancy_brackets() {
let err = crate::parser::top_level_parse(r#"zz({{{{{{{{)iegAng{{{{{{{##"#).unwrap_err(); assert_err(r#"zz({{{{{{{{)iegAng{{{{{{{##"#, "Unexpected token: (", [2, 3]);
assert_eq!(
err.to_string(),
r#"syntax: KclErrorDetails { source_ranges: [SourceRange([2, 3, 0])], message: "Unexpected token: (" }"#
);
} }
#[test] #[test]
fn test_parse_weird_close_before_open() { fn test_parse_weird_close_before_open() {
let err = crate::parser::top_level_parse( assert_err_contains(
r#"fn)n r#"fn)n
e e
["#, ["#,
) "expected whitespace, found ')' which is brace",
.unwrap_err(); );
assert!(err
.to_string()
.contains("expected whitespace, found ')' which is brace"));
} }
#[test] #[test]
fn test_parse_weird_close_before_nada() { fn test_parse_weird_close_before_nada() {
let err = crate::parser::top_level_parse(r#"fn)n-"#).unwrap_err(); assert_err_contains(r#"fn)n-"#, "expected whitespace, found ')' which is brace");
assert!(err
.to_string()
.contains("expected whitespace, found ')' which is brace"));
} }
#[test] #[test]
fn test_parse_weird_lots_of_slashes() { fn test_parse_weird_lots_of_slashes() {
let err = crate::parser::top_level_parse( assert_err_contains(
r#"J///////////o//+///////////P++++*++++++P///////˟ r#"J///////////o//+///////////P++++*++++++P///////˟
++4"#, ++4"#,
) "Unexpected token: +",
.unwrap_err(); );
let actual = err.to_string();
assert!(actual.contains("Unexpected token: +"), "actual={actual:?}");
} }
#[test] #[test]
@ -3324,62 +3393,53 @@ e
#[test] #[test]
fn test_error_keyword_in_variable() { fn test_error_keyword_in_variable() {
let err = crate::parser::top_level_parse(r#"const let = "thing""#).unwrap_err(); assert_err(
assert_eq!( r#"const let = "thing""#,
err.to_string(), "Cannot assign a variable to a reserved keyword: let",
r#"syntax: KclErrorDetails { source_ranges: [SourceRange([6, 9, 0])], message: "Cannot assign a variable to a reserved keyword: let" }"# [6, 9],
); );
} }
#[test] #[test]
fn test_error_keyword_in_fn_name() { fn test_error_keyword_in_fn_name() {
let err = crate::parser::top_level_parse(r#"fn let = () {}"#).unwrap_err(); assert_err(
assert_eq!( r#"fn let = () {}"#,
err.to_string(), "Cannot assign a variable to a reserved keyword: let",
r#"syntax: KclErrorDetails { source_ranges: [SourceRange([3, 6, 0])], message: "Cannot assign a variable to a reserved keyword: let" }"# [3, 6],
); );
} }
#[test] #[test]
fn test_error_stdlib_in_fn_name() { fn test_error_stdlib_in_fn_name() {
let err = crate::parser::top_level_parse( assert_err(
r#"fn cos = () => { r#"fn cos = () => {
return 1 return 1
}"#, }"#,
) "Cannot assign a variable to a reserved keyword: cos",
.unwrap_err(); [3, 6],
assert_eq!(
err.to_string(),
r#"syntax: KclErrorDetails { source_ranges: [SourceRange([3, 6, 0])], message: "Cannot assign a variable to a reserved keyword: cos" }"#
); );
} }
#[test] #[test]
fn test_error_keyword_in_fn_args() { fn test_error_keyword_in_fn_args() {
let err = crate::parser::top_level_parse( assert_err(
r#"fn thing = (let) => { r#"fn thing = (let) => {
return 1 return 1
}"#, }"#,
"Cannot assign a variable to a reserved keyword: let",
[12, 15],
) )
.unwrap_err();
assert_eq!(
err.to_string(),
r#"syntax: KclErrorDetails { source_ranges: [SourceRange([12, 15, 0])], message: "Cannot assign a variable to a reserved keyword: let" }"#
);
} }
#[test] #[test]
fn test_error_stdlib_in_fn_args() { fn test_error_stdlib_in_fn_args() {
let err = crate::parser::top_level_parse( assert_err(
r#"fn thing = (cos) => { r#"fn thing = (cos) => {
return 1 return 1
}"#, }"#,
"Cannot assign a variable to a reserved keyword: cos",
[12, 15],
) )
.unwrap_err();
assert_eq!(
err.to_string(),
r#"syntax: KclErrorDetails { source_ranges: [SourceRange([12, 15, 0])], message: "Cannot assign a variable to a reserved keyword: cos" }"#
);
} }
#[test] #[test]
@ -3491,28 +3551,20 @@ thing(false)
"#, "#,
name name
); );
let err = crate::parser::top_level_parse(&some_program_string).unwrap_err(); assert_err(
assert_eq!( &some_program_string,
err.to_string(), "Expected a `fn` variable kind, found: `const`",
format!( [0, name.len()],
r#"syntax: KclErrorDetails {{ source_ranges: [SourceRange([0, {}, 0])], message: "Expected a `fn` variable kind, found: `const`" }}"#,
name.len(),
)
); );
} }
} }
#[test] #[test]
fn test_error_define_var_as_function() { fn test_error_define_var_as_function() {
let some_program_string = r#"fn thing = "thing""#;
let err = crate::parser::top_level_parse(some_program_string).unwrap_err();
// TODO: https://github.com/KittyCAD/modeling-app/issues/784 // TODO: https://github.com/KittyCAD/modeling-app/issues/784
// Improve this error message. // Improve this error message.
// It should say that the compiler is expecting a function expression on the RHS. // It should say that the compiler is expecting a function expression on the RHS.
assert_eq!( assert_err(r#"fn thing = "thing""#, "Unexpected token: \"thing\"", [11, 18]);
err.to_string(),
r#"syntax: KclErrorDetails { source_ranges: [SourceRange([11, 18, 0])], message: "Unexpected token: \"thing\"" }"#
);
} }
#[test] #[test]
@ -3525,7 +3577,7 @@ thing(false)
|> line([-5.09, 12.33], %) |> line([-5.09, 12.33], %)
asdasd asdasd
"#; "#;
crate::parser::top_level_parse(test_program).unwrap_err(); crate::parser::top_level_parse(test_program).unwrap_errs();
} }
#[test] #[test]
@ -3573,18 +3625,41 @@ let myBox = box([0,0], -3, -16, -10)
"#; "#;
crate::parser::top_level_parse(some_program_string).unwrap(); crate::parser::top_level_parse(some_program_string).unwrap();
} }
#[test] #[test]
fn must_use_percent_in_pipeline_fn() { fn must_use_percent_in_pipeline_fn() {
let some_program_string = r#" let some_program_string = r#"
foo() foo()
|> bar(2) |> bar(2)
"#; "#;
let err = crate::parser::top_level_parse(some_program_string).unwrap_err(); assert_err(
assert_eq!( some_program_string,
err.to_string(), "All expressions in a pipeline must use the % (substitution operator)",
r#"syntax: KclErrorDetails { source_ranges: [SourceRange([30, 36, 0])], message: "All expressions in a pipeline must use the % (substitution operator)" }"# [30, 36],
); );
} }
#[test]
fn test_parse_tag_named_std_lib() {
let some_program_string = r#"startSketchOn('XY')
|> startProfileAt([0, 0], %)
|> line([5, 5], %, $xLine)
"#;
assert_err(
some_program_string,
"Cannot assign a tag to a reserved keyword: xLine",
[76, 82],
);
}
#[test]
fn test_parse_empty_tag() {
let some_program_string = r#"startSketchOn('XY')
|> startProfileAt([0, 0], %)
|> line([5, 5], %, $)
"#;
assert_err(some_program_string, "Unexpected token: |>", [57, 59]);
}
} }
#[cfg(test)] #[cfg(test)]

View File

@ -1,12 +1,9 @@
use winnow::{ use winnow::{error::StrContext, stream::Stream};
error::{ErrorKind, ParseError, StrContext},
stream::Stream,
};
use crate::{ use crate::{
errors::{KclError, KclErrorDetails}, errors::{KclError, KclErrorDetails},
executor::SourceRange, executor::SourceRange,
token::{Input, Token}, token::Token,
}; };
/// Accumulate context while backtracking errors /// Accumulate context while backtracking errors
@ -19,69 +16,115 @@ pub struct ContextError<C = StrContext> {
pub cause: Option<KclError>, pub cause: Option<KclError>,
} }
impl From<ParseError<Input<'_>, winnow::error::ContextError>> for KclError { /// An error which occurred during parsing.
fn from(err: ParseError<Input<'_>, winnow::error::ContextError>) -> Self { ///
let (input, offset): (Vec<char>, usize) = (err.input().chars().collect(), err.offset()); /// In contrast to Winnow errors which may not be an actual error but just an attempted parse which
let module_id = err.input().state.module_id; /// didn't work out, these are errors which are always a result of incorrect user code and which should
/// be presented to the user.
#[derive(Debug, Clone)]
pub(crate) struct ParseError {
pub source_range: SourceRange,
pub message: String,
#[allow(dead_code)]
pub suggestion: String,
pub severity: Severity,
}
if offset >= input.len() { impl ParseError {
// From the winnow docs: pub(super) fn err(source_range: SourceRange, message: impl ToString) -> ParseError {
// ParseError {
// This is an offset, not an index, and may point to source_range,
// the end of input (input.len()) on eof errors. message: message.to_string(),
suggestion: String::new(),
return KclError::Lexical(KclErrorDetails { severity: Severity::Error,
source_ranges: vec![SourceRange([offset, offset, module_id.as_usize()])],
message: "unexpected EOF while parsing".to_string(),
});
} }
}
// TODO: Add the Winnow tokenizer context to the error. #[allow(dead_code)]
// See https://github.com/KittyCAD/modeling-app/issues/784 pub(super) fn with_suggestion(
let bad_token = &input[offset]; source_range: SourceRange,
// TODO: Add the Winnow parser context to the error. message: impl ToString,
// See https://github.com/KittyCAD/modeling-app/issues/784 suggestion: impl ToString,
KclError::Lexical(KclErrorDetails { ) -> ParseError {
source_ranges: vec![SourceRange([offset, offset + 1, module_id.as_usize()])], ParseError {
message: format!("found unknown token '{}'", bad_token), source_range,
message: message.to_string(),
suggestion: suggestion.to_string(),
severity: Severity::Error,
}
}
}
impl From<ParseError> for KclError {
fn from(err: ParseError) -> Self {
KclError::Syntax(KclErrorDetails {
source_ranges: vec![err.source_range],
message: err.message,
}) })
} }
} }
impl From<ParseError<&[Token], ContextError>> for KclError { #[derive(Debug, Clone)]
fn from(err: ParseError<&[Token], ContextError>) -> Self { pub(crate) enum Severity {
#[allow(dead_code)]
Warning,
Error,
}
/// Helper enum for the below conversion of Winnow errors into either a parse error or an unexpected
/// error.
pub(super) enum ErrorKind {
Parse(ParseError),
Internal(KclError),
}
impl ErrorKind {
#[cfg(test)]
pub fn unwrap_parse_error(self) -> ParseError {
match self {
ErrorKind::Parse(parse_error) => parse_error,
ErrorKind::Internal(_) => panic!(),
}
}
}
impl From<winnow::error::ParseError<&[Token], ContextError>> for ErrorKind {
fn from(err: winnow::error::ParseError<&[Token], ContextError>) -> Self {
let Some(last_token) = err.input().last() else { let Some(last_token) = err.input().last() else {
return KclError::Syntax(KclErrorDetails { return ErrorKind::Parse(ParseError::err(Default::default(), "file is empty"));
source_ranges: Default::default(),
message: "file is empty".to_owned(),
});
}; };
let (input, offset, err) = (err.input().to_vec(), err.offset(), err.into_inner()); let (input, offset, err) = (err.input().to_vec(), err.offset(), err.into_inner());
if let Some(e) = err.cause { if let Some(e) = err.cause {
return e; return match e {
KclError::Syntax(details) => ErrorKind::Parse(ParseError::err(
details.source_ranges.into_iter().next().unwrap(),
details.message,
)),
e => ErrorKind::Internal(e),
};
} }
// See docs on `offset`. // See docs on `offset`.
if offset >= input.len() { if offset >= input.len() {
let context = err.context.first(); let context = err.context.first();
return KclError::Syntax(KclErrorDetails { return ErrorKind::Parse(ParseError::err(
source_ranges: last_token.as_source_ranges(), last_token.as_source_range(),
message: match context { match context {
Some(what) => format!("Unexpected end of file. The compiler {what}"), Some(what) => format!("Unexpected end of file. The compiler {what}"),
None => "Unexpected end of file while still parsing".to_owned(), None => "Unexpected end of file while still parsing".to_owned(),
}, },
}); ));
} }
let bad_token = &input[offset]; let bad_token = &input[offset];
// TODO: Add the Winnow parser context to the error. // TODO: Add the Winnow parser context to the error.
// See https://github.com/KittyCAD/modeling-app/issues/784 // See https://github.com/KittyCAD/modeling-app/issues/784
KclError::Syntax(KclErrorDetails { ErrorKind::Parse(ParseError::err(
source_ranges: bad_token.as_source_ranges(), bad_token.as_source_range(),
message: format!("Unexpected token: {}", bad_token.value), format!("Unexpected token: {}", bad_token.value),
}) ))
} }
} }
@ -108,12 +151,17 @@ where
I: Stream, I: Stream,
{ {
#[inline] #[inline]
fn from_error_kind(_input: &I, _kind: ErrorKind) -> Self { fn from_error_kind(_input: &I, _kind: winnow::error::ErrorKind) -> Self {
Self::default() Self::default()
} }
#[inline] #[inline]
fn append(self, _input: &I, _input_checkpoint: &<I as Stream>::Checkpoint, _kind: ErrorKind) -> Self { fn append(
self,
_input: &I,
_input_checkpoint: &<I as Stream>::Checkpoint,
_kind: winnow::error::ErrorKind,
) -> Self {
self self
} }
@ -136,7 +184,7 @@ where
impl<C, I> winnow::error::FromExternalError<I, KclError> for ContextError<C> { impl<C, I> winnow::error::FromExternalError<I, KclError> for ContextError<C> {
#[inline] #[inline]
fn from_external_error(_input: &I, _kind: ErrorKind, e: KclError) -> Self { fn from_external_error(_input: &I, _kind: winnow::error::ErrorKind, e: KclError) -> Self {
let mut err = Self::default(); let mut err = Self::default();
{ {
err.cause = Some(e); err.cause = Some(e);

View File

@ -60,7 +60,7 @@ fn parse(test_name: &str) {
}; };
// Parse the tokens into an AST. // Parse the tokens into an AST.
let parse_res = crate::parser::parse_tokens(tokens); let parse_res = Result::<_, KclError>::Ok(crate::parser::parse_tokens(tokens).unwrap());
assert_snapshot(test_name, "Result of parsing", || { assert_snapshot(test_name, "Result of parsing", || {
insta::assert_json_snapshot!("ast", parse_res); insta::assert_json_snapshot!("ast", parse_res);
}); });

View File

@ -5,7 +5,7 @@ use parse_display::{Display, FromStr};
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tower_lsp::lsp_types::SemanticTokenType; use tower_lsp::lsp_types::SemanticTokenType;
use winnow::stream::ContainsToken; use winnow::{error::ParseError, stream::ContainsToken};
use crate::{ use crate::{
ast::types::{ItemVisibility, ModuleId, VariableKind}, ast::types::{ItemVisibility, ModuleId, VariableKind},
@ -251,7 +251,36 @@ impl From<&Token> for SourceRange {
} }
pub fn lexer(s: &str, module_id: ModuleId) -> Result<Vec<Token>, KclError> { pub fn lexer(s: &str, module_id: ModuleId) -> Result<Vec<Token>, KclError> {
tokeniser::lexer(s, module_id).map_err(From::from) tokeniser::lex(s, module_id).map_err(From::from)
}
impl From<ParseError<Input<'_>, winnow::error::ContextError>> for KclError {
fn from(err: ParseError<Input<'_>, winnow::error::ContextError>) -> Self {
let (input, offset): (Vec<char>, usize) = (err.input().chars().collect(), err.offset());
let module_id = err.input().state.module_id;
if offset >= input.len() {
// From the winnow docs:
//
// This is an offset, not an index, and may point to
// the end of input (input.len()) on eof errors.
return KclError::Lexical(crate::errors::KclErrorDetails {
source_ranges: vec![SourceRange([offset, offset, module_id.as_usize()])],
message: "unexpected EOF while parsing".to_string(),
});
}
// TODO: Add the Winnow tokenizer context to the error.
// See https://github.com/KittyCAD/modeling-app/issues/784
let bad_token = &input[offset];
// TODO: Add the Winnow parser context to the error.
// See https://github.com/KittyCAD/modeling-app/issues/784
KclError::Lexical(crate::errors::KclErrorDetails {
source_ranges: vec![SourceRange([offset, offset + 1, module_id.as_usize()])],
message: format!("found unknown token '{}'", bad_token),
})
}
} }
#[cfg(test)] #[cfg(test)]

View File

@ -62,7 +62,7 @@ lazy_static! {
}; };
} }
pub fn lexer(i: &str, module_id: ModuleId) -> Result<Vec<Token>, ParseError<Input<'_>, ContextError>> { pub fn lex(i: &str, module_id: ModuleId) -> Result<Vec<Token>, ParseError<Input<'_>, ContextError>> {
let state = State::new(module_id); let state = State::new(module_id);
let input = Input { let input = Input {
input: Located::new(i), input: Located::new(i),
@ -469,7 +469,7 @@ mod tests {
fn test_program0() { fn test_program0() {
let program = "const a=5"; let program = "const a=5";
let module_id = ModuleId::from_usize(1); let module_id = ModuleId::from_usize(1);
let actual = lexer(program, module_id).unwrap(); let actual = lex(program, module_id).unwrap();
let expected = vec![ let expected = vec![
Token { Token {
token_type: TokenType::Keyword, token_type: TokenType::Keyword,
@ -514,7 +514,7 @@ mod tests {
fn test_program1() { fn test_program1() {
let program = "54 + 22500 + 6"; let program = "54 + 22500 + 6";
let module_id = ModuleId::from_usize(1); let module_id = ModuleId::from_usize(1);
let actual = lexer(program, module_id).unwrap(); let actual = lex(program, module_id).unwrap();
let expected = vec![ let expected = vec![
Token { Token {
token_type: TokenType::Number, token_type: TokenType::Number,
@ -1388,7 +1388,7 @@ show(part001)"#;
value: ")".to_owned(), value: ")".to_owned(),
}, },
]; ];
let actual = lexer(program, module_id).unwrap(); let actual = lex(program, module_id).unwrap();
assert_tokens(expected, actual); assert_tokens(expected, actual);
} }
@ -1403,7 +1403,7 @@ const things = "things"
// this is also a comment"#; // this is also a comment"#;
let module_id = ModuleId::from_usize(1); let module_id = ModuleId::from_usize(1);
let actual = lexer(program, module_id).unwrap(); let actual = lex(program, module_id).unwrap();
use TokenType::*; use TokenType::*;
let expected = vec![ let expected = vec![
Token { Token {
@ -1837,26 +1837,26 @@ const things = "things"
value: "]".to_owned(), value: "]".to_owned(),
}, },
]; ];
let actual = lexer(program, module_id).unwrap(); let actual = lex(program, module_id).unwrap();
assert_tokens(expected, actual); assert_tokens(expected, actual);
} }
#[test] #[test]
fn test_kitt() { fn test_kitt() {
let program = include_str!("../../../tests/executor/inputs/kittycad_svg.kcl"); let program = include_str!("../../../tests/executor/inputs/kittycad_svg.kcl");
let actual = lexer(program, ModuleId::default()).unwrap(); let actual = lex(program, ModuleId::default()).unwrap();
assert_eq!(actual.len(), 5103); assert_eq!(actual.len(), 5103);
} }
#[test] #[test]
fn test_pipes_on_pipes() { fn test_pipes_on_pipes() {
let program = include_str!("../../../tests/executor/inputs/pipes_on_pipes.kcl"); let program = include_str!("../../../tests/executor/inputs/pipes_on_pipes.kcl");
let actual = lexer(program, ModuleId::default()).unwrap(); let actual = lex(program, ModuleId::default()).unwrap();
assert_eq!(actual.len(), 17841); assert_eq!(actual.len(), 17841);
} }
#[test] #[test]
fn test_lexer_negative_word() { fn test_lexer_negative_word() {
let module_id = ModuleId::from_usize(1); let module_id = ModuleId::from_usize(1);
let actual = lexer("-legX", module_id).unwrap(); let actual = lex("-legX", module_id).unwrap();
let expected = vec![ let expected = vec![
Token { Token {
token_type: TokenType::Operator, token_type: TokenType::Operator,
@ -1879,7 +1879,7 @@ const things = "things"
#[test] #[test]
fn not_eq() { fn not_eq() {
let module_id = ModuleId::from_usize(1); let module_id = ModuleId::from_usize(1);
let actual = lexer("!=", module_id).unwrap(); let actual = lex("!=", module_id).unwrap();
let expected = vec![Token { let expected = vec![Token {
token_type: TokenType::Operator, token_type: TokenType::Operator,
value: "!=".to_owned(), value: "!=".to_owned(),
@ -1893,7 +1893,7 @@ const things = "things"
#[test] #[test]
fn test_unrecognized_token() { fn test_unrecognized_token() {
let module_id = ModuleId::from_usize(1); let module_id = ModuleId::from_usize(1);
let actual = lexer("12 ; 8", module_id).unwrap(); let actual = lex("12 ; 8", module_id).unwrap();
let expected = vec![ let expected = vec![
Token { Token {
token_type: TokenType::Number, token_type: TokenType::Number,
@ -1938,7 +1938,7 @@ const things = "things"
#[test] #[test]
fn import_keyword() { fn import_keyword() {
let module_id = ModuleId::from_usize(1); let module_id = ModuleId::from_usize(1);
let actual = lexer("import foo", module_id).unwrap(); let actual = lex("import foo", module_id).unwrap();
let expected = Token { let expected = Token {
token_type: TokenType::Keyword, token_type: TokenType::Keyword,
value: "import".to_owned(), value: "import".to_owned(),
@ -1952,7 +1952,7 @@ const things = "things"
#[test] #[test]
fn import_function() { fn import_function() {
let module_id = ModuleId::from_usize(1); let module_id = ModuleId::from_usize(1);
let actual = lexer("import(3)", module_id).unwrap(); let actual = lex("import(3)", module_id).unwrap();
let expected = Token { let expected = Token {
token_type: TokenType::Word, token_type: TokenType::Word,
value: "import".to_owned(), value: "import".to_owned(),

View File

@ -999,19 +999,6 @@ myNestedVar = [
assert_eq!(recasted, r#""#); assert_eq!(recasted, r#""#);
} }
#[test]
fn test_recast_shebang_only() {
let some_program_string = r#"#!/usr/local/env zoo kcl"#;
let result = crate::parser::top_level_parse(some_program_string);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().to_string(),
r#"syntax: KclErrorDetails { source_ranges: [SourceRange([21, 24, 0])], message: "Unexpected end of file. The compiler expected a function body items (functions are made up of variable declarations, expressions, and return statements, each of those is a possible body item" }"#
);
}
#[test] #[test]
fn test_recast_shebang() { fn test_recast_shebang() {
let some_program_string = r#"#!/usr/local/env zoo kcl let some_program_string = r#"#!/usr/local/env zoo kcl