From 58a977fbf358f50be2a5506f8a344b73aa440f0e Mon Sep 17 00:00:00 2001 From: Steve Dignam Date: Sat, 26 Sep 2026 15:32:14 -0400 Subject: [PATCH] parser: refactor plpgsql order --- crates/squawk_parser/src/plpgsql_grammar.rs | 543 +++++++++--------- ...syntax__test__plpgsql_comp_option_err.snap | 56 +- ...k_syntax__test__plpgsql_loop_stmt_err.snap | 12 +- 3 files changed, 308 insertions(+), 303 deletions(-) diff --git a/crates/squawk_parser/src/plpgsql_grammar.rs b/crates/squawk_parser/src/plpgsql_grammar.rs index 482aed24..6421a91c 100644 --- a/crates/squawk_parser/src/plpgsql_grammar.rs +++ b/crates/squawk_parser/src/plpgsql_grammar.rs @@ -17,14 +17,12 @@ pub(crate) fn plpgsql_entry_point(p: &mut Parser) { if at_block_start(p) { opt_block(p); } else { - temp_unknown(p, "expected a block"); + err_recover_stmt(p, "expected a block", at_block_start); } } m.complete(p, PLPGSQL); } -const VARIABLE_CONFLICT_VALUES: [SyntaxKind; 3] = [ERROR_KW, USE_VARIABLE_KW, USE_COLUMN_KW]; - fn comp_option(p: &mut Parser) { assert!(p.at(POUND)); let m = p.start(); @@ -48,6 +46,8 @@ fn comp_option(p: &mut Parser) { m.complete(p, kind); } +const VARIABLE_CONFLICT_VALUES: [SyntaxKind; 3] = [ERROR_KW, USE_VARIABLE_KW, USE_COLUMN_KW]; + fn variable_conflict_value(p: &mut Parser) { match VARIABLE_CONFLICT_VALUES .into_iter() @@ -66,8 +66,6 @@ fn option_value(p: &mut Parser) { name(p, PLPGSQL_OPTION_VALUE); } -const BLOCK_FIRST: TokenSet = TokenSet::new(&[BEGIN_KW, DECLARE_KW]); - fn opt_block(p: &mut Parser) { if !at_block_start(p) { return; @@ -120,10 +118,10 @@ fn opt_declare_section(p: &mut Parser) { alias_decl(p); } else if at_cursor_decl(p) { cursor_decl(p); - } else if at_var_decl(p) { + } else if at_name(p) { var_decl(p); } else { - temp_unknown(p, "expected a declaration"); + err_recover_stmt(p, "expected a declaration", |p| p.at(BEGIN_KW)); } } m.complete(p, PLPGSQL_DECLARE_SECTION); @@ -255,29 +253,6 @@ fn path_segment_ref(p: &mut Parser) { m.complete(p, PATH_SEGMENT_REF); } -fn at_percent_datatype(p: &Parser) -> bool { - let Some(n) = at_path(p) else { - return false; - }; - p.nth_at(n, PERCENT) && (p.nth_at(n + 1, TYPE_KW) || p.nth_at_contextual_kw(n + 1, ROWTYPE_KW)) -} - -fn at_path(p: &Parser) -> Option { - let mut n = 0; - while !p.nth_at(n, EOF) && nth_at_name(p, n) { - let unicode_ident = p.nth_at(n, IDENT); - n += 1; - if unicode_ident && p.nth_at(n, UESCAPE_KW) && p.nth_at(n + 1, STRING) { - n += 2; - } - if !p.nth_at(n, DOT) { - return Some(n); - } - n += 1; - } - None -} - fn opt_not_null(p: &mut Parser) { if !p.at(NOT_KW) { return; @@ -300,6 +275,45 @@ fn opt_var_init(p: &mut Parser) { m.complete(p, PLPGSQL_VAR_INIT); } +fn opt_exception_section(p: &mut Parser) { + if !p.at_contextual_kw(EXCEPTION_KW) { + return; + } + let m = p.start(); + p.bump_remap(EXCEPTION_KW); + while !p.at(EOF) && p.at(WHEN_KW) { + exception_handler(p); + } + m.complete(p, PLPGSQL_EXCEPTION_SECTION); +} + +fn exception_handler(p: &mut Parser) { + assert!(p.at(WHEN_KW)); + let m = p.start(); + // TODO: use delimited + p.bump(WHEN_KW); + condition(p); + while !p.at(EOF) && p.eat(OR_KW) { + condition(p); + } + p.expect(THEN_KW); + body(p, BodyKind::ExceptionHandler); + m.complete(p, PLPGSQL_EXCEPTION_HANDLER); +} + +fn condition(p: &mut Parser) { + let m = p.start(); + if p.at_contextual_kw(SQLSTATE_KW) { + p.bump_remap(SQLSTATE_KW); + p.expect(STRING); + } else if at_name(p) { + p.bump_any(); + } else { + p.error(format!("expected a condition name, got {:?}", p.current())); + } + m.complete(p, PLPGSQL_CONDITION); +} + fn expr(p: &mut Parser) { if grammar::plpgsql_expr(p).is_none() { p.error("expected an expression"); @@ -314,16 +328,6 @@ fn expr_until(p: &mut Parser, terminator: Option<(SyntaxKind, usize)>) { } } -const SEMICOLON_TERMINATOR: TokenSet = TokenSet::new(&[SEMICOLON]); -const COMMA_TERMINATOR: TokenSet = TokenSet::new(&[COMMA, SEMICOLON]); -const COMMA_OR_USING_TERMINATOR: TokenSet = TokenSet::new(&[COMMA, SEMICOLON, USING_KW]); -const LOOP_TERMINATOR: TokenSet = TokenSet::new(&[LOOP_KW]); -const DOT_DOT_TERMINATOR: TokenSet = TokenSet::new(&[DOT_DOT, SEMICOLON]); -const BY_TERMINATOR: TokenSet = TokenSet::new(&[BY_KW, SEMICOLON]); -const THEN_TERMINATOR: TokenSet = TokenSet::new(&[THEN_KW]); -const WHEN_TERMINATOR: TokenSet = TokenSet::new(&[WHEN_KW]); -const USING_TERMINATOR: TokenSet = TokenSet::new(&[USING_KW, SEMICOLON]); -const INTO_OR_USING_TERMINATOR: TokenSet = TokenSet::new(&[INTO_KW, USING_KW, SEMICOLON]); const RECOVERY_TERMINATORS: TokenSet = TokenSet::new(&[THEN_KW, WHEN_KW, USING_KW, ELSE_KW, END_KW, SEMICOLON]); @@ -333,22 +337,76 @@ fn expr_until_ts(p: &mut Parser, stop: TokenSet, contextual_stop: TokenSet) { expr_until(p, terminator); } +// postgres does a similar thing to look ahead until it sees a loop token +fn top_level_terminator( + p: &Parser, + stop: TokenSet, + contextual_stop: TokenSet, +) -> Option<(SyntaxKind, usize)> { + let mut depth = 0i32; + let mut case_depth = 0i32; + let mut n = 0; + while !p.nth_at(n, EOF) { + match p.nth(n) { + L_PAREN | L_BRACK => depth += 1, + R_PAREN | R_BRACK if depth > 0 => depth -= 1, + CASE_KW => case_depth += 1, + END_KW if case_depth > 0 => case_depth -= 1, + kind if depth == 0 && case_depth == 0 && stop.contains(kind) => { + return Some((kind, n)); + } + _ if depth == 0 && case_depth == 0 && p.nth_at_contextual_ts(n, contextual_stop) => { + return Some((p.nth_contextual_kind(n), n)); + } + _ => (), + } + n += 1; + } + None +} + +const SEMICOLON_TERMINATOR: TokenSet = TokenSet::new(&[SEMICOLON]); + fn expr_until_semi(p: &mut Parser) { expr_until_ts(p, SEMICOLON_TERMINATOR, TokenSet::EMPTY); } +const THEN_TERMINATOR: TokenSet = TokenSet::new(&[THEN_KW]); + fn expr_until_then(p: &mut Parser) { expr_until_ts(p, THEN_TERMINATOR, TokenSet::EMPTY); } +const WHEN_TERMINATOR: TokenSet = TokenSet::new(&[WHEN_KW]); + fn expr_until_when(p: &mut Parser) { expr_until_ts(p, WHEN_TERMINATOR, TokenSet::EMPTY); } +const USING_TERMINATOR: TokenSet = TokenSet::new(&[USING_KW, SEMICOLON]); + fn expr_until_using(p: &mut Parser) { expr_until_ts(p, USING_TERMINATOR, TokenSet::EMPTY); } +const INTO_OR_USING_TERMINATOR: TokenSet = TokenSet::new(&[INTO_KW, USING_KW, SEMICOLON]); + +fn expr_until_into_or_using(p: &mut Parser) { + expr_until_ts(p, INTO_OR_USING_TERMINATOR, TokenSet::EMPTY); +} + +const LOOP_TERMINATOR: TokenSet = TokenSet::new(&[LOOP_KW]); + +fn expr_until_loop(p: &mut Parser, extra_follow: TokenSet) { + expr_until_ts(p, SEMICOLON_TERMINATOR.union(extra_follow), LOOP_TERMINATOR); +} + +fn expr_until_loop_or_using(p: &mut Parser) { + expr_until_ts(p, USING_TERMINATOR, LOOP_TERMINATOR); +} + +const COMMA_TERMINATOR: TokenSet = TokenSet::new(&[COMMA, SEMICOLON]); + fn comma_expr(p: &mut Parser) { expr_until_ts(p, COMMA_TERMINATOR, TokenSet::EMPTY); } @@ -357,6 +415,8 @@ fn comma_expr_until_loop(p: &mut Parser) { expr_until_ts(p, COMMA_TERMINATOR, LOOP_TERMINATOR); } +const COMMA_OR_USING_TERMINATOR: TokenSet = TokenSet::new(&[COMMA, SEMICOLON, USING_KW]); + fn comma_expr_until_using(p: &mut Parser) { expr_until_ts(p, COMMA_OR_USING_TERMINATOR, TokenSet::EMPTY); } @@ -375,12 +435,12 @@ enum BodyKind { fn body(p: &mut Parser, kind: BodyKind) { let m = p.start(); while !p.at(EOF) && !at_body_end(p, kind) { - stmt(p); + stmt(p, kind); } m.complete(p, PLPGSQL_BODY); } -fn stmt(p: &mut Parser) { +fn stmt(p: &mut Parser, kind: BodyKind) { if at_block_start(p) { opt_block(p); } else if at_loop_start(p) { @@ -425,13 +485,13 @@ fn stmt(p: &mut Parser) { p.bump(SEMICOLON); m.complete(p, PLPGSQL_NULL_STMT); } else if at_exec_sql_stmt(p) { - exec_sql_stmt(p); + exec_sql_stmt(p, kind); } else { - temp_unknown(p, "expected a statement"); + err_recover_stmt(p, "expected a statement", |p| at_body_end(p, kind)); } } -fn exec_sql_stmt(p: &mut Parser) { +fn exec_sql_stmt(p: &mut Parser, kind: BodyKind) { let m = p.start(); // Another hack since postgres just looks for certain tokens and passes the // text in between to the sql parser. @@ -445,13 +505,15 @@ fn exec_sql_stmt(p: &mut Parser) { grammar::stmt(p, &grammar::StmtRestrictions::default()); } } - if !p.at(SEMICOLON) { + if p.at(SEMICOLON) || at_body_end(p, kind) { + p.expect(SEMICOLON); + } else { let m = p.start(); p.error(format!("expected SEMICOLON, got {:?}", p.current())); - skip_to_stmt_end(p); + skip_to_stmt_end(p, |p| at_body_end(p, kind)); m.complete(p, ERROR); + p.eat(SEMICOLON); } - p.expect(SEMICOLON); m.complete(p, PLPGSQL_EXEC_SQL_STMT); } @@ -552,27 +614,6 @@ fn opt_using_clause(p: &mut Parser, param: fn(&mut Parser)) { m.complete(p, PLPGSQL_USING_CLAUSE); } -const RAISE_LEVELS: [SyntaxKind; 6] = [ - EXCEPTION_KW, - WARNING_KW, - NOTICE_KW, - INFO_KW, - LOG_KW, - DEBUG_KW, -]; - -const RAISE_OPTIONS: [(SyntaxKind, SyntaxKind); 9] = [ - (ERRCODE_KW, PLPGSQL_RAISE_OPTION_ERRCODE), - (MESSAGE_KW, PLPGSQL_RAISE_OPTION_MESSAGE), - (DETAIL_KW, PLPGSQL_RAISE_OPTION_DETAIL), - (HINT_KW, PLPGSQL_RAISE_OPTION_HINT), - (COLUMN_KW, PLPGSQL_RAISE_OPTION_COLUMN), - (CONSTRAINT_KW, PLPGSQL_RAISE_OPTION_CONSTRAINT), - (DATATYPE_KW, PLPGSQL_RAISE_OPTION_DATATYPE), - (TABLE_KW, PLPGSQL_RAISE_OPTION_TABLE), - (SCHEMA_KW, PLPGSQL_RAISE_OPTION_SCHEMA), -]; - fn raise_stmt(p: &mut Parser) { assert!(p.at_contextual_kw(RAISE_KW)); let m = p.start(); @@ -592,6 +633,15 @@ fn raise_stmt(p: &mut Parser) { m.complete(p, PLPGSQL_RAISE_STMT); } +const RAISE_LEVELS: [SyntaxKind; 6] = [ + EXCEPTION_KW, + WARNING_KW, + NOTICE_KW, + INFO_KW, + LOG_KW, + DEBUG_KW, +]; + fn opt_raise_level(p: &mut Parser) { let Some(kw) = RAISE_LEVELS .into_iter() @@ -625,6 +675,18 @@ fn opt_raise_using_clause(p: &mut Parser) { m.complete(p, PLPGSQL_RAISE_USING_CLAUSE); } +const RAISE_OPTIONS: [(SyntaxKind, SyntaxKind); 9] = [ + (ERRCODE_KW, PLPGSQL_RAISE_OPTION_ERRCODE), + (MESSAGE_KW, PLPGSQL_RAISE_OPTION_MESSAGE), + (DETAIL_KW, PLPGSQL_RAISE_OPTION_DETAIL), + (HINT_KW, PLPGSQL_RAISE_OPTION_HINT), + (COLUMN_KW, PLPGSQL_RAISE_OPTION_COLUMN), + (CONSTRAINT_KW, PLPGSQL_RAISE_OPTION_CONSTRAINT), + (DATATYPE_KW, PLPGSQL_RAISE_OPTION_DATATYPE), + (TABLE_KW, PLPGSQL_RAISE_OPTION_TABLE), + (SCHEMA_KW, PLPGSQL_RAISE_OPTION_SCHEMA), +]; + fn raise_option(p: &mut Parser) { let m = p.start(); let kind = match RAISE_OPTIONS @@ -661,22 +723,6 @@ fn transaction_stmt(p: &mut Parser) { m.complete(p, kind); } -const DIAG_ITEM_KINDS: [SyntaxKind; 13] = [ - ROW_COUNT_KW, - PG_ROUTINE_OID_KW, - PG_CONTEXT_KW, - PG_EXCEPTION_DETAIL_KW, - PG_EXCEPTION_HINT_KW, - PG_EXCEPTION_CONTEXT_KW, - COLUMN_NAME_KW, - CONSTRAINT_NAME_KW, - PG_DATATYPE_NAME_KW, - MESSAGE_TEXT_KW, - TABLE_NAME_KW, - SCHEMA_NAME_KW, - RETURNED_SQLSTATE_KW, -]; - fn get_diag_stmt(p: &mut Parser) { assert!(at_stmt_kw(p, GET_KW)); let m = p.start(); @@ -741,6 +787,22 @@ fn scalar_target(p: &mut Parser, kind: SyntaxKind) { m.complete(p, kind); } +const DIAG_ITEM_KINDS: [SyntaxKind; 13] = [ + ROW_COUNT_KW, + PG_ROUTINE_OID_KW, + PG_CONTEXT_KW, + PG_EXCEPTION_DETAIL_KW, + PG_EXCEPTION_HINT_KW, + PG_EXCEPTION_CONTEXT_KW, + COLUMN_NAME_KW, + CONSTRAINT_NAME_KW, + PG_DATATYPE_NAME_KW, + MESSAGE_TEXT_KW, + TABLE_NAME_KW, + SCHEMA_NAME_KW, + RETURNED_SQLSTATE_KW, +]; + fn diag_kind(p: &mut Parser) { let m = p.start(); match DIAG_ITEM_KINDS @@ -797,6 +859,47 @@ fn fetch_stmt(p: &mut Parser) { m.complete(p, PLPGSQL_FETCH_STMT); } +fn move_stmt(p: &mut Parser) { + assert!(at_stmt_kw(p, MOVE_KW)); + let m = p.start(); + p.bump(MOVE_KW); + fetch_direction(p); + cursor_variable_ref(p); + p.expect(SEMICOLON); + m.complete(p, PLPGSQL_MOVE_STMT); +} + +fn fetch_direction(p: &mut Parser) { + let direction = grammar::opt_direction(p); + let from_or_in = p.eat(FROM_KW) || p.eat(IN_KW); + if direction && !from_or_in { + p.error("expected FROM or IN"); + } +} + +fn close_stmt(p: &mut Parser) { + assert!(at_stmt_kw(p, CLOSE_KW)); + let m = p.start(); + p.bump(CLOSE_KW); + cursor_variable_ref(p); + p.expect(SEMICOLON); + m.complete(p, PLPGSQL_CLOSE_STMT); +} + +fn cursor_variable_ref(p: &mut Parser) { + if !at_name(p) { + p.error(format!("expected a cursor variable, got {:?}", p.current())); + return; + } + name(p, PLPGSQL_CURSOR_VARIABLE_REF); + if p.at(DOT) || p.at(L_BRACK) { + let m = p.start(); + p.error("a cursor variable must be a simple variable"); + grammar::accessors(p); + m.complete(p, ERROR); + } +} + fn dyn_execute_stmt(p: &mut Parser) { assert!(at_stmt_kw(p, EXECUTE_KW)); let m = p.start(); @@ -819,24 +922,6 @@ fn dyn_execute_stmt(p: &mut Parser) { m.complete(p, PLPGSQL_DYN_EXECUTE_STMT); } -fn move_stmt(p: &mut Parser) { - assert!(at_stmt_kw(p, MOVE_KW)); - let m = p.start(); - p.bump(MOVE_KW); - fetch_direction(p); - cursor_variable_ref(p); - p.expect(SEMICOLON); - m.complete(p, PLPGSQL_MOVE_STMT); -} - -fn fetch_direction(p: &mut Parser) { - let direction = grammar::opt_direction(p); - let from_or_in = p.eat(FROM_KW) || p.eat(IN_KW); - if direction && !from_or_in { - p.error("expected FROM or IN"); - } -} - fn into_clause(p: &mut Parser) { if !p.at(INTO_KW) { p.error(format!("expected INTO, got {:?}", p.current())); @@ -866,29 +951,6 @@ fn into_target(p: &mut Parser) { scalar_target(p, PLPGSQL_INTO_TARGET); } -fn close_stmt(p: &mut Parser) { - assert!(at_stmt_kw(p, CLOSE_KW)); - let m = p.start(); - p.bump(CLOSE_KW); - cursor_variable_ref(p); - p.expect(SEMICOLON); - m.complete(p, PLPGSQL_CLOSE_STMT); -} - -fn cursor_variable_ref(p: &mut Parser) { - if !at_name(p) { - p.error(format!("expected a cursor variable, got {:?}", p.current())); - return; - } - name(p, PLPGSQL_CURSOR_VARIABLE_REF); - if p.at(DOT) || p.at(L_BRACK) { - let m = p.start(); - p.error("a cursor variable must be a simple variable"); - grammar::accessors(p); - m.complete(p, ERROR); - } -} - fn assign_stmt(p: &mut Parser) { assert!(at_assign_stmt(p)); let m = p.start(); @@ -923,6 +985,33 @@ fn if_stmt(p: &mut Parser) { m.complete(p, PLPGSQL_IF_STMT); } +fn opt_elsif_clause(p: &mut Parser) -> bool { + if !at_elsif(p) { + return false; + } + let m = p.start(); + if p.at_contextual_kw(ELSEIF_KW) { + p.bump_remap(ELSEIF_KW); + } else { + p.bump_remap(ELSIF_KW); + } + expr_until_then(p); + p.expect(THEN_KW); + body(p, BodyKind::IfThen); + m.complete(p, PLPGSQL_ELSIF_CLAUSE); + true +} + +fn opt_else_clause(p: &mut Parser, kind: BodyKind) { + if !p.at(ELSE_KW) { + return; + } + let m = p.start(); + p.bump(ELSE_KW); + body(p, kind); + m.complete(p, PLPGSQL_ELSE_CLAUSE); +} + fn case_stmt(p: &mut Parser) { assert!(p.at(CASE_KW)); let m = p.start(); @@ -977,6 +1066,8 @@ fn loop_stmt(p: &mut Parser) { m.complete(p, kind); } +const DOT_DOT_TERMINATOR: TokenSet = TokenSet::new(&[DOT_DOT, SEMICOLON]); + fn for_head(p: &mut Parser) -> SyntaxKind { assert!(p.at(FOR_KW)); p.bump(FOR_KW); @@ -987,7 +1078,7 @@ fn for_head(p: &mut Parser) -> SyntaxKind { opt_using_clause(p, comma_expr_until_loop); return PLPGSQL_FOR_DYN_STMT; } - if at_for_cursor(p, 0) { + if at_for_cursor(p) { cursor_variable_ref(p); if p.at(L_PAREN) { grammar::arg_list(p); @@ -1066,6 +1157,8 @@ fn for_variable(p: &mut Parser) { scalar_target(p, PLPGSQL_FOR_VARIABLE); } +const BY_TERMINATOR: TokenSet = TokenSet::new(&[BY_KW, SEMICOLON]); + fn for_range(p: &mut Parser) { let m = p.start(); expr_until_loop(p, DOT_DOT_TERMINATOR); @@ -1077,18 +1170,6 @@ fn for_range(p: &mut Parser) { m.complete(p, PLPGSQL_FOR_RANGE); } -fn expr_until_into_or_using(p: &mut Parser) { - expr_until_ts(p, INTO_OR_USING_TERMINATOR, TokenSet::EMPTY); -} - -fn expr_until_loop(p: &mut Parser, extra_follow: TokenSet) { - expr_until_ts(p, SEMICOLON_TERMINATOR.union(extra_follow), LOOP_TERMINATOR); -} - -fn expr_until_loop_or_using(p: &mut Parser) { - expr_until_ts(p, USING_TERMINATOR, LOOP_TERMINATOR); -} - fn exit_stmt(p: &mut Parser) { assert!(at_exit_stmt(p)); let m = p.start(); @@ -1114,115 +1195,38 @@ fn opt_exit_when(p: &mut Parser) { m.complete(p, PLPGSQL_EXIT_WHEN); } -fn expect_contextual_kw(p: &mut Parser, kw: SyntaxKind) { - if p.at_contextual_kw(kw) { - p.bump_remap(kw); - } else { - p.error(format!("expected {kw:?}")); - } -} - -fn opt_elsif_clause(p: &mut Parser) -> bool { - if !at_elsif(p) { - return false; - } - let m = p.start(); - if p.at_contextual_kw(ELSEIF_KW) { - p.bump_remap(ELSEIF_KW); - } else { - p.bump_remap(ELSIF_KW); - } - expr_until_then(p); - p.expect(THEN_KW); - body(p, BodyKind::IfThen); - m.complete(p, PLPGSQL_ELSIF_CLAUSE); - true +fn at_alias_decl(p: &Parser) -> bool { + at_name(p) && p.nth_at_contextual_kw(1, ALIAS_KW) } -fn opt_else_clause(p: &mut Parser, kind: BodyKind) { - if !p.at(ELSE_KW) { - return; - } - let m = p.start(); - p.bump(ELSE_KW); - body(p, kind); - m.complete(p, PLPGSQL_ELSE_CLAUSE); +fn at_cursor_decl(p: &Parser) -> bool { + at_name(p) && (p.nth_at(1, CURSOR_KW) || p.nth_at(1, SCROLL_KW) || p.nth_at(1, NO_KW)) } -// TODO: remove this once we get all the ast nodes working -fn temp_unknown(p: &mut Parser, message: &str) { - let m = p.start(); - p.error(format!("{message}, got {:?}", p.current())); - skip_to_stmt_end(p); - p.eat(SEMICOLON); - m.complete(p, ERROR); +fn at_percent_datatype(p: &Parser) -> bool { + let Some(n) = at_path(p) else { + return false; + }; + p.nth_at(n, PERCENT) && (p.nth_at(n + 1, TYPE_KW) || p.nth_at_contextual_kw(n + 1, ROWTYPE_KW)) } -fn skip_to_stmt_end(p: &mut Parser) { - let mut depth = 0; - while !p.at(EOF) { - if depth == 0 && p.at(SEMICOLON) { - break; +fn at_path(p: &Parser) -> Option { + let mut n = 0; + while !p.nth_at(n, EOF) && nth_at_name(p, n) { + let unicode_ident = p.nth_at(n, IDENT); + n += 1; + if unicode_ident && p.nth_at(n, UESCAPE_KW) && p.nth_at(n + 1, STRING) { + n += 2; } - if p.at(BEGIN_KW) { - depth += 1; - } else if p.at(END_KW) && depth > 0 { - depth -= 1; + if !p.nth_at(n, DOT) { + return Some(n); } - p.bump_any(); - } -} - -fn opt_exception_section(p: &mut Parser) { - if !p.at_contextual_kw(EXCEPTION_KW) { - return; - } - let m = p.start(); - p.bump_remap(EXCEPTION_KW); - while !p.at(EOF) && p.at(WHEN_KW) { - exception_handler(p); - } - m.complete(p, PLPGSQL_EXCEPTION_SECTION); -} - -fn exception_handler(p: &mut Parser) { - assert!(p.at(WHEN_KW)); - let m = p.start(); - // TODO: use delimited - p.bump(WHEN_KW); - condition(p); - while !p.at(EOF) && p.eat(OR_KW) { - condition(p); - } - p.expect(THEN_KW); - body(p, BodyKind::ExceptionHandler); - m.complete(p, PLPGSQL_EXCEPTION_HANDLER); -} - -fn condition(p: &mut Parser) { - let m = p.start(); - if p.at_contextual_kw(SQLSTATE_KW) { - p.bump_remap(SQLSTATE_KW); - p.expect(STRING); - } else if at_name(p) { - p.bump_any(); - } else { - p.error(format!("expected a condition name, got {:?}", p.current())); + n += 1; } - m.complete(p, PLPGSQL_CONDITION); -} - -fn at_alias_decl(p: &Parser) -> bool { - at_name(p) && p.nth_at_contextual_kw(1, ALIAS_KW) -} - -fn at_cursor_decl(p: &Parser) -> bool { - at_name(p) && (p.nth_at(1, CURSOR_KW) || p.nth_at(1, SCROLL_KW) || p.nth_at(1, NO_KW)) + None } -fn at_var_decl(p: &Parser) -> bool { - at_name(p) -} +const BLOCK_FIRST: TokenSet = TokenSet::new(&[BEGIN_KW, DECLARE_KW]); fn at_block_start(p: &Parser) -> bool { p.at_ts(BLOCK_FIRST) || at_block_label(p) @@ -1249,11 +1253,11 @@ fn at_loop_kw(p: &Parser, n: usize) -> bool { const NOT_A_CURSOR_NAME: TokenSet = TokenSet::new(&[SELECT_KW, VALUES_KW]); -fn at_for_cursor(p: &Parser, n: usize) -> bool { - if !nth_at_name(p, n) || p.nth_at_ts(n, NOT_A_CURSOR_NAME) { +fn at_for_cursor(p: &Parser) -> bool { + if !at_name(p) || p.at_ts(NOT_A_CURSOR_NAME) { return false; } - let Some(n) = cursor_args_end(p, n + 1) else { + let Some(n) = cursor_args_end(p, 1) else { return false; }; p.nth_at_contextual_kw(n, LOOP_KW) @@ -1284,34 +1288,6 @@ fn at_for_reverse(p: &Parser, n: usize) -> bool { p.nth_at_contextual_kw(n, REVERSE_KW) && !p.nth_at(n + 1, DOT) } -// postgres does a similar thing to look ahead until it sees a loop token -fn top_level_terminator( - p: &Parser, - stop: TokenSet, - contextual_stop: TokenSet, -) -> Option<(SyntaxKind, usize)> { - let mut depth = 0i32; - let mut case_depth = 0i32; - let mut n = 0; - while !p.nth_at(n, EOF) { - match p.nth(n) { - L_PAREN | L_BRACK => depth += 1, - R_PAREN | R_BRACK if depth > 0 => depth -= 1, - CASE_KW => case_depth += 1, - END_KW if case_depth > 0 => case_depth -= 1, - kind if depth == 0 && case_depth == 0 && stop.contains(kind) => { - return Some((kind, n)); - } - _ if depth == 0 && case_depth == 0 && p.nth_at_contextual_ts(n, contextual_stop) => { - return Some((p.nth_contextual_kind(n), n)); - } - _ => (), - } - n += 1; - } - None -} - fn at_exec_sql_stmt(p: &Parser) -> bool { if !at_name(p) || at_assign_op(p) { return false; @@ -1405,6 +1381,14 @@ fn bump_maybe_contextual_kw(p: &mut Parser, kw: SyntaxKind) { } } +fn expect_contextual_kw(p: &mut Parser, kw: SyntaxKind) { + if p.at_contextual_kw(kw) { + p.bump_remap(kw); + } else { + p.error(format!("expected {kw:?}")); + } +} + fn at_elsif(p: &Parser) -> bool { p.at_contextual_kw(ELSIF_KW) || p.at_contextual_kw(ELSEIF_KW) } @@ -1442,3 +1426,26 @@ fn at_block_end(p: &Parser) -> bool { } !p.nth_at(1, IF_KW) && !p.nth_at(1, CASE_KW) && !p.nth_at_contextual_kw(1, LOOP_KW) } + +fn err_recover_stmt(p: &mut Parser, message: &str, at_end: impl Fn(&Parser) -> bool) { + let m = p.start(); + p.error(format!("{message}, got {:?}", p.current())); + skip_to_stmt_end(p, at_end); + p.eat(SEMICOLON); + m.complete(p, ERROR); +} + +fn skip_to_stmt_end(p: &mut Parser, at_end: impl Fn(&Parser) -> bool) { + let mut depth = 0; + while !p.at(EOF) { + if depth == 0 && (p.at(SEMICOLON) || at_end(p)) { + break; + } + if p.at(BEGIN_KW) || p.at(CASE_KW) { + depth += 1; + } else if p.at(END_KW) && depth > 0 { + depth -= 1; + } + p.bump_any(); + } +} diff --git a/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_comp_option_err.snap b/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_comp_option_err.snap index f84e38da..cbf120a3 100644 --- a/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_comp_option_err.snap +++ b/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_comp_option_err.snap @@ -8,11 +8,13 @@ PLPGSQL@0..25 POUND@1..2 "#" OPTION_KW@2..8 "option" WHITESPACE@8..9 " " - ERROR@9..24 + ERROR@9..14 IDENT@9..14 "dumpp" - WHITESPACE@14..15 "\n" + WHITESPACE@14..15 "\n" + PLPGSQL_BLOCK@15..24 BEGIN_KW@15..20 "begin" WHITESPACE@20..21 " " + PLPGSQL_BODY@21..21 END_KW@21..24 "end" WHITESPACE@24..25 " " --- @@ -20,13 +22,15 @@ PLPGSQL@0..20 WHITESPACE@0..1 "\n" ERROR@1..2 POUND@1..2 "#" - ERROR@2..19 + ERROR@2..9 IDENT@2..5 "foo" WHITESPACE@5..6 " " IDENT@6..9 "bar" - WHITESPACE@9..10 "\n" + WHITESPACE@9..10 "\n" + PLPGSQL_BLOCK@10..19 BEGIN_KW@10..15 "begin" WHITESPACE@15..16 " " + PLPGSQL_BODY@16..16 END_KW@16..19 "end" WHITESPACE@19..20 " " --- @@ -49,11 +53,13 @@ PLPGSQL@0..42 POUND@1..2 "#" VARIABLE_CONFLICT_KW@2..19 "variable_conflict" WHITESPACE@19..20 " " - ERROR@20..41 + ERROR@20..31 IDENT@20..31 "use_default" - WHITESPACE@31..32 "\n" + WHITESPACE@31..32 "\n" + PLPGSQL_BLOCK@32..41 BEGIN_KW@32..37 "begin" WHITESPACE@37..38 " " + PLPGSQL_BODY@38..38 END_KW@38..41 "end" WHITESPACE@41..42 " " --- @@ -63,11 +69,13 @@ PLPGSQL@0..34 POUND@1..2 "#" PRINT_STRICT_PARAMS_KW@2..21 "print_strict_params" WHITESPACE@21..22 " " - ERROR@22..33 + ERROR@22..23 INT_NUMBER@22..23 "1" - WHITESPACE@23..24 "\n" + WHITESPACE@23..24 "\n" + PLPGSQL_BLOCK@24..33 BEGIN_KW@24..29 "begin" WHITESPACE@29..30 " " + PLPGSQL_BODY@30..30 END_KW@30..33 "end" WHITESPACE@33..34 " " --- @@ -109,18 +117,20 @@ PLPGSQL@0..37 POUND@1..2 "#" PRINT_STRICT_PARAMS_KW@2..21 "print_strict_params" WHITESPACE@21..22 " " - ERROR@22..36 + ERROR@22..26 IDENT@22..26 "loop" - WHITESPACE@26..27 "\n" + WHITESPACE@26..27 "\n" + PLPGSQL_BLOCK@27..36 BEGIN_KW@27..32 "begin" WHITESPACE@32..33 " " + PLPGSQL_BODY@33..33 END_KW@33..36 "end" WHITESPACE@36..37 " " --- PLPGSQL@0..39 WHITESPACE@0..1 "\n" - PLPGSQL_BLOCK@1..39 - PLPGSQL_DECLARE_SECTION@1..38 + PLPGSQL_BLOCK@1..38 + PLPGSQL_DECLARE_SECTION@1..28 DECLARE_KW@1..8 "declare" WHITESPACE@8..9 " " PLPGSQL_VAR_DECL@9..15 @@ -133,17 +143,17 @@ PLPGSQL@0..39 INT_KW@11..14 "int" SEMICOLON@14..15 ";" WHITESPACE@15..16 "\n" - ERROR@16..38 + ERROR@16..28 POUND@16..17 "#" OPTION_KW@17..23 "option" WHITESPACE@23..24 " " IDENT@24..28 "dump" - WHITESPACE@28..29 "\n" - BEGIN_KW@29..34 "begin" - WHITESPACE@34..35 " " - END_KW@35..38 "end" - WHITESPACE@38..39 " " - PLPGSQL_BODY@39..39 + WHITESPACE@28..29 "\n" + BEGIN_KW@29..34 "begin" + WHITESPACE@34..35 " " + PLPGSQL_BODY@35..35 + END_KW@35..38 "end" + WHITESPACE@38..39 " " error[syntax-error]: expected DUMP_KW ╭▸ @@ -201,11 +211,3 @@ error[syntax-error]: expected a declaration, got POUND ╭▸ 40 │ #option dump ╰╴━ -error[syntax-error]: expected BEGIN_KW - ╭▸ -41 │ begin end $$; - ╰╴ ━ -error[syntax-error]: expected END_KW - ╭▸ -41 │ begin end $$; - ╰╴ ━ diff --git a/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_loop_stmt_err.snap b/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_loop_stmt_err.snap index 464e2252..61cc649c 100644 --- a/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_loop_stmt_err.snap +++ b/crates/squawk_syntax/src/snapshots/squawk_syntax__test__plpgsql_loop_stmt_err.snap @@ -7,7 +7,7 @@ PLPGSQL@0..114 PLPGSQL_BLOCK@1..113 BEGIN_KW@1..6 "begin" WHITESPACE@6..9 "\n " - PLPGSQL_BODY@9..113 + PLPGSQL_BODY@9..109 PLPGSQL_LOOP_STMT@9..30 LOOP_KW@9..13 "loop" WHITESPACE@13..18 "\n " @@ -61,10 +61,10 @@ PLPGSQL@0..114 PLPGSQL_LABEL_NAME_REF@100..104 IDENT@100..104 "exit" WHITESPACE@104..105 " " - ERROR@105..113 + ERROR@105..109 WHEN_KW@105..109 "when" - WHITESPACE@109..110 "\n" - END_KW@110..113 "end" + WHITESPACE@109..110 "\n" + END_KW@110..113 "end" WHITESPACE@113..114 "\n" error[syntax-error]: expected LOOP_KW @@ -87,7 +87,3 @@ error[syntax-error]: expected a statement, got WHEN_KW ╭▸ 15 │ exit when ╰╴ ━ -error[syntax-error]: expected END_KW - ╭▸ -16 │ end - ╰╴ ━