feat: execute Bro-compatible integer range loops
This commit is contained in:
+127
-1
@@ -185,6 +185,7 @@ parse_statement proc(state @mut State, allow_else bool) ast.NodeId ! ParseError
|
||||
match current(state).kind {
|
||||
.if: return try parse_if(state)
|
||||
.while: return try parse_while(state, allow_else)
|
||||
.for: return try parse_for(state, allow_else)
|
||||
.return: return try parse_return(state, allow_else)
|
||||
.break, .continue: return try parse_loop_control(state, allow_else)
|
||||
.ident, .int, .float, .string, .true, .false, .minus, .bang, .open_paren: {
|
||||
@@ -302,6 +303,92 @@ parse_while proc(state @mut State, allow_else bool) ast.NodeId ! ParseError {
|
||||
)
|
||||
}
|
||||
|
||||
@hide:file
|
||||
CaptureList :: struct {
|
||||
item ?ast.NodeId = null
|
||||
index ?ast.NodeId = null
|
||||
pointer bool = false
|
||||
indexed bool = false
|
||||
}
|
||||
|
||||
@hide:file
|
||||
parse_capture_name proc(state @mut State) ?ast.NodeId ! ParseError {
|
||||
name :: try consume(state, .ident)
|
||||
capture ?ast.NodeId := null
|
||||
if (
|
||||
name.span.end != name.span.start + 1
|
||||
or state.builder.source[usize(name.span.start)] != '_'
|
||||
) capture = try ast.add_identifier(&state.builder, name.span)
|
||||
return capture
|
||||
}
|
||||
|
||||
@hide:file
|
||||
parse_captures proc(state @mut State) CaptureList ! ParseError {
|
||||
_ = try consume(state, .pipe)
|
||||
captures := CaptureList{}
|
||||
if next_is(state, .at) {
|
||||
captures.pointer = true
|
||||
state.next_token += 1
|
||||
}
|
||||
captures.item = try parse_capture_name(state)
|
||||
if next_is(state, .comma) {
|
||||
captures.indexed = true
|
||||
state.next_token += 1
|
||||
captures.index = try parse_capture_name(state)
|
||||
}
|
||||
_ = try consume(state, .pipe)
|
||||
return captures
|
||||
}
|
||||
|
||||
@hide:file
|
||||
parse_for proc(state @mut State, allow_else bool) ast.NodeId ! ParseError {
|
||||
keyword :: try consume(state, .for)
|
||||
skip_newlines(state)
|
||||
iterable_start :: state.next_token
|
||||
iterable := try parse_expr(state)
|
||||
grouped :: parenthesized_guard(state, iterable_start)
|
||||
skip_newlines(state)
|
||||
captures :: parse_captures(state) catch |err| {
|
||||
match err {
|
||||
.unexpected_token: {}
|
||||
else: return err
|
||||
}
|
||||
diagnostic :: try diagnostics.add(state.diagnostics, .unexpected_token, current(state).span)
|
||||
iterable = try ast.add_invalid_expr(
|
||||
&state.builder, ast.builder_span(&state.builder, iterable), diagnostic,
|
||||
)
|
||||
while (
|
||||
!next_is(state, .open_curly) and !next_is(state, .close_curly)
|
||||
and !next_is(state, .newline) and !next_is(state, .eof)
|
||||
and !next_is(state, .else)
|
||||
) state.next_token += 1
|
||||
yield CaptureList{}
|
||||
}
|
||||
after_captures :: state.next_token
|
||||
skip_newlines(state)
|
||||
if (
|
||||
!next_is(state, .open_curly) and !grouped
|
||||
and ast.builder_expr_kind(&state.builder, iterable) != .call
|
||||
and ast.builder_expr_kind(&state.builder, iterable) != .invalid
|
||||
) {
|
||||
diagnostic :: try diagnostics.add(state.diagnostics, .unexpected_token, current(state).span)
|
||||
iterable = try ast.add_invalid_expr(
|
||||
&state.builder, ast.builder_span(&state.builder, iterable), diagnostic,
|
||||
)
|
||||
}
|
||||
# a malformed capture list must not swallow the next line's sibling.
|
||||
if (
|
||||
ast.builder_expr_kind(&state.builder, iterable) == .invalid
|
||||
and !next_is(state, .open_curly)
|
||||
) state.next_token = after_captures
|
||||
body :: try parse_arm(state, allow_else)
|
||||
return try ast.add_for_stmt(
|
||||
&state.builder,
|
||||
ast.Span{ start = keyword.span.start, end = ast.builder_span(&state.builder, body).end },
|
||||
iterable, captures.item, captures.index, captures.pointer, captures.indexed, body,
|
||||
)
|
||||
}
|
||||
|
||||
@hide:file
|
||||
parse_guard proc(state @mut State) ast.NodeId ! ParseError {
|
||||
condition_start :: state.next_token
|
||||
@@ -320,6 +407,7 @@ parse_guard proc(state @mut State) ast.NodeId ! ParseError {
|
||||
and !next_is(state, .newline) and !next_is(state, .eof)
|
||||
and !next_is(state, .else) and !next_is(state, .return)
|
||||
and !next_is(state, .if) and !next_is(state, .while)
|
||||
and !next_is(state, .for)
|
||||
and !next_is(state, .break) and !next_is(state, .continue)
|
||||
) {
|
||||
if (next_is(state, .ident) and next_n_is(state, 1, .equal)) break
|
||||
@@ -435,6 +523,7 @@ parse_expr proc(state @mut State) ast.NodeId ! ParseError {
|
||||
parse_expr_bp proc(state @mut State, min_bp BindingPowerScalar) ast.NodeId ! ParseError {
|
||||
skip_operand_newlines(state)
|
||||
token := current(state)
|
||||
expression_start :: state.next_token
|
||||
left := if (prefix_binding_power(token.kind)) |right_bp| {
|
||||
state.next_token += 1
|
||||
operand :: try parse_expr_bp(state, right_bp)
|
||||
@@ -449,12 +538,32 @@ parse_expr_bp proc(state @mut State, min_bp BindingPowerScalar) ast.NodeId ! Par
|
||||
} else {
|
||||
yield try parse_primary(state)
|
||||
}
|
||||
left_grouped := parenthesized_guard(state, expression_start)
|
||||
|
||||
while true {
|
||||
skip_continuation_newlines(state)
|
||||
token = current(state)
|
||||
if (token.kind == .open_paren) {
|
||||
left = try parse_call(state, left)
|
||||
left_grouped = false
|
||||
continue
|
||||
}
|
||||
if (token.kind == .range or token.kind == .range_inclusive) {
|
||||
if (min_bp > BindingPowerScalar(0)) break
|
||||
left = try validate_range_bound(state, left, left_grouped)
|
||||
state.next_token += 1
|
||||
right_start :: state.next_token
|
||||
right := try parse_expr_bp(state, BindingPowerScalar(1))
|
||||
right = try validate_range_bound(state, right, parenthesized_guard(state, right_start))
|
||||
left = try ast.add_range(
|
||||
&state.builder,
|
||||
ast.Span{
|
||||
start = state.tokens[expression_start].span.start,
|
||||
end = state.tokens[state.next_token - 1].span.end,
|
||||
},
|
||||
left, right, token.kind == .range_inclusive,
|
||||
)
|
||||
left_grouped = false
|
||||
continue
|
||||
}
|
||||
if (infix_binding_power(token.kind)) |power| {
|
||||
@@ -470,6 +579,7 @@ parse_expr_bp proc(state @mut State, min_bp BindingPowerScalar) ast.NodeId ! Par
|
||||
left,
|
||||
right,
|
||||
)
|
||||
left_grouped = false
|
||||
continue
|
||||
}
|
||||
break
|
||||
@@ -477,6 +587,21 @@ parse_expr_bp proc(state @mut State, min_bp BindingPowerScalar) ast.NodeId ! Par
|
||||
return left
|
||||
}
|
||||
|
||||
@hide:file
|
||||
validate_range_bound proc(
|
||||
state @mut State, bound ast.NodeId, grouped bool,
|
||||
) ast.NodeId ! (mem.AllocError | diagnostics.CapacityError) {
|
||||
if (grouped) return bound
|
||||
match ast.builder_expr_kind(&state.builder, bound) {
|
||||
.unary, .binary, .range: {
|
||||
span :: ast.builder_span(&state.builder, bound)
|
||||
diagnostic :: try diagnostics.add(state.diagnostics, .unexpected_token, span)
|
||||
return try ast.add_invalid_expr(&state.builder, span, diagnostic)
|
||||
}
|
||||
else: return bound
|
||||
}
|
||||
}
|
||||
|
||||
@hide:file
|
||||
parse_call proc(state @mut State, callee ast.NodeId) ast.NodeId ! ParseError {
|
||||
_ = try consume(state, .open_paren)
|
||||
@@ -566,7 +691,7 @@ parse_primary proc(state @mut State) ast.NodeId ! ParseError {
|
||||
@hide:file
|
||||
is_expression_boundary proc(kind lexer.TokenKind) bool {
|
||||
return match kind {
|
||||
.close_paren, .newline, .close_curly, .open_curly, .else, .return, .if, .while, .eof: true
|
||||
.close_paren, .newline, .close_curly, .open_curly, .else, .return, .if, .while, .for, .eof: true
|
||||
else: false
|
||||
}
|
||||
}
|
||||
@@ -586,6 +711,7 @@ is_expression_recovery_boundary proc(kind lexer.TokenKind) bool {
|
||||
kind == .comma
|
||||
or kind == .and
|
||||
or kind == .or
|
||||
or kind == .pipe
|
||||
or is_expression_boundary(kind)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import "@std/mem"
|
||||
import "@std/testing"
|
||||
import "@std/arraylist"
|
||||
|
||||
import "@source/diagnostics"
|
||||
import "@source/strpool"
|
||||
@@ -1083,3 +1084,119 @@ preserves_control_bodies_after_malformed_guards test {
|
||||
}
|
||||
try testing.expect_equal(ast.StmtKind.declaration, ast.stmt_kind(&tree, ast.block_statement(&tree, body, 3)))
|
||||
}
|
||||
|
||||
parses_range_endpoints_and_capture_shapes 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 ::
|
||||
`stored :: (1 + 2)..=(limit() * 2)
|
||||
`run proc() void {
|
||||
` for (-2)..1 |item| { item }
|
||||
` for (0..10) |_| continue
|
||||
` for 0..10 |@item, _| {}
|
||||
` for 0..10 |item, index| { index }
|
||||
` for
|
||||
` 0..10
|
||||
` |item|
|
||||
` { item }
|
||||
`}
|
||||
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(0, diagnostics.count(&diagnostic_store))
|
||||
stored :: ast.statement_decl(&tree, ast.package_decl(&tree, 0)).initializer
|
||||
interval :: ast.range_expr(&tree, stored)
|
||||
try testing.expect(interval.inclusive)
|
||||
try testing.expect_equal(ast.ExprKind.binary, ast.expr_kind(&tree, interval.start))
|
||||
try testing.expect_equal(ast.ExprKind.binary, ast.expr_kind(&tree, interval.end))
|
||||
body :: ast.function_decl(&tree, ast.package_decl(&tree, 1)).body
|
||||
try testing.expect_equal(5, ast.block_statement_count(&tree, body))
|
||||
first :: ast.for_stmt(&tree, ast.block_statement(&tree, body, 0))
|
||||
if (first.capture) |capture| {
|
||||
try testing.expect_equal(ast.ExprKind.identifier, ast.expr_kind(&tree, capture))
|
||||
try testing.expect_equal("item", ast.source_text(&tree, ast.node_span(&tree, capture)))
|
||||
} else return .expectation_failed
|
||||
second :: ast.for_stmt(&tree, ast.block_statement(&tree, body, 1))
|
||||
try testing.expect(second.capture == null)
|
||||
try testing.expect_equal(ast.StmtKind.continue_stmt, ast.stmt_kind(
|
||||
&tree, ast.block_statement(&tree, second.body, 0),
|
||||
))
|
||||
third :: ast.for_stmt(&tree, ast.block_statement(&tree, body, 2))
|
||||
try testing.expect(third.pointer_capture and third.index_capture and third.index == null)
|
||||
fourth :: ast.for_stmt(&tree, ast.block_statement(&tree, body, 3))
|
||||
try testing.expect(fourth.index_capture and fourth.index != null)
|
||||
}
|
||||
|
||||
rejects_bro_invalid_range_headers_without_losing_siblings test {
|
||||
strpool.strings = strpool.init(mem.c_allocator)
|
||||
defer strpool.deinit(&strpool.strings)
|
||||
cases [13][]u8 :: [
|
||||
"for -2..1 |i| {}",
|
||||
"for 0..limit+1 |i| {}",
|
||||
"for (0..limit+1) |i| {}",
|
||||
"for 0..10 |i| continue",
|
||||
"for (0)..10 |i| continue",
|
||||
"for 0..10 {}",
|
||||
"for 0..10 || {}",
|
||||
"for 0..10 |i int| {}",
|
||||
"for 0..10 |i,| {}",
|
||||
"for 0..10 |i, j, k| {}",
|
||||
"for 0..10 |i\n| {}",
|
||||
"for ..10 |i| {}",
|
||||
"for 0.. |i| {}",
|
||||
]
|
||||
for (cases) |header| {
|
||||
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&diagnostic_store)
|
||||
source arraylist.ArrayList(u8) := arraylist.init(mem.c_allocator)
|
||||
defer arraylist.deinit(&source)
|
||||
for ("run proc() void {\n") |byte| try arraylist.append(&source, byte)
|
||||
for (header) |byte| try arraylist.append(&source, byte)
|
||||
for ("\nkept :: 7\n}\nafter :: 9\n") |byte| try arraylist.append(&source, byte)
|
||||
scan_state := lexer.init(mem.c_allocator)
|
||||
defer lexer.deinit(&scan_state)
|
||||
try lexer.scan(&scan_state, source.items, &diagnostic_store)
|
||||
tree := try parse(mem.c_allocator, scan_state.tokens.items, source.items, &diagnostic_store)
|
||||
defer ast.deinit(&tree)
|
||||
try testing.expect(diagnostics.count(&diagnostic_store) > 0)
|
||||
try testing.expect_equal(2, ast.package_decl_count(&tree))
|
||||
try testing.expect_equal("after", ast.source_text(
|
||||
&tree, ast.statement_decl(&tree, ast.package_decl(&tree, 1)).name,
|
||||
))
|
||||
body :: ast.function_decl(&tree, ast.package_decl(&tree, 0)).body
|
||||
last :: ast.block_statement(&tree, body, ast.block_statement_count(&tree, body) - 1)
|
||||
try testing.expect_equal(ast.StmtKind.declaration, ast.stmt_kind(&tree, last))
|
||||
try testing.expect_equal("kept", ast.source_text(&tree, ast.statement_decl(&tree, last).name))
|
||||
}
|
||||
}
|
||||
|
||||
range_loop_preserves_nearest_else_and_canonical_body 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 ::
|
||||
`run proc() void {
|
||||
` if (true) for (0..2) |i| if (false) break else continue
|
||||
` for values() |item| return
|
||||
`}
|
||||
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(0, diagnostics.count(&diagnostic_store))
|
||||
body :: ast.function_decl(&tree, ast.package_decl(&tree, 0)).body
|
||||
outer :: ast.if_stmt(&tree, ast.block_statement(&tree, body, 0))
|
||||
try testing.expect(outer.else_block == null)
|
||||
loop :: ast.for_stmt(&tree, ast.block_statement(&tree, outer.then_block, 0))
|
||||
nested :: ast.if_stmt(&tree, ast.block_statement(&tree, loop.body, 0))
|
||||
try testing.expect(nested.else_block != null)
|
||||
call_loop :: ast.for_stmt(&tree, ast.block_statement(&tree, body, 1))
|
||||
try testing.expect_equal(ast.ExprKind.call, ast.expr_kind(&tree, call_loop.iterable))
|
||||
try testing.expect_equal(1, ast.block_statement_count(&tree, call_loop.body))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user