From 6de9f39bc1a6183a20c9fc773957cdc5a9acb2c9 Mon Sep 17 00:00:00 2001 From: Cam Pedersen Date: Sat, 7 Nov 2026 18:53:06 -0500 Subject: [PATCH 1/3] fix(lexer): scientific notation without a decimal point (1e6, 2E-3) 1e6 lexed as Int("1e") + Int("6"), and the "e" suffix desugared to [unit 1 :e] -> runtime error "unknown unit: e". Add a Float rule for digits+exponent that wins by longest match; unit suffixes still work. Co-Authored-By: Claude Opus 5.5 --- crates/loon-lang/src/parser/mod.rs | 30 +++++++++++++++++++++++++++ crates/loon-lang/src/syntax/tokens.rs | 4 ++++ 2 files changed, 34 insertions(+) diff --git a/crates/loon-lang/src/parser/mod.rs b/crates/loon-lang/src/parser/mod.rs index 3370e4d..5a95649 100644 --- a/crates/loon-lang/src/parser/mod.rs +++ b/crates/loon-lang/src/parser/mod.rs @@ -523,6 +523,36 @@ mod tests { assert_eq!(exprs.len(), 1); } + #[test] + fn parse_scientific_notation_without_decimal_point() { + for (src, want) in [ + ("1e6", 1e6), + ("2.5e-3", 2.5e-3), + ("1E+3", 1e3), + ("-4e2", -4e2), + ("3e-0", 3.0), + ] { + let exprs = parse(src).unwrap(); + assert_eq!(exprs.len(), 1, "{src}"); + match &exprs[0].kind { + ExprKind::Float(f) => assert_eq!(*f, want, "{src}"), + other => panic!("{src} parsed as {other:?}"), + } + } + // Inside a call: one argument each, not `1e` + `6`. + let exprs = parse("[cube 1e6 1e6 1e6]").unwrap(); + assert_eq!(exprs[0].to_string(), "[cube 1000000.0 1000000.0 1000000.0]"); + } + + #[test] + fn unit_suffixes_still_desugar() { + assert_eq!(parse("10mm").unwrap()[0].to_string(), "[unit 10 :mm]"); + assert_eq!(parse("2.5in").unwrap()[0].to_string(), "[unit 2.5 :in]"); + assert_eq!(parse("90deg").unwrap()[0].to_string(), "[unit 90 :deg]"); + // An exponent followed by a unit keeps both. + assert_eq!(parse("1e3mm").unwrap()[0].to_string(), "[unit 1000.0 :mm]"); + } + #[test] fn parse_hello_world() { let src = r#"[fn main [] [println "hello, world!"]]"#; diff --git a/crates/loon-lang/src/syntax/tokens.rs b/crates/loon-lang/src/syntax/tokens.rs index 0f5b2fc..abbab5e 100644 --- a/crates/loon-lang/src/syntax/tokens.rs +++ b/crates/loon-lang/src/syntax/tokens.rs @@ -129,6 +129,10 @@ pub enum Token { // Literals — higher priority than Symbol #[regex(r"-?[0-9]+\.[0-9]+([eE][+-]?[0-9]+)?(f32|f64|[a-zA-Z]+)?", priority = 10, callback = |lex| lex.slice().to_string())] + // Scientific notation without a decimal point (1e6, 2E-3, 1e+3mm). Longest + // match beats the Int rule's unit-suffix group, which would otherwise read + // `1e6` as `1e` (unit `:e`) followed by `6`. + #[regex(r"-?[0-9]+[eE][+-]?[0-9]+(f32|f64|[a-zA-Z]+)?", priority = 11, callback = |lex| lex.slice().to_string())] Float(String), // Radix literals (issue #24): hex 0xFF, octal 0o17, binary 0b1010. // Higher priority than the decimal rule, which would otherwise split From fd7cc652cb4d4c80fff2e4a11075f9ffb8a7dabe Mon Sep 17 00:00:00 2001 From: Cam Pedersen Date: Sat, 7 Nov 2026 18:53:28 -0500 Subject: [PATCH 2/3] test: fix expected display strings Co-Authored-By: Claude Opus 5.5 --- crates/loon-lang/src/parser/mod.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/crates/loon-lang/src/parser/mod.rs b/crates/loon-lang/src/parser/mod.rs index 5a95649..4947959 100644 --- a/crates/loon-lang/src/parser/mod.rs +++ b/crates/loon-lang/src/parser/mod.rs @@ -541,7 +541,14 @@ mod tests { } // Inside a call: one argument each, not `1e` + `6`. let exprs = parse("[cube 1e6 1e6 1e6]").unwrap(); - assert_eq!(exprs[0].to_string(), "[cube 1000000.0 1000000.0 1000000.0]"); + match &exprs[0].kind { + ExprKind::List(items) => { + assert_eq!(items.len(), 4); + assert!(items[1..].iter().all(|e| matches!(e.kind, ExprKind::Float(f) if f == 1e6))); + } + other => panic!("{other:?}"), + } + assert_eq!(exprs[0].to_string(), "[cube 1000000 1000000 1000000]"); } #[test] @@ -550,7 +557,7 @@ mod tests { assert_eq!(parse("2.5in").unwrap()[0].to_string(), "[unit 2.5 :in]"); assert_eq!(parse("90deg").unwrap()[0].to_string(), "[unit 90 :deg]"); // An exponent followed by a unit keeps both. - assert_eq!(parse("1e3mm").unwrap()[0].to_string(), "[unit 1000.0 :mm]"); + assert_eq!(parse("1e3mm").unwrap()[0].to_string(), "[unit 1000 :mm]"); } #[test] From f7117d0b5a2fa8b0bd8cf51fea0e3243cd87b621 Mon Sep 17 00:00:00 2001 From: Cam Pedersen Date: Sat, 7 Nov 2026 18:54:05 -0500 Subject: [PATCH 3/3] style: cargo fmt Co-Authored-By: Claude Opus 5.5 --- crates/loon-lang/src/parser/mod.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/loon-lang/src/parser/mod.rs b/crates/loon-lang/src/parser/mod.rs index 4947959..19acf24 100644 --- a/crates/loon-lang/src/parser/mod.rs +++ b/crates/loon-lang/src/parser/mod.rs @@ -544,7 +544,9 @@ mod tests { match &exprs[0].kind { ExprKind::List(items) => { assert_eq!(items.len(), 4); - assert!(items[1..].iter().all(|e| matches!(e.kind, ExprKind::Float(f) if f == 1e6))); + assert!(items[1..] + .iter() + .all(|e| matches!(e.kind, ExprKind::Float(f) if f == 1e6))); } other => panic!("{other:?}"), }