fix: allow multiline parenthesized expressions in every context
This commit is contained in:
@@ -37,9 +37,9 @@ a recursive example is in [programs/factorial.hon](programs/factorial.hon); `./b
|
||||
|
||||
an iterative example is in [programs/iterative_factorial.hon](programs/iterative_factorial.hon); `./build/honey run programs/iterative_factorial.hon` also prints `120`.
|
||||
|
||||
the parser supports arithmetic expressions with nested parenthesized groups, such as `a + (b - c) * 2 / 3`. groups control precedence without adding AST nodes; inner expression spans exclude the grouping parentheses. parsing accepts integer division syntax, but type checking requires float operands for `/`.
|
||||
the parser supports arithmetic expressions with nested parenthesized groups, such as `a + (b - c) * 2 / 3`. groups control precedence without adding AST nodes; inner expression spans exclude the grouping parentheses. groups may span lines in any expression position, including returns, initializers, arguments and guards. parsing accepts integer division syntax, but type checking requires float operands for `/`.
|
||||
|
||||
function calls are postfix expressions: `f()`, `f(a, b)`, and `factory()(x)`. calls bind tighter than unary and binary arithmetic and can appear in initializers or as expression statements inside function bodies. argument lists allow a trailing comma; newlines remain statement boundaries. type checking validates callable values, argument counts and argument types.
|
||||
function calls are postfix expressions: `f()`, `f(a, b)`, and `factory()(x)`. calls bind tighter than unary and binary arithmetic and can appear in initializers or as expression statements inside function bodies. argument lists allow newlines and a trailing comma; outside parentheses, newlines remain statement boundaries. type checking validates callable values, argument counts and argument types.
|
||||
|
||||
`true` and `false` are boolean literals; `bool` is available for parameters, storage and results. `==`, `!=`, `<`, `<=`, `>` and `>=` compare compatible numeric operands and produce `bool`; `==` and `!=` also accept boolean operands. comparisons preserve contextual numeric inference without making their operands boolean. arithmetic binds tighter than relational comparisons, which bind tighter than equality; comparisons associate left to right. boolean arithmetic/ordering and string/function/void comparisons are invalid; there is no implicit numeric-to-boolean conversion.
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ State :: struct {
|
||||
tokens []lexer.Token
|
||||
allocator mem.Allocator
|
||||
next_token usize
|
||||
paren_depth usize = 0
|
||||
}
|
||||
|
||||
#! borrows `tokens` and `source` while parsing; the returned Tree borrows `source` until ast.deinit.
|
||||
@@ -413,6 +414,7 @@ parse_expr proc(state @mut State) ast.NodeId ! ParseError {
|
||||
|
||||
@hide:file
|
||||
parse_expr_bp proc(state @mut State, min_bp BindingPowerScalar) ast.NodeId ! ParseError {
|
||||
skip_operand_newlines(state)
|
||||
token := current(state)
|
||||
left := if (prefix_binding_power(token.kind)) |right_bp| {
|
||||
state.next_token += 1
|
||||
@@ -430,6 +432,7 @@ parse_expr_bp proc(state @mut State, min_bp BindingPowerScalar) ast.NodeId ! Par
|
||||
}
|
||||
|
||||
while true {
|
||||
skip_continuation_newlines(state)
|
||||
token = current(state)
|
||||
if (token.kind == .open_paren) {
|
||||
left = try parse_call(state, left)
|
||||
@@ -458,6 +461,9 @@ parse_expr_bp proc(state @mut State, min_bp BindingPowerScalar) ast.NodeId ! Par
|
||||
@hide:file
|
||||
parse_call proc(state @mut State, callee ast.NodeId) ast.NodeId ! ParseError {
|
||||
_ = try consume(state, .open_paren)
|
||||
state.paren_depth += 1
|
||||
defer state.paren_depth -= 1
|
||||
skip_operand_newlines(state)
|
||||
arguments arraylist.ArrayList(ast.NodeId) := arraylist.init(state.allocator)
|
||||
defer arraylist.deinit(&arguments)
|
||||
|
||||
@@ -466,6 +472,7 @@ parse_call proc(state @mut State, callee ast.NodeId) ast.NodeId ! ParseError {
|
||||
try arraylist.append(&arguments, argument)
|
||||
if (!next_is(state, .comma)) break
|
||||
state.next_token += 1
|
||||
skip_operand_newlines(state)
|
||||
}
|
||||
close :: try consume(state, .close_paren)
|
||||
return try ast.add_call(
|
||||
@@ -485,6 +492,8 @@ parse_primary proc(state @mut State) ast.NodeId ! ParseError {
|
||||
match token.kind {
|
||||
.open_paren: {
|
||||
state.next_token += 1
|
||||
state.paren_depth += 1
|
||||
defer state.paren_depth -= 1
|
||||
expression :: try parse_expr(state)
|
||||
if next_is(state, .close_paren) {
|
||||
state.next_token += 1
|
||||
@@ -553,6 +562,38 @@ is_expression_recovery_boundary proc(kind lexer.TokenKind) bool {
|
||||
)
|
||||
}
|
||||
|
||||
@hide:file
|
||||
skip_operand_newlines proc(state @mut State) void {
|
||||
if (state.paren_depth == 0 or !next_is(state, .newline)) return
|
||||
start :: state.next_token
|
||||
skip_newlines(state)
|
||||
kind :: current(state).kind
|
||||
if (kind != .close_paren and is_expression_boundary(kind)) state.next_token = start
|
||||
# a missing operand must not consume the declaration or assignment on the next line.
|
||||
if (
|
||||
next_is(state, .ident)
|
||||
and (
|
||||
next_n_is(state, 1, .double_colon)
|
||||
or next_n_is(state, 1, .colon_equal)
|
||||
or next_n_is(state, 1, .equal)
|
||||
or next_n_is(state, 1, .ident)
|
||||
or next_n_is(state, 1, .proc)
|
||||
)
|
||||
) state.next_token = start
|
||||
}
|
||||
|
||||
@hide:file
|
||||
skip_continuation_newlines proc(state @mut State) void {
|
||||
if (state.paren_depth == 0 or !next_is(state, .newline)) return
|
||||
start :: state.next_token
|
||||
skip_newlines(state)
|
||||
kind :: current(state).kind
|
||||
if (
|
||||
kind != .close_paren and kind != .comma and kind != .open_paren
|
||||
and infix_binding_power(kind) == null
|
||||
) state.next_token = start
|
||||
}
|
||||
|
||||
@hide:file
|
||||
recover_decl proc(
|
||||
state @mut State,
|
||||
|
||||
@@ -813,14 +813,42 @@ parses_logical_expressions_in_all_expression_positions test {
|
||||
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&diagnostic_store)
|
||||
source ::
|
||||
`global :: false and absent
|
||||
`global :: (
|
||||
` false
|
||||
` and absent
|
||||
`)
|
||||
`main proc() bool {
|
||||
` value := true or absent
|
||||
` value = !value and ready()
|
||||
` value := (
|
||||
` true
|
||||
` or absent
|
||||
` )
|
||||
` value = (
|
||||
` !
|
||||
` value
|
||||
` and ready(
|
||||
` )
|
||||
` )
|
||||
` !value
|
||||
` if !value or ready() { value = false }
|
||||
` while value and !done() { value = false }
|
||||
` return choose(value or false, nested(!value and true))
|
||||
` if (
|
||||
` !value
|
||||
` or ready()
|
||||
` ) { value = false }
|
||||
` while (
|
||||
` value
|
||||
` and !done()
|
||||
` ) { value = false }
|
||||
` return (
|
||||
` choose(
|
||||
` (
|
||||
` value
|
||||
` or false
|
||||
` ),
|
||||
` nested(
|
||||
` !value
|
||||
` and true
|
||||
` ),
|
||||
` )
|
||||
` )
|
||||
`}
|
||||
scan_state := lexer.init(mem.c_allocator)
|
||||
defer lexer.deinit(&scan_state)
|
||||
@@ -854,6 +882,40 @@ parses_logical_expressions_in_all_expression_positions test {
|
||||
try testing.expect_equal(ast.BinaryOperator.logical_and, ast.binary_expr(&tree, nested_argument).operator)
|
||||
}
|
||||
|
||||
multiline_group_errors_preserve_statement_boundaries test {
|
||||
strpool.strings = strpool.init(mem.c_allocator)
|
||||
defer strpool.deinit(&strpool.strings)
|
||||
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&diagnostic_store)
|
||||
source ::
|
||||
`main proc() void {
|
||||
` value := (
|
||||
` 1 +
|
||||
` next int :: 2
|
||||
` value = 3
|
||||
` return (
|
||||
` true
|
||||
`}
|
||||
`tail :: 9
|
||||
scan_state := lexer.init(mem.c_allocator)
|
||||
defer lexer.deinit(&scan_state)
|
||||
try lexer.scan(&scan_state, source, &diagnostic_store)
|
||||
tree := try parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store)
|
||||
defer ast.deinit(&tree)
|
||||
|
||||
try testing.expect_equal(3, diagnostics.count(&diagnostic_store))
|
||||
try testing.expect_equal(2, ast.package_decl_count(&tree))
|
||||
body :: ast.function_decl(&tree, ast.package_decl(&tree, 0)).body
|
||||
try testing.expect_equal(4, ast.block_statement_count(&tree, body))
|
||||
next :: ast.statement_decl(&tree, ast.block_statement(&tree, body, 1))
|
||||
try testing.expect_equal("next", ast.source_text(&tree, next.name))
|
||||
assignment :: ast.assignment_stmt(&tree, ast.block_statement(&tree, body, 2))
|
||||
try testing.expect_equal("3", ast.source_text(&tree, ast.node_span(&tree, assignment.value)))
|
||||
try testing.expect_equal(ast.StmtKind.return_stmt, ast.stmt_kind(&tree, ast.block_statement(&tree, body, 3)))
|
||||
tail :: ast.statement_decl(&tree, ast.package_decl(&tree, 1))
|
||||
try testing.expect_equal("tail", ast.source_text(&tree, tail.name))
|
||||
}
|
||||
|
||||
recovers_missing_logical_operands_and_preserves_enclosing_nodes test {
|
||||
strpool.strings = strpool.init(mem.c_allocator)
|
||||
defer strpool.deinit(&strpool.strings)
|
||||
|
||||
Reference in New Issue
Block a user