From 50a10bb404930d1d1b5ee9af5f451a7220ba6137 Mon Sep 17 00:00:00 2001 From: hl-valdemar Date: Fri, 2 Oct 2026 00:36:57 +0200 Subject: [PATCH] feat: parse function calls --- README.md | 2 + source/ast/ast.hon | 102 ++++++++++++++++++++++++++++--- source/ast/renderer/renderer.hon | 6 ++ source/main.hon | 1 + source/parser/parse.hon | 49 +++++++++++++-- source/parser/parse.test.hon | 100 ++++++++++++++++++++++++++++++ 6 files changed, 246 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 16d02da..3443a9d 100644 --- a/README.md +++ b/README.md @@ -9,3 +9,5 @@ bro build ``` 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. + +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. Name resolution and argument/type checking belong to semantic analysis. diff --git a/source/ast/ast.hon b/source/ast/ast.hon index d87488e..a0efcfd 100644 --- a/source/ast/ast.hon +++ b/source/ast/ast.hon @@ -2,9 +2,10 @@ #! * type ..... -> identifier #! * assignment -> :: | := #! * literal .. -> int | float | string -#! * expr ..... -> identifier | literal +#! * op ....... -> '+' | '-' | '*' | '/' +#! * expr ..... -> identifier | literal | '(' expr ')' | '-' expr | expr op expr | expr '(' [expr {',' expr} [',']] ')' #! * decl ..... -> identifier [type] assignment expr -#! * stmt ..... -> decl +#! * stmt ..... -> decl | expr import "@std/debug" import "@std/mem" @@ -20,13 +21,38 @@ NodeId :: distinct u32 @hide:file NO_NODE :: maxval!(NodeId) -LiteralKind :: enum { int, float, string } -ExprKind :: enum { literal_int, literal_float, literal_string, identifier, unary, binary, invalid } -DeclKind :: enum { statement, function, invalid } -StmtKind :: enum { declaration, invalid } -Mutability :: enum { constant, variable } +LiteralKind :: enum { + int, float, string +} + +ExprKind :: enum { + literal_int + literal_float + literal_string + identifier + unary + binary + call + invalid +} + +DeclKind :: enum { + statement, function, invalid +} + +StmtKind :: enum { + declaration, expression, invalid +} + +Mutability :: enum { + constant, variable +} + UnaryOperator :: enum { negate } -BinaryOperator :: enum { add, subtract, multiply, divide } + +BinaryOperator :: enum { + add, subtract, multiply, divide +} @hide:file NodeKind :: enum { @@ -36,6 +62,8 @@ NodeKind :: enum { expr_identifier, expr_unary, expr_binary, + expr_call, + stmt_expr, stmt_decl, func_decl, param_decl, @@ -116,6 +144,11 @@ BinaryExprView :: struct { right NodeId } +CallExprView :: struct { + span Span + callee NodeId +} + InvalidView :: struct { span Span diagnostic DiagnosticId @@ -197,6 +230,29 @@ add_binary proc( }, span) } +add_call proc( + builder @mut Builder, + span Span, + callee NodeId, + arguments []NodeId, +) NodeId ! mem.AllocError { + start :: builder.storage.extra.items.len + for (arguments) |argument| _ = try add_extra(builder, u32(argument)) + return try add_node(builder, Node{ + kind = .expr_call, + main = NodeData{ node_id = callee }, + data0 = NodeData{ extra = u32(start) }, + data1 = NodeData{ extra = u32(builder.storage.extra.items.len) }, + }, span) +} + +add_expression_stmt proc(builder @mut Builder, expression NodeId) NodeId ! mem.AllocError { + return try add_node(builder, Node{ + kind = .stmt_expr, + main = NodeData{ node_id = expression }, + }, builder_span(builder, expression)) +} + add_statement_decl proc( builder @mut Builder, span Span, @@ -314,11 +370,18 @@ decl_kind proc(tree @Tree, id NodeId) DeclKind { stmt_kind proc(tree @Tree, id NodeId) StmtKind { return match get_node(&tree.storage, id).kind { .stmt_decl: .declaration + .stmt_expr: .expression .invalid_stmt: .invalid else: unreachable } } +statement_expr proc(tree @Tree, id NodeId) NodeId { + node :: get_node(&tree.storage, id) + debug.assert(node.kind == .stmt_expr) + return node.main.node_id +} + statement_decl proc(tree @Tree, id NodeId) StatementDeclView { node :: get_node(&tree.storage, id) debug.assert(node.kind == .stmt_decl) @@ -389,6 +452,7 @@ expr_kind proc(tree @Tree, id NodeId) ExprKind { .expr_identifier: .identifier .expr_unary: .unary .expr_binary: .binary + .expr_call: .call .invalid_expr: .invalid else: unreachable } @@ -415,6 +479,28 @@ binary_expr proc(tree @Tree, id NodeId) BinaryExprView { } } +call_expr proc(tree @Tree, id NodeId) CallExprView { + node :: get_node(&tree.storage, id) + debug.assert(node.kind == .expr_call) + return CallExprView{ + span = get_span(&tree.storage, id), + callee = node.main.node_id, + } +} + +call_arg_count proc(tree @Tree, id NodeId) usize { + node :: get_node(&tree.storage, id) + debug.assert(node.kind == .expr_call) + return usize(node.data1.extra - node.data0.extra) +} + +call_arg proc(tree @Tree, id NodeId, index usize) NodeId { + node :: get_node(&tree.storage, id) + debug.assert(node.kind == .expr_call) + debug.assert(index < usize(node.data1.extra - node.data0.extra)) + return NodeId(tree.storage.extra.items[usize(node.data0.extra) + index]) +} + node_span proc(tree @Tree, id NodeId) Span { return get_span(&tree.storage, id) } invalid proc(tree @Tree, id NodeId) InvalidView { diff --git a/source/ast/renderer/renderer.hon b/source/ast/renderer/renderer.hon index 5f024ed..dcd99c2 100644 --- a/source/ast/renderer/renderer.hon +++ b/source/ast/renderer/renderer.hon @@ -41,6 +41,12 @@ render_function proc( statement :: ast.block_statement(tree, function.body, index) match ast.stmt_kind(tree, statement) { .declaration: try render_statement(renderer, tree, statement, " ") + .expression: { + expression :: ast.statement_expr(tree, statement) + try append_text(renderer, " expression ") + try append_text(renderer, ast.source_text(tree, ast.node_span(tree, expression))) + try append_text(renderer, "\n") + } .invalid: try append_text(renderer, " invalid statement\n") } } diff --git a/source/main.hon b/source/main.hon index d9f0687..7222c9a 100644 --- a/source/main.hon +++ b/source/main.hon @@ -33,6 +33,7 @@ program :: `empty proc() void {} `grouped proc(a, b T, c U) void { ` value int :: 1 + ` empty() `} main proc() void! { diff --git a/source/parser/parse.hon b/source/parser/parse.hon index c8f8fe8..2fa8c5f 100644 --- a/source/parser/parse.hon +++ b/source/parser/parse.hon @@ -167,8 +167,18 @@ parse_block proc(state @mut State) ast.NodeId ! ParseError { @hide:file parse_statement proc(state @mut State) ast.NodeId ! ParseError { - if next_is(state, .ident) return try parse_statement_decl(state) - return .unexpected_token + if (next_is(state, .ident) and ( + next_n_is(state, 1, .ident) or + next_n_is(state, 1, .double_colon) or + next_n_is(state, 1, .colon_equal) + )) return try parse_statement_decl(state) + match current(state).kind { + .ident, .int, .float, .string, .minus, .open_paren: { + expression :: try parse_expr(state) + return try ast.add_expression_stmt(&state.builder, expression) + } + else: return .unexpected_token + } } @hide:file @@ -202,12 +212,12 @@ parse_type proc(state @mut State) ast.NodeId ! ParseError { } @hide:file -parse_expr proc(state @mut State) ast.NodeId ! (mem.AllocError | diagnostics.CapacityError) { +parse_expr proc(state @mut State) ast.NodeId ! ParseError { return try parse_expr_bp(state, BindingPowerScalar(0)) } @hide:file -parse_expr_bp proc(state @mut State, min_bp BindingPowerScalar) ast.NodeId ! (mem.AllocError | diagnostics.CapacityError) { +parse_expr_bp proc(state @mut State, min_bp BindingPowerScalar) ast.NodeId ! ParseError { token := current(state) left := if (prefix_binding_power(token.kind)) |right_bp| { state.next_token += 1 @@ -225,6 +235,10 @@ parse_expr_bp proc(state @mut State, min_bp BindingPowerScalar) ast.NodeId ! (me while true { token = current(state) + if (token.kind == .open_paren) { + left = try parse_call(state, left) + continue + } if (infix_binding_power(token.kind)) |power| { if (power.left < min_bp) break state.next_token += 1 @@ -246,7 +260,30 @@ parse_expr_bp proc(state @mut State, min_bp BindingPowerScalar) ast.NodeId ! (me } @hide:file -parse_primary proc(state @mut State) ast.NodeId ! (mem.AllocError | diagnostics.CapacityError) { +parse_call proc(state @mut State, callee ast.NodeId) ast.NodeId ! ParseError { + _ = try consume(state, .open_paren) + arguments arraylist.ArrayList(ast.NodeId) := arraylist.init(state.allocator) + defer arraylist.deinit(&arguments) + + while !next_is(state, .close_paren) and !next_is(state, .newline) and + !next_is(state, .close_curly) and !next_is(state, .eof) + { + argument :: try parse_expr(state) + try arraylist.append(&arguments, argument) + if (!next_is(state, .comma)) break + state.next_token += 1 + } + close :: try consume(state, .close_paren) + return try ast.add_call( + &state.builder, + ast.Span{ start = ast.builder_span(&state.builder, callee).start, end = close.span.end }, + callee, + arguments.items, + ) +} + +@hide:file +parse_primary proc(state @mut State) ast.NodeId ! ParseError { token :: current(state) match token.kind { .open_paren: { @@ -284,7 +321,7 @@ parse_primary proc(state @mut State) ast.NodeId ! (mem.AllocError | diagnostics. else try diagnostics.add(state.diagnostics, .unexpected_token, token.span) - if (token.kind != .newline and token.kind != .close_paren and token.kind != .close_curly and token.kind != .eof) { + if (token.kind != .newline and token.kind != .comma and token.kind != .close_paren and token.kind != .close_curly and token.kind != .eof) { state.next_token += 1 } diff --git a/source/parser/parse.test.hon b/source/parser/parse.test.hon index 79c644d..de264fb 100644 --- a/source/parser/parse.test.hon +++ b/source/parser/parse.test.hon @@ -159,3 +159,103 @@ recovers_malformed_groups_and_preserves_following_statements test { try testing.expect_equal("after", ast.source_text(&tree, after.name)) try testing.expect_equal("4", ast.source_text(&tree, ast.node_span(&tree, after.initializer))) } + +parses_call_statements_and_postfix_precedence 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 { + ` ping() + ` result int := -factory()(add(1, 2 * 3), (4 + 5),) + 6 + ` (handler)(result) + `} + scan_state := lexer.init(mem.c_allocator) + defer lexer.deinit(&scan_state) + try lexer.scan(&scan_state, source, &diagnostic_store) + tree := try parse(scan_state.tokens.items, source, &diagnostic_store, mem.c_allocator) + defer ast.deinit(&tree) + + try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) + function :: ast.function_decl(&tree, ast.package_decl(&tree, 0)) + try testing.expect_equal(3, ast.block_statement_count(&tree, function.body)) + ping_stmt :: ast.block_statement(&tree, function.body, 0) + try testing.expect_equal(ast.StmtKind.expression, ast.stmt_kind(&tree, ping_stmt)) + ping :: ast.statement_expr(&tree, ping_stmt) + try testing.expect_equal(ast.ExprKind.call, ast.expr_kind(&tree, ping)) + try testing.expect_equal(0, ast.call_arg_count(&tree, ping)) + try testing.expect_equal("ping", ast.source_text(&tree, ast.node_span(&tree, ast.call_expr(&tree, ping).callee))) + try testing.expect_equal("ping()", ast.source_text(&tree, ast.call_expr(&tree, ping).span)) + + result :: ast.statement_decl(&tree, ast.block_statement(&tree, function.body, 1)) + try testing.expect_equal(ast.Mutability.variable, result.mutability) + add :: ast.binary_expr(&tree, result.initializer) + try testing.expect_equal(ast.BinaryOperator.add, add.operator) + try testing.expect_equal("6", ast.source_text(&tree, ast.node_span(&tree, add.right))) + negate :: ast.unary_expr(&tree, add.left) + call :: negate.operand + try testing.expect_equal(2, ast.call_arg_count(&tree, call)) + factory :: ast.call_expr(&tree, call).callee + try testing.expect_equal(0, ast.call_arg_count(&tree, factory)) + try testing.expect_equal("factory", ast.source_text(&tree, ast.node_span(&tree, ast.call_expr(&tree, factory).callee))) + nested :: ast.call_arg(&tree, call, 0) + try testing.expect_equal(2, ast.call_arg_count(&tree, nested)) + try testing.expect_equal("add", ast.source_text(&tree, ast.node_span(&tree, ast.call_expr(&tree, nested).callee))) + try testing.expect_equal("1", ast.source_text(&tree, ast.node_span(&tree, ast.call_arg(&tree, nested, 0)))) + multiply :: ast.binary_expr(&tree, ast.call_arg(&tree, nested, 1)) + try testing.expect_equal(ast.BinaryOperator.multiply, multiply.operator) + try testing.expect_equal("2", ast.source_text(&tree, ast.node_span(&tree, multiply.left))) + try testing.expect_equal("3", ast.source_text(&tree, ast.node_span(&tree, multiply.right))) + grouped :: ast.binary_expr(&tree, ast.call_arg(&tree, call, 1)) + try testing.expect_equal(ast.BinaryOperator.add, grouped.operator) + try testing.expect_equal("4", ast.source_text(&tree, ast.node_span(&tree, grouped.left))) + try testing.expect_equal("5", ast.source_text(&tree, ast.node_span(&tree, grouped.right))) + + handler :: ast.statement_expr(&tree, ast.block_statement(&tree, function.body, 2)) + try testing.expect_equal("handler", ast.source_text(&tree, ast.node_span(&tree, ast.call_expr(&tree, handler).callee))) + try testing.expect_equal(1, ast.call_arg_count(&tree, handler)) + try testing.expect_equal("result", ast.source_text(&tree, ast.node_span(&tree, ast.call_arg(&tree, handler, 0)))) +} + +recovers_malformed_calls_and_preserves_siblings 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 { + ` bad(1 2) + ` good() + ` missing(1 + ` after :: 3 + ` holes(, 2) + `} + `tail :: f() + scan_state := lexer.init(mem.c_allocator) + defer lexer.deinit(&scan_state) + try lexer.scan(&scan_state, source, &diagnostic_store) + tree := try parse(scan_state.tokens.items, source, &diagnostic_store, mem.c_allocator) + defer ast.deinit(&tree) + + try testing.expect_equal(3, diagnostics.count(&diagnostic_store)) + try testing.expect_equal(2, ast.package_decl_count(&tree)) + function :: ast.function_decl(&tree, ast.package_decl(&tree, 0)) + try testing.expect_equal(5, ast.block_statement_count(&tree, function.body)) + for ([0, 2]) |index| { + statement :: ast.block_statement(&tree, function.body, usize(index)) + try testing.expect_equal(ast.StmtKind.invalid, ast.stmt_kind(&tree, statement)) + try testing.expect(diagnostics.get(&diagnostic_store, ast.invalid(&tree, statement).diagnostic) != null) + } + good :: ast.statement_expr(&tree, ast.block_statement(&tree, function.body, 1)) + try testing.expect_equal("good", ast.source_text(&tree, ast.node_span(&tree, ast.call_expr(&tree, good).callee))) + after :: ast.statement_decl(&tree, ast.block_statement(&tree, function.body, 3)) + try testing.expect_equal("after", ast.source_text(&tree, after.name)) + holes :: ast.statement_expr(&tree, ast.block_statement(&tree, function.body, 4)) + try testing.expect_equal(2, ast.call_arg_count(&tree, holes)) + try testing.expect_equal(ast.ExprKind.invalid, ast.expr_kind(&tree, ast.call_arg(&tree, holes, 0))) + try testing.expect_equal("2", ast.source_text(&tree, ast.node_span(&tree, ast.call_arg(&tree, holes, 1)))) + tail :: ast.statement_decl(&tree, ast.package_decl(&tree, 1)) + try testing.expect_equal("tail", ast.source_text(&tree, tail.name)) + try testing.expect_equal(0, ast.call_arg_count(&tree, tail.initializer)) +}