use crate::SyntaxKind::{
self, AMP_AMP, ARRAY_LITERAL, BANG, BANG_EQ, BANG_QUESTION, BOOLEAN_LIT, CALL_EXPR, CARET,
COLON, COMMA, DIVERT, DIVERT_TARGET_EXPR, DOT, EOF, EQ, EQ_EQ, FIELD_ACCESS_EXPR, FLOAT,
FLOAT_LIT, FN_LITERAL, FUNCTION_CALL, GT, GT_EQ, HASH, IDENT, IDENTIFIER, INDEX_EXPR,
INFIX_EXPR, INTEGER, INTEGER_LIT, KW_AND, KW_FALSE, KW_HAS, KW_HASNT, KW_MOD, KW_NOT, KW_OR,
KW_REF, KW_SHUFFLE, KW_TRUE, L_BRACE, L_BRACKET, L_PAREN, LIST_EXPR, LT, LT_EQ, MAP_ENTRY,
MAP_LITERAL, MINUS, MINUS_EQ, NEWLINE, PAREN_EXPR, PERCENT, PIPE, PLUS, PLUS_EQ, POSTFIX_EXPR,
PREFIX_EXPR, QUESTION, QUOTE, R_BRACE, R_BRACKET, R_PAREN, RANGE_EXPR, REF_EXPR, SLASH, STAR,
STRING_LIT, STRUCT_FIELD_INIT, STRUCT_LITERAL,
};
use super::Parser;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[repr(u8)]
enum Prec {
None = 0,
Assign = 1, Or = 2, And = 3, Equality = 4, Comparison = 5, HasOps = 6, Range = 7, Add = 8, Mul = 9, Intersect = 10, Prefix = 11, }
fn infix_binding_power(kind: SyntaxKind) -> Option<(Prec, bool)> {
Some(match kind {
PLUS_EQ | MINUS_EQ => (Prec::Assign, true),
KW_OR => (Prec::Or, false),
AMP_AMP | KW_AND => (Prec::And, false),
EQ_EQ | BANG_EQ => (Prec::Equality, false),
LT | GT | LT_EQ | GT_EQ => (Prec::Comparison, false),
QUESTION | BANG_QUESTION | KW_HAS | KW_HASNT => (Prec::HasOps, false),
PLUS | MINUS => (Prec::Add, false),
STAR | SLASH | PERCENT | KW_MOD => (Prec::Mul, false),
CARET => (Prec::Intersect, true),
_ => return None,
})
}
fn is_prefix_op(kind: SyntaxKind) -> bool {
matches!(kind, MINUS | BANG | KW_NOT)
}
pub(crate) fn expression(p: &mut Parser<'_, '_>) {
expression_bp(p, Prec::None);
}
#[expect(
clippy::too_many_lines,
reason = "one commented arm per operator family — the NS-A5 range arm \
pushed this just past 100; splitting the loop would obscure \
the single Pratt structure"
)]
fn expression_bp(p: &mut Parser<'_, '_>, min_bp: Prec) {
let checkpoint = p.checkpoint();
if is_prefix_op(p.current()) {
p.start_node_at(checkpoint, PREFIX_EXPR);
p.skip_ws();
p.bump(); p.skip_ws();
expression_bp(p, Prec::Prefix);
p.finish_node();
} else {
if !atom(p) {
return;
}
}
loop {
p.skip_ws();
if p.current() == PLUS && p.nth_raw(1) == PLUS {
p.start_node_at(checkpoint, POSTFIX_EXPR);
p.bump(); p.bump(); p.finish_node();
continue;
}
if p.current() == MINUS && p.nth_raw(1) == MINUS {
p.start_node_at(checkpoint, POSTFIX_EXPR);
p.bump(); p.bump(); p.finish_node();
continue;
}
if p.current() == L_BRACKET {
p.start_node_at(checkpoint, INDEX_EXPR);
index_bracket(p);
p.finish_node();
continue;
}
if p.current() == DOT && p.nth(1) == IDENT {
p.start_node_at(checkpoint, FIELD_ACCESS_EXPR);
p.bump(); p.skip_ws();
p.start_node(IDENTIFIER);
p.bump(); p.finish_node();
p.finish_node();
continue;
}
if p.current() == L_PAREN {
p.start_node_at(checkpoint, CALL_EXPR);
p.bump(); p.skip_ws();
if p.current() != R_PAREN {
super::divert::arg_list(p);
}
p.skip_ws();
p.expect(R_PAREN);
p.finish_node();
continue;
}
if p.current() == DOT && p.nth_raw(1) == DOT {
let prec = Prec::Range;
if prec <= min_bp {
break;
}
let inclusive = p.nth_raw(2) == EQ;
p.start_node_at(checkpoint, RANGE_EXPR);
p.bump(); p.bump(); if inclusive {
p.bump(); }
p.skip_ws();
expression_bp(p, prec);
p.finish_node();
continue;
}
if p.current() == PIPE && p.nth_raw(1) == PIPE {
let (prec, right_assoc) = (Prec::Or, false);
if prec < min_bp || (prec == min_bp && !right_assoc) {
break;
}
p.start_node_at(checkpoint, INFIX_EXPR);
p.bump(); p.bump(); p.skip_ws();
expression_bp(p, Prec::Or);
p.finish_node();
continue;
}
let Some((prec, right_assoc)) = infix_binding_power(p.current()) else {
break;
};
if prec < min_bp {
break;
}
if prec == min_bp && !right_assoc {
break;
}
p.start_node_at(checkpoint, INFIX_EXPR);
p.skip_ws();
p.bump(); p.skip_ws();
expression_bp(p, prec);
p.finish_node();
}
}
fn atom(p: &mut Parser<'_, '_>) -> bool {
match p.current() {
L_PAREN => {
if looks_like_list_expr(p) {
list_expr(p);
} else {
paren_expr(p);
}
true
}
DIVERT => {
divert_target_expr(p);
true
}
IDENT => {
if p.nth(1) == L_PAREN {
function_call(p);
} else if p.nth(1) == HASH && p.nth(2) == L_BRACE {
struct_literal(p);
} else {
super::divert::path(p);
}
true
}
KW_SHUFFLE if p.nth(1) == L_PAREN => {
function_call(p);
true
}
INTEGER => {
p.start_node(INTEGER_LIT);
p.bump();
p.finish_node();
true
}
FLOAT => {
p.start_node(FLOAT_LIT);
p.bump();
p.finish_node();
true
}
QUOTE => {
string_literal(p);
true
}
KW_TRUE | KW_FALSE => {
p.start_node(BOOLEAN_LIT);
p.bump();
p.finish_node();
true
}
HASH if p.nth(1) == IDENT && p.nth_text(1) == "fn" && p.nth(2) == L_PAREN => {
fn_literal(p);
true
}
HASH if p.nth(1) == L_BRACKET => {
array_literal(p);
true
}
HASH if p.nth(1) == L_BRACE => {
map_literal(p);
true
}
KW_REF => {
ref_expr(p);
true
}
_ => false,
}
}
fn ref_expr(p: &mut Parser<'_, '_>) {
p.start_node(REF_EXPR);
p.bump(); p.skip_ws();
expression_bp(p, Prec::Prefix);
p.finish_node();
}
pub(crate) fn index_bracket(p: &mut Parser<'_, '_>) {
p.bump(); if p.at_depth_limit() {
p.error("nesting depth limit exceeded".into());
skip_balanced(p, L_BRACKET, R_BRACKET);
return;
}
p.depth += 1;
p.skip_ws();
expression(p);
p.skip_ws();
p.expect(R_BRACKET);
p.depth -= 1;
}
fn array_literal(p: &mut Parser<'_, '_>) {
p.start_node(ARRAY_LITERAL);
p.bump(); p.skip_ws();
p.bump_assert(L_BRACKET);
if p.at_depth_limit() {
p.error("nesting depth limit exceeded".into());
skip_balanced(p, L_BRACKET, R_BRACKET);
p.finish_node();
return;
}
p.depth += 1;
p.skip_ws();
if p.current() != R_BRACKET {
expression(p);
loop {
p.skip_ws();
if !p.eat(COMMA) {
break;
}
p.skip_ws();
if p.current() == R_BRACKET {
break; }
expression(p);
}
}
p.skip_ws();
p.expect(R_BRACKET);
p.depth -= 1;
p.finish_node();
}
fn map_literal(p: &mut Parser<'_, '_>) {
p.start_node(MAP_LITERAL);
p.bump(); p.skip_ws();
p.bump_assert(L_BRACE);
if p.at_depth_limit() {
p.error("nesting depth limit exceeded".into());
skip_balanced(p, L_BRACE, R_BRACE);
p.finish_node();
return;
}
p.depth += 1;
p.skip_ws();
if p.current() != R_BRACE {
map_entry(p);
loop {
p.skip_ws();
if !p.eat(COMMA) {
break;
}
p.skip_ws();
if p.current() == R_BRACE {
break; }
map_entry(p);
}
}
p.skip_ws();
p.expect(R_BRACE);
p.depth -= 1;
p.finish_node();
}
fn map_entry(p: &mut Parser<'_, '_>) {
p.start_node(MAP_ENTRY);
expression(p);
p.skip_ws();
p.expect(COLON);
p.skip_ws();
expression(p);
p.finish_node();
}
fn struct_literal(p: &mut Parser<'_, '_>) {
p.start_node(STRUCT_LITERAL);
p.start_node(IDENTIFIER);
p.bump(); p.finish_node();
p.skip_ws();
p.bump_assert(HASH);
p.skip_ws();
p.bump_assert(L_BRACE);
if p.at_depth_limit() {
p.error("nesting depth limit exceeded".into());
skip_balanced(p, L_BRACE, R_BRACE);
p.finish_node();
return;
}
p.depth += 1;
p.skip_ws();
if p.current() != R_BRACE {
struct_field_init(p);
loop {
p.skip_ws();
if !p.eat(COMMA) {
break;
}
p.skip_ws();
if p.current() == R_BRACE {
break; }
struct_field_init(p);
}
}
p.skip_ws();
p.expect(R_BRACE);
p.depth -= 1;
p.finish_node();
}
fn fn_literal(p: &mut Parser<'_, '_>) {
p.start_node(FN_LITERAL);
p.bump(); p.skip_ws();
p.bump_assert(IDENT); p.skip_ws();
p.bump_assert(L_PAREN);
if p.at_depth_limit() {
p.error("nesting depth limit exceeded".into());
skip_balanced(p, L_PAREN, R_PAREN);
p.finish_node();
return;
}
p.depth += 1;
p.skip_ws();
if p.current() != R_PAREN {
if p.at_ident_or_keyword() {
super::divert::path(p);
} else {
p.error("expected function name".into());
}
loop {
p.skip_ws();
if !p.eat(COMMA) {
break;
}
p.skip_ws();
if p.current() == R_PAREN {
break; }
expression(p);
}
}
p.skip_ws();
p.expect(R_PAREN);
p.depth -= 1;
p.finish_node();
}
fn struct_field_init(p: &mut Parser<'_, '_>) {
p.start_node(STRUCT_FIELD_INIT);
p.start_node(IDENTIFIER);
p.expect_ident_or_keyword();
p.finish_node();
p.skip_ws();
p.expect(COLON);
p.skip_ws();
expression(p);
p.finish_node();
}
pub(crate) fn skip_balanced(p: &mut Parser<'_, '_>, open: SyntaxKind, close: SyntaxKind) {
let mut depth = 1u32;
while !p.at_eof() && depth > 0 {
let k = p.current();
if k == open {
depth += 1;
p.bump();
} else if k == close {
depth -= 1;
if depth > 0 {
p.bump();
}
} else {
p.bump();
}
}
if p.current() == close {
p.bump();
}
}
fn looks_like_list_expr(p: &Parser<'_, '_>) -> bool {
if p.nth(1) == R_PAREN {
return true;
}
let mut i = 1;
if p.nth(i) != IDENT {
return false;
}
i += 1;
while p.nth(i) == DOT && p.nth(i + 1) == IDENT {
i += 2;
}
matches!(p.nth(i), COMMA | R_PAREN)
}
fn paren_expr(p: &mut Parser<'_, '_>) {
p.start_node(PAREN_EXPR);
p.bump();
if p.at_depth_limit() {
p.error("nesting depth limit exceeded".into());
let mut depth = 1u32;
while !p.at_eof() && depth > 0 {
match p.current() {
L_PAREN => {
depth += 1;
p.bump();
}
R_PAREN => {
depth -= 1;
if depth > 0 {
p.bump();
}
}
_ => p.bump(),
}
}
if p.current() == R_PAREN {
p.bump();
}
p.finish_node();
return;
}
p.depth += 1;
p.skip_ws();
expression(p);
p.skip_ws();
p.expect(R_PAREN);
p.depth -= 1;
p.finish_node();
}
fn list_expr(p: &mut Parser<'_, '_>) {
p.start_node(LIST_EXPR);
p.bump(); p.skip_ws();
if p.current() != R_PAREN {
super::divert::path(p);
loop {
p.skip_ws();
if !p.eat(COMMA) {
break;
}
p.skip_ws();
super::divert::path(p);
}
}
p.skip_ws();
p.expect(R_PAREN);
p.finish_node();
}
fn divert_target_expr(p: &mut Parser<'_, '_>) {
p.start_node(DIVERT_TARGET_EXPR);
p.bump(); p.skip_ws();
super::divert::path(p);
p.finish_node();
}
fn function_call(p: &mut Parser<'_, '_>) {
p.start_node(FUNCTION_CALL);
p.start_node(IDENTIFIER);
p.bump(); p.finish_node();
p.skip_ws();
p.bump(); p.skip_ws();
if p.current() != R_PAREN {
super::divert::arg_list(p);
}
p.skip_ws();
p.expect(R_PAREN);
p.finish_node();
}
fn string_literal(p: &mut Parser<'_, '_>) {
p.start_node(STRING_LIT);
p.bump();
loop {
match p.nth_raw(0) {
QUOTE => {
p.bump(); break;
}
L_BRACE => {
super::inline::inline_logic(p);
}
NEWLINE | EOF => {
p.error("unterminated string literal".into());
break;
}
_ => {
p.bump();
}
}
}
p.finish_node();
}