From 29a80854a4bd117b92897fe5b306964a8a71ccb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=83=AA=E3=82=AD=E8=90=8C?= Date: Tue, 2 Sep 2025 18:44:38 +0200 Subject: [PATCH] fix parsing prefix operators in calls --- crates/haku-wasm/src/lib.rs | 31 +++++++-------- crates/haku/src/parser.rs | 75 ++++++++++++++++++++++++------------- crates/haku/src/token.rs | 4 ++ static/brush-box.js | 2 +- 4 files changed, 69 insertions(+), 43 deletions(-) diff --git a/crates/haku-wasm/src/lib.rs b/crates/haku-wasm/src/lib.rs index 8459dc6..85c0635 100644 --- a/crates/haku-wasm/src/lib.rs +++ b/crates/haku-wasm/src/lib.rs @@ -401,6 +401,7 @@ unsafe extern "C" fn haku_compile_brush( "compiling: parsed successfully into {} AST nodes", ast.len() ); + // debug!("ast: {}", ast::dump::dump(&ast, root, Some(code))); let src = Source { code, ast: &ast }; @@ -431,21 +432,21 @@ unsafe extern "C" fn haku_compile_brush( ); debug!("compiling: {closure_spec:?}"); - debug!("bytecode: {:?}", chunk.bytecode); - { - let mut cursor = 0_usize; - for info in &chunk.span_info { - let slice = &chunk.bytecode[cursor..cursor + info.len as usize]; - debug!( - "{:?} | 0x{:x} {:?} | {:?}", - info.span, - cursor, - slice, - info.span.slice(src.code), - ); - cursor += info.len as usize; - } - } + // debug!("bytecode: {:?}", chunk.bytecode); + // { + // let mut cursor = 0_usize; + // for info in &chunk.span_info { + // let slice = &chunk.bytecode[cursor..cursor + info.len as usize]; + // debug!( + // "{:?} | 0x{:x} {:?} | {:?}", + // info.span, + // cursor, + // slice, + // info.span.slice(src.code), + // ); + // cursor += info.len as usize; + // } + // } instance.compile_result2 = Some(CompileResult { defs_string: instance.defs.serialize_defs(), diff --git a/crates/haku/src/parser.rs b/crates/haku/src/parser.rs index a8e0cee..5d02a5a 100644 --- a/crates/haku/src/parser.rs +++ b/crates/haku/src/parser.rs @@ -311,31 +311,13 @@ enum Tighter { fn tighter(left: (TokenKind, Spaces), right: (TokenKind, Spaces)) -> Tighter { #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] - enum Spacing { - Loose, + enum Tightness { + Loose(usize), Call, - Tight, + Tight(usize), } - fn tightness((kind, spaces): (TokenKind, Spaces)) -> Option<(Spacing, usize)> { - let spacing = match kind { - // There are a few types of operators which are independent of tightness. - - // For : and =, it does not matter if they're spelled one way or the other, because - // there is only one way to use them (at the beginning of the expression). - TokenKind::Colon | TokenKind::Equal => Spacing::Loose, - - // For calls, there is a special intermediate level, such that they can sit between - // loose operators and tight operators. - _ if PREFIX_TOKENS.contains(kind) => Spacing::Call, - - // For everything else, the usual rules apply. - _ => match spaces.pair() { - (false, false) => Spacing::Tight, - (true, true) => Spacing::Loose, - _ => return None, // not a valid infix operator - }, - }; + fn tightness((kind, spaces): (TokenKind, Spaces)) -> Option { let index = match kind { TokenKind::Equal | TokenKind::Colon => 0, // 1: reserved for `and` and `or` @@ -350,7 +332,28 @@ fn tighter(left: (TokenKind, Spaces), right: (TokenKind, Spaces)) -> Tighter { _ if PREFIX_TOKENS.contains(kind) => 5, _ => return None, // not an infix operator }; - Some((spacing, index)) + Some(match kind { + // There are a few types of operators which are independent of tightness. + + // For : and =, it does not matter if they're spelled one way or the other, because + // there is only one way to use them (at the beginning of the expression). + TokenKind::Colon | TokenKind::Equal => Tightness::Loose(index), + + // For unary -, we treat it as having Tight spacing rather than Call, else it would + // be allowed to begin function calls. + TokenKind::Minus if spaces.pair() == (true, false) => Tightness::Tight(index), + + // For calls, there is a special intermediate level, such that they can sit between + // loose operators and tight operators. + _ if PREFIX_TOKENS.contains(kind) => Tightness::Call, + + // For everything else, the usual rules apply. + _ => match spaces.pair() { + (false, false) => Tightness::Tight(index), + (true, true) => Tightness::Loose(index), + _ => return None, // not a valid infix operator + }, + }) } let Some(right_tightness) = tightness(right) else { @@ -361,6 +364,12 @@ fn tighter(left: (TokenKind, Spaces), right: (TokenKind, Spaces)) -> Tighter { return Tighter::Right; }; + // When we're inside a call, subsequent arguments must not be slurped into the current + // expression, as it would result in calls being parsed as (vec (1 (-1))), which is not correct. + if left_tightness == Tightness::Call { + return Tighter::Left; + } + if right_tightness > left_tightness { Tighter::Right } else { @@ -585,18 +594,29 @@ const PREFIX_TOKENS: TokenKindSet = TokenKindSet::new(&[ ]); fn prefix(p: &mut Parser) -> Closed { - match p.peek() { + let (kind, spaces) = p.peek_with_spaces(); + match kind { TokenKind::Ident => one(p, NodeKind::Ident), TokenKind::Tag => one(p, NodeKind::Tag), TokenKind::Number => one(p, NodeKind::Number), TokenKind::Color => one(p, NodeKind::Color), TokenKind::LBrack => list(p), - TokenKind::Minus | TokenKind::Not => unary(p), + TokenKind::Minus if !spaces.right() => unary(p), + TokenKind::Not => unary(p), TokenKind::LParen => paren(p), TokenKind::Backslash => lambda(p), TokenKind::If => if_expr(p), + TokenKind::Minus if spaces.pair() == (false, true) => { + let span = p.span(); + p.emit(Diagnostic::error( + span, + "`-` operator must be surrounded by an equal amount of spaces", + )); + p.advance_with_error() + } + _ => { assert!( !PREFIX_TOKENS.contains(p.peek()), @@ -615,9 +635,9 @@ fn prefix(p: &mut Parser) -> Closed { } fn infix(p: &mut Parser, op: (TokenKind, Spaces)) -> NodeKind { - match op.0 { + let (kind, spaces) = op; + match kind { TokenKind::Plus - | TokenKind::Minus | TokenKind::Star | TokenKind::Slash | TokenKind::EqualEqual @@ -627,6 +647,7 @@ fn infix(p: &mut Parser, op: (TokenKind, Spaces)) -> NodeKind { | TokenKind::Greater | TokenKind::GreaterEqual | TokenKind::Colon => infix_binary(p, op), + TokenKind::Minus if spaces.are_balanced() => infix_binary(p, op), TokenKind::Equal => infix_let(p, op), diff --git a/crates/haku/src/token.rs b/crates/haku/src/token.rs index c613408..047caf0 100644 --- a/crates/haku/src/token.rs +++ b/crates/haku/src/token.rs @@ -135,6 +135,10 @@ impl Spaces { pub fn pair(self) -> (bool, bool) { (self.left(), self.right()) } + + pub fn are_balanced(self) -> bool { + matches!(self.pair(), (true, true) | (false, false)) + } } impl fmt::Debug for Spaces { diff --git a/static/brush-box.js b/static/brush-box.js index dfcc850..27750af 100644 --- a/static/brush-box.js +++ b/static/brush-box.js @@ -100,7 +100,7 @@ norm: \\u -> u / vec l l perpClockwise: \\v -> - vec (vecY v) (-(vecX v)) + vec (vecY v) -(vecX v) withDotter \\d -> pi = 3.14159265