while loops

This commit is contained in:
2026-06-21 23:26:11 +02:00
parent f4194492cc
commit 380b5943b3
12 changed files with 548 additions and 16 deletions
+9 -7
View File
@@ -12,11 +12,12 @@
### types and expressions ### types and expressions
- exact-width `i8` through `i64`, `u8` through `u64`, `f32`, `f64`, `isize`, `usize`, `void`, and inferred integer-constrained `int` - exact-width `i8` through `i64`, `u8` through `u64`, `f32`, `f64`, `isize`, `usize`, `bool`, `void`, and inferred integer-constrained `int`
- target-dependent atomic C primitives from `c_char` through `c_longdouble` - target-dependent atomic C primitives from `c_char` through `c_longdouble`
- C primitives remain semantically distinct from exact-width Brolang primitives until target lowering - C primitives remain semantically distinct from exact-width Brolang primitives until target lowering
- contextual integer literals and constant folding of addition and negation trees - contextual integer and character literals and constant folding of addition and negation trees
- strict numeric conversions, checked integer addition, and unary negation - strict numeric conversions, checked integer addition, and unary negation
- boolean literals, comparisons, unary `!`, and short-circuiting `and` / `or`
- arrays `[N]T`, sentinel arrays `[N;S]T`, single-item pointers `@T`, many-item pointers `*T`, sentinel many-item pointers `[*;S]T`, pointer offsets, slices, and explicit slicing - arrays `[N]T`, sentinel arrays `[N;S]T`, single-item pointers `@T`, many-item pointers `*T`, sentinel many-item pointers `[*;S]T`, pointer offsets, slices, and explicit slicing
- immutable UTF-8 string literals typed as pointers to static sentinel arrays: `@[N;0]u8` - immutable UTF-8 string literals typed as pointers to static sentinel arrays: `@[N;0]u8`
- pointer-to-array `.len`, indexing, and slicing without explicit dereference - pointer-to-array `.len`, indexing, and slicing without explicit dereference
@@ -24,10 +25,12 @@
- information-preserving and information-forgetting pointer-to-array decay and sentinel slice/pointer weakening; array values never implicitly decay - information-preserving and information-forgetting pointer-to-array decay and sentinel slice/pointer weakening; array values never implicitly decay
- narrow immutable zero-terminated byte pointer conversion to `*c_char` and `[*;0]c_char`, without general `u8`/`c_char` interchange - narrow immutable zero-terminated byte pointer conversion to `*c_char` and `[*;0]c_char`, without general `u8`/`c_char` interchange
- optionals with trapping postfix `?`, `orelse`, and nullable pointer representation - optionals with trapping postfix `?`, `orelse`, and nullable pointer representation
- conditional optional unwrapping with immutable then-block bindings: `if value |binding| { ... }`
- source-order native structs, defined or opaque `c_struct`, and keyed record literals - source-order native structs, defined or opaque `c_struct`, and keyed record literals
- complete plain imported C structs and unions as runtime values; incomplete or unsupported-layout records remain pointer-only - complete plain imported C structs and unions as runtime values; incomplete or unsupported-layout records remain pointer-only
- C function pointer types as pointer-sized runtime values, including manual `*c_func(...) T` spelling and nullable imported callback typedefs - C function pointer types as pointer-sized runtime values, including manual `*c_func(...) T` spelling and nullable imported callback typedefs
- postfix pointer dereference, general writable locations, function calls, assignments, and returns - postfix pointer dereference, general writable locations, function calls, assignments, and returns
- boolean `if` statements and `while` loops with optional post-iteration assignment/expression clauses
### functions and packages ### functions and packages
@@ -40,6 +43,9 @@
- file-local relative imports, aliases, and qualified member access - file-local relative imports, aliases, and qualified member access
- relative `.h` imports as synthetic package namespaces - relative `.h` imports as synthetic package namespaces
- transitive external C function prototypes, typedef chains, C scalars, fixed arrays, complete plain records/unions, and pointers to opaque C records - transitive external C function prototypes, typedef chains, C scalars, fixed arrays, complete plain records/unions, and pointers to opaque C records
- imported external C object variables, including writable globals and immutable arrays
- object-like scalar and plain record/union C macro constants
- supported static inline C functions through generated external wrappers
- bodyless manual and imported C variadic declarations with target-aware default argument promotions - bodyless manual and imported C variadic declarations with target-aware default argument promotions
- passing concrete `c_func` declarations/definitions as C callback values and calling non-null C function pointers with postfix call syntax - passing concrete `c_func` declarations/definitions as C callback values and calling non-null C function pointers with postfix call syntax
- reference-time diagnostics for unsupported imported C declarations - reference-time diagnostics for unsupported imported C declarations
@@ -59,13 +65,9 @@
### foreign functions and linking ### foreign functions and linking
- exporting brolang functions to c - exporting brolang functions to c
- additional target-specific C ABI lowering
### scalar and compound types ### scalar and compound types
- tuples and native variadic functions - tuples and native variadic functions
- C enums and non-plain C record layouts - C enums and non-plain C record layouts
### advanced c imports
- C enums, external variables, macros, and static inline functions
- additional target-specific C ABI lowering
+65 -3
View File
@@ -95,9 +95,12 @@
- new `Optional_Is_Some` / `Optional_Value` IR opcodes (the `Unwrap` presence-test + extract, minus the trap) - new `Optional_Is_Some` / `Optional_Value` IR opcodes (the `Unwrap` presence-test + extract, minus the trap)
- conditional unwrapping with guard clause: `if val |v : v >= 10| { ... } else { ... }` - unwrap `val` into `v` if it is not `none` - conditional unwrapping with guard clause: `if val |v : v >= 10| { ... } else { ... }` - unwrap `val` into `v` if it is not `none`
- multi-unwrap (see section below) - multi-unwrap (see section below)
- while loops (operates on boolean conditions). examples: - while loops (implemented; operates on boolean conditions). examples:
- `while condition { ... }` - iterate while the condition is true - `while condition { ... }` - iterate while the condition is true
- `while condition : i += 1 { ... }` - iterate while the condition is true and execute `i += 1` (continue expression) after each iteration - `while condition : i = i + 1 { ... }` - execute the update after each completed iteration
- the condition and update may be parenthesized independently for visual clarity
- update targets must already be declared and mutable; loops do not introduce implicit induction variables
- compound assignment (`+=`) remains deferred
- ranges (see section below) - ranges (see section below)
- for loops (operates on iterable sequences). examples: - for loops (operates on iterable sequences). examples:
- `for items |item| { ... }` - capture just the `item` value in the array/slice (uses copy semantics, i.e. gets a `T`) - `for items |item| { ... }` - capture just the `item` value in the array/slice (uses copy semantics, i.e. gets a `T`)
@@ -109,8 +112,11 @@
- `for 0..(len) |i| { ... }` or equivalently `for 0..=(len - 1) |i| { ... }` - calculating range bounds, expressions must be parenthesized - `for 0..(len) |i| { ... }` or equivalently `for 0..=(len - 1) |i| { ... }` - calculating range bounds, expressions must be parenthesized
- for all conditionals/guards, parentheses are optional but allowed for visual clarity - for all conditionals/guards, parentheses are optional but allowed for visual clarity
6. compound assignment 6. compound assignment: `+=`, `-=`, `*=`, `/=`
7. enums (native and c interop) (see below)
8. distinct types (see below)
## A word on multi-unwrap ## A word on multi-unwrap
@@ -175,3 +181,59 @@ Ranges represent a sequence of values, commonly used in for loops, and is itself
``` ```
This rule keeps the grammar simple and forces clarity at the call site — no precedence rules to remember. Also, being a value type, ranges can be assigned to variables and passed around like any other value. This rule keeps the grammar simple and forces clarity at the call site — no precedence rules to remember. Also, being a value type, ranges can be assigned to variables and passed around like any other value.
# A word on distinct types
Distinct types are considered distinct from their backing type. They do not implicitly coerce to their backing type.
```
# distinct type
UserID :: distinct u32
# instantiate distinct type
my_id UserId :: UserID(42) # value must be of to backing type
```
# A word on enums
```
# standard enums
Animal :: enum {
dog
cat
bird
lizard
}
# enums with backing type
Nat :: enum(u8) { # in this case, a maximum of 256 values are possible
one # default: implicitly starts from value 0
two
three
four
five
}
# enums with backing type with explicit associated values
# note: must not be jumbled (i.e. `first_val = 1` must come before `other_val = 2`), but is allowed to be discontiguous (i.e. `one = 1` can be followed by `three = 3` without `two = 2` in between)
Nat :: enum(u8) {
one = 1
two = 2
three = 3
# no four
five = 5
}
# enums with backing type with semi-implicit associated values
Nat :: enum(u8) {
one = 1 # starts from value 1
two # implicitly gets value 2
three # etc...
four
five
}
# using enums
dog_tag1 Animal :: Animal.dog
dog_tag2 Animal :: .dog # type inferred
```
+4
View File
@@ -120,6 +120,7 @@ Stmt_Kind :: enum u8 {
Return, Return,
Expression, Expression,
If, If,
While,
} }
Stmt :: struct { Stmt :: struct {
@@ -133,8 +134,11 @@ Stmt :: struct {
// `If` statements use `expr` as the condition, `body` as the then-block, and // `If` statements use `expr` as the condition, `body` as the then-block, and
// `else_body` as the else-block. An `else if` chain is represented as an // `else_body` as the else-block. An `else if` chain is represented as an
// `else_body` holding a single nested `If` statement. // `else_body` holding a single nested `If` statement.
// `While` statements use `expr` as the condition, `body` as the loop body,
// and `update` as the optional post-iteration statement.
body: []Stmt_Id, body: []Stmt_Id,
else_body: []Stmt_Id, else_body: []Stmt_Id,
update: Stmt_Id,
diagnostic: source.Diagnostic_Id, diagnostic: source.Diagnostic_Id,
} }
+70 -3
View File
@@ -631,6 +631,13 @@ mark_block_imports_used :: proc(checker: ^Checker, statements: []ast.Stmt_Id, fi
mark_expr_imports_used(checker, statement.expr, file) mark_expr_imports_used(checker, statement.expr, file)
mark_block_imports_used(checker, statement.body, file) mark_block_imports_used(checker, statement.body, file)
mark_block_imports_used(checker, statement.else_body, file) mark_block_imports_used(checker, statement.else_body, file)
case .While:
mark_expr_imports_used(checker, statement.expr, file)
mark_block_imports_used(checker, statement.body, file)
if statement.update != ast.INVALID_STMT {
update := [1]ast.Stmt_Id{statement.update}
mark_block_imports_used(checker, update[:], file)
}
case .Invalid: case .Invalid:
} }
} }
@@ -1424,6 +1431,13 @@ infer_statements :: proc(
infer_statements(checker, statement.body, locals, pkg, file, demanded, result) infer_statements(checker, statement.body, locals, pkg, file, demanded, result)
infer_statements(checker, statement.else_body, locals, pkg, file, demanded, result) infer_statements(checker, statement.else_body, locals, pkg, file, demanded, result)
} }
case .While:
_ = infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded)
infer_statements(checker, statement.body, locals, pkg, file, demanded, result)
if statement.update != ast.INVALID_STMT {
update := [1]ast.Stmt_Id{statement.update}
infer_statements(checker, update[:], locals, pkg, file, demanded, result)
}
} }
} }
resize(locals, scope_start) resize(locals, scope_start)
@@ -2460,7 +2474,13 @@ build_expr :: proc(
append(&stack, Build_Expr_Frame{expr=expr.left, expected=types.INVALID, template=ast.INVALID_FUNCTION}) append(&stack, Build_Expr_Frame{expr=expr.left, expected=types.INVALID, template=ast.INVALID_FUNCTION})
case .Add: case .Add:
stack[frame_index].stage = 1 stack[frame_index].stage = 1
append(&stack, Build_Expr_Frame{expr=expr.left, expected=types.INVALID, template=ast.INVALID_FUNCTION}) // Preserve assignment/return context for literal operands, e.g.
// assigning `i + 1` back into a `u32` local.
left_expected := types.INVALID
if types.is_concrete_scalar(frame.expected) && !types.is_bool(frame.expected) {
left_expected = frame.expected
}
append(&stack, Build_Expr_Frame{expr=expr.left, expected=left_expected, template=ast.INVALID_FUNCTION})
case .Call: case .Call:
if expr.left != ast.INVALID_EXPR { if expr.left != ast.INVALID_EXPR {
stack[frame_index].stage = 6 stack[frame_index].stage = 6
@@ -2600,6 +2620,12 @@ build_expr :: proc(
right_expected := types.INVALID right_expected := types.INVALID
if types.is_many_pointer(checker.module.exprs[last].type, &checker.module.types) { if types.is_many_pointer(checker.module.exprs[last].type, &checker.module.types) {
right_expected = types.USIZE right_expected = types.USIZE
} else if eval_constant(checker, expr.right).kind == .Value {
// A constant RHS adopts the concrete LHS type before numeric
// compatibility is checked.
right_expected = checker.module.exprs[last].type
} else if types.is_concrete_scalar(frame.expected) && !types.is_bool(frame.expected) {
right_expected = frame.expected
} }
append(&stack, Build_Expr_Frame{expr=expr.right, expected=right_expected, template=ast.INVALID_FUNCTION}) append(&stack, Build_Expr_Frame{expr=expr.right, expected=right_expected, template=ast.INVALID_FUNCTION})
continue continue
@@ -3067,6 +3093,39 @@ build_block :: proc(ctx: ^Build_Ctx, statements: []ast.Stmt_Id) -> []hir.Stmt_Id
diagnostic = source.INVALID_DIAGNOSTIC, diagnostic = source.INVALID_DIAGNOSTIC,
}) })
ctx.problematic^ = ctx.problematic^ || checker.module.exprs[condition].kind == .Invalid ctx.problematic^ = ctx.problematic^ || checker.module.exprs[condition].kind == .Invalid
case .While:
condition := build_expr(
checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls,
types.BOOL, ctx.pkg, ctx.file,
)
if checker.module.exprs[condition].kind != .Invalid &&
!types.is_bool(checker.module.exprs[condition].type) {
id := source.add(checker.diagnostics, statement.span, "'while' condition must be a bool")
condition = invalid_hir_expr(checker, statement.span, id, types.BOOL)
ctx.problematic^ = true
}
loop_body := build_block(ctx, statement.body)
update := hir.INVALID_STMT
if statement.update != ast.INVALID_STMT {
update_ast := [1]ast.Stmt_Id{statement.update}
update_body := build_block(ctx, update_ast[:])
if len(update_body) > 0 {
update = update_body[0]
}
delete(update_body, checker.allocator)
}
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind=.While,
span=statement.span,
expr=condition,
then_body=loop_body,
update=update,
local=hir.INVALID_LOCAL,
target=hir.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
})
ctx.problematic^ = ctx.problematic^ || checker.module.exprs[condition].kind == .Invalid
case .Invalid: case .Invalid:
append(&body, hir.stmt_id(len(checker.module.statements))) append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{ append(&checker.module.statements, hir.Stmt{
@@ -3082,8 +3141,9 @@ build_block :: proc(ctx: ^Build_Ctx, statements: []ast.Stmt_Id) -> []hir.Stmt_Id
// Reports whether every control-flow path through `stmts` terminates (returns or traps), // Reports whether every control-flow path through `stmts` terminates (returns or traps),
// so the end of the block is unreachable. A `.Return` or `.Trap` terminates outright; an // so the end of the block is unreachable. A `.Return` or `.Trap` terminates outright; an
// `.If` terminates only when it has an `else` and both arms terminate. Recursion into the // `.If` terminates only when it has an `else` and both arms terminate. A literal
// `then_body`/`else_body` slices handles nested ifs and `else if` chains. // `while true` cannot fall through because the language has no `break` statement.
// Recursion into the branch slices handles nested ifs and `else if` chains.
all_paths_return :: proc(module: ^hir.Module, stmts: []hir.Stmt_Id) -> bool { all_paths_return :: proc(module: ^hir.Module, stmts: []hir.Stmt_Id) -> bool {
for id in stmts { for id in stmts {
statement := module.statements[id] statement := module.statements[id]
@@ -3096,6 +3156,13 @@ all_paths_return :: proc(module: ^hir.Module, stmts: []hir.Stmt_Id) -> bool {
all_paths_return(module, statement.else_body) { all_paths_return(module, statement.else_body) {
return true return true
} }
case .While:
if statement.expr != hir.INVALID_EXPR && int(statement.expr) < len(module.exprs) {
condition := module.exprs[statement.expr]
if condition.kind == .Bool && condition.integer != 0 {
return true
}
}
} }
} }
return false return false
+4
View File
@@ -141,6 +141,7 @@ Stmt_Kind :: enum u8 {
Sink, Sink,
Trap, Trap,
If, If,
While,
} }
Stmt :: struct { Stmt :: struct {
@@ -151,8 +152,11 @@ Stmt :: struct {
expr: Expr_Id, expr: Expr_Id,
// `If` statements use `expr` as the condition and `then_body`/`else_body` as // `If` statements use `expr` as the condition and `then_body`/`else_body` as
// the branch statement lists. // the branch statement lists.
// `While` statements use `expr` as the condition, `then_body` as the loop
// body, and `update` as the optional post-iteration statement.
then_body: []Stmt_Id, then_body: []Stmt_Id,
else_body: []Stmt_Id, else_body: []Stmt_Id,
update: Stmt_Id,
diagnostic: source.Diagnostic_Id, diagnostic: source.Diagnostic_Id,
} }
+2 -2
View File
@@ -26,6 +26,7 @@ keyword_kind :: proc(text: string) -> token.Kind {
case "and": return .Keyword_And case "and": return .Keyword_And
case "or": return .Keyword_Or case "or": return .Keyword_Or
case "if": return .Keyword_If case "if": return .Keyword_If
case "while": return .Keyword_While
case "else": return .Keyword_Else case "else": return .Keyword_Else
case "true": return .Keyword_True case "true": return .Keyword_True
case "false": return .Keyword_False case "false": return .Keyword_False
@@ -109,8 +110,7 @@ lex :: proc(
cursor += 1 cursor += 1
append_token(&stream, source_file, .Colon_Colon, start, cursor) append_token(&stream, source_file, .Colon_Colon, start, cursor)
} else { } else {
id := source.add(diagnostics, source.Span{file=source_file.id, start=source.Offset(start), end=source.Offset(cursor)}, "expected a second ':'") append_token(&stream, source_file, .Colon, start, cursor)
append_token(&stream, source_file, .Invalid, start, cursor, diagnostic=id)
} }
case '=': case '=':
start := cursor start := cursor
+18 -1
View File
@@ -497,6 +497,20 @@ float_predicate :: proc(predicate: ir.Compare_Predicate) -> string {
return "oeq" return "oeq"
} }
emit_entry_allocas :: proc(emitter: ^Emitter, instructions: []ir.Instruction) {
for instruction, instruction_index in instructions {
if instruction.op == .Alloca &&
types.is_runtime_value(instruction.type, &emitter.module.types) {
fmt.sbprintf(
&emitter.builder,
" %%v%d = alloca %s\n",
instruction_index,
llvm_type(instruction.type, &emitter.module.types),
)
}
}
}
emit_instruction_stream :: proc( emit_instruction_stream :: proc(
emitter: ^Emitter, emitter: ^Emitter,
instructions: []ir.Instruction, instructions: []ir.Instruction,
@@ -706,7 +720,9 @@ emit_instruction_stream :: proc(
emit_recovery_value(emitter, instruction_index, instruction, "invalid allocation type") emit_recovery_value(emitter, instruction_index, instruction, "invalid allocation type")
continue continue
} }
fmt.sbprintf(&emitter.builder, " %%v%d = alloca %s\n", instruction_index, llvm_type(instruction.type, &emitter.module.types)) if global_initializer {
fmt.sbprintf(&emitter.builder, " %%v%d = alloca %s\n", instruction_index, llvm_type(instruction.type, &emitter.module.types))
}
case .Index_Address: case .Index_Address:
if !valid_instruction(instructions, instruction.a) || if !valid_instruction(instructions, instruction.a) ||
!valid_value(instructions, instruction.b, types.USIZE, &emitter.module.types) { !valid_value(instructions, instruction.b, types.USIZE, &emitter.module.types) {
@@ -1726,6 +1742,7 @@ emit_functions :: proc(emitter: ^Emitter) {
continue continue
} }
strings.write_string(&emitter.builder, ") {\nentry:\n") strings.write_string(&emitter.builder, ") {\nentry:\n")
emit_entry_allocas(emitter, function.instructions)
if function.calling_convention == .C { if function.calling_convention == .C {
for param_type, index in function.param_types { for param_type, index in function.param_types {
if !types.is_record(param_type, &emitter.module.types) { if !types.is_record(param_type, &emitter.module.types) {
+54
View File
@@ -708,6 +708,60 @@ lower_statements :: proc(state: ^State, statements: []hir.Stmt_Id) {
target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC, diagnostic=source.INVALID_DIAGNOSTIC,
}) })
case .While:
condition_lbl := fresh_label(state)
body_lbl := fresh_label(state)
exit_lbl := fresh_label(state)
update_lbl := condition_lbl
if statement.update != hir.INVALID_STMT {
update_lbl = fresh_label(state)
}
append_instruction(state, ir.Instruction{
op=.Br, span=statement.span, type=types.VOID, integer=condition_lbl,
target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC,
})
append_instruction(state, ir.Instruction{
op=.Label, span=statement.span, type=types.VOID, integer=condition_lbl,
target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC,
})
condition := lower_expr(state, statement.expr)
append_instruction(state, ir.Instruction{
op=.Cond_Br, span=statement.span, type=types.VOID,
a=condition, integer=body_lbl, target=ir.Ref(u32(exit_lbl)),
b=ir.INVALID_INSTRUCTION, diagnostic=source.INVALID_DIAGNOSTIC,
})
append_instruction(state, ir.Instruction{
op=.Label, span=statement.span, type=types.VOID, integer=body_lbl,
target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC,
})
lower_statements(state, statement.then_body)
append_instruction(state, ir.Instruction{
op=.Br, span=statement.span, type=types.VOID, integer=update_lbl,
target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC,
})
if statement.update != hir.INVALID_STMT {
append_instruction(state, ir.Instruction{
op=.Label, span=statement.span, type=types.VOID, integer=update_lbl,
target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC,
})
update := [1]hir.Stmt_Id{statement.update}
lower_statements(state, update[:])
append_instruction(state, ir.Instruction{
op=.Br, span=statement.span, type=types.VOID, integer=condition_lbl,
target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC,
})
}
append_instruction(state, ir.Instruction{
op=.Label, span=statement.span, type=types.VOID, integer=exit_lbl,
target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC,
})
} }
} }
} }
+93
View File
@@ -928,6 +928,9 @@ parse_statement :: proc(parser: ^Parser) -> ast.Stmt_Id {
if current(parser).kind == .Keyword_If { if current(parser).kind == .Keyword_If {
return parse_if(parser) return parse_if(parser)
} }
if current(parser).kind == .Keyword_While {
return parse_while(parser)
}
if current(parser).kind == .Identifier || current(parser).kind == .Underscore { if current(parser).kind == .Identifier || current(parser).kind == .Underscore {
start_cursor := parser.cursor start_cursor := parser.cursor
@@ -1123,6 +1126,96 @@ parse_if :: proc(parser: ^Parser) -> ast.Stmt_Id {
return id return id
} }
parse_while_update :: proc(parser: ^Parser) -> ast.Stmt_Id {
parenthesized := false
if _, ok := allow(parser, .Left_Paren); ok {
parenthesized = true
parser.delimiter_depth += 1
skip_newlines(parser)
}
update := ast.INVALID_STMT
if current(parser).kind == .Left_Brace ||
current(parser).kind == .Right_Paren ||
current(parser).kind == .Eof {
diagnostic := source.add(
parser.diagnostics,
current(parser).span,
"expected a while update statement after ':'",
)
update = ast.stmt_id(len(parser.module.statements))
append(&parser.module.statements, ast.Stmt{
kind=.Invalid,
span=current(parser).span,
expr=ast.INVALID_EXPR,
update=ast.INVALID_STMT,
diagnostic=diagnostic,
})
} else {
saved := parser.no_struct_literal
parser.no_struct_literal = true
update = parse_statement(parser)
parser.no_struct_literal = saved
statement := &parser.module.statements[update]
switch statement.kind {
case .Assignment, .Expression:
case .Invalid, .Declaration, .Return, .If, .While:
diagnostic := source.add(
parser.diagnostics,
statement.span,
"while update must be an assignment, sink, or expression statement",
)
statement.kind = .Invalid
statement.diagnostic = diagnostic
}
}
if parenthesized {
skip_newlines(parser)
if _, ok := allow(parser, .Right_Paren); !ok {
source.add(parser.diagnostics, current(parser).span, "expected ')' after while update")
for current(parser).kind != .Right_Paren &&
current(parser).kind != .Left_Brace &&
current(parser).kind != .Newline &&
current(parser).kind != .Eof {
advance(parser)
}
_, _ = allow(parser, .Right_Paren)
}
parser.delimiter_depth -= 1
}
return update
}
parse_while :: proc(parser: ^Parser) -> ast.Stmt_Id {
start := advance(parser) // consume 'while'
skip_newlines(parser)
saved := parser.no_struct_literal
parser.no_struct_literal = true
condition := parse_expression(parser)
parser.no_struct_literal = saved
skip_newlines(parser)
update := ast.INVALID_STMT
if _, ok := allow(parser, .Colon); ok {
skip_newlines(parser)
update = parse_while_update(parser)
skip_newlines(parser)
}
body := parse_block(parser)
id := ast.stmt_id(len(parser.module.statements))
append(&parser.module.statements, ast.Stmt{
kind=.While,
span=span_from(start.span, previous(parser).span),
expr=condition,
body=body,
update=update,
diagnostic=source.INVALID_DIAGNOSTIC,
})
return id
}
parse_function :: proc(parser: ^Parser, name: token.Token, c_abi: bool) { parse_function :: proc(parser: ^Parser, name: token.Token, c_abi: bool) {
advance(parser) advance(parser)
if _, ok := allow(parser, .Left_Paren); !ok { if _, ok := allow(parser, .Left_Paren); !ok {
+2
View File
@@ -13,6 +13,7 @@ Kind :: enum u8 {
String, String,
Character, Character,
Underscore, Underscore,
Colon,
Colon_Colon, Colon_Colon,
Equal, Equal,
Equal_Equal, Equal_Equal,
@@ -53,6 +54,7 @@ Kind :: enum u8 {
Keyword_And, Keyword_And,
Keyword_Or, Keyword_Or,
Keyword_If, Keyword_If,
Keyword_While,
Keyword_Else, Keyword_Else,
Keyword_True, Keyword_True,
Keyword_False, Keyword_False,
+175
View File
@@ -2293,6 +2293,18 @@ count_substring_occurrences :: proc(text, needle: string) -> int {
return count return count
} }
find_substring_offset :: proc(text, needle: string) -> int {
if len(needle) == 0 {
return 0
}
for index := 0; index + len(needle) <= len(text); index += 1 {
if text[index:index + len(needle)] == needle {
return index
}
}
return -1
}
find_cimport_variable :: proc(result: ^cimport.Result, name: string) -> (^cimport.Variable, bool) { find_cimport_variable :: proc(result: ^cimport.Result, name: string) -> (^cimport.Variable, bool) {
for &variable in result.variables { for &variable in result.variables {
if variable.name == name { if variable.name == name {
@@ -3905,3 +3917,166 @@ if_unwrap_binding_is_immutable :: proc(t: ^testing.T) {
} }
testing.expect(t, found) testing.expect(t, found)
} }
@(test)
while_loops_compile_and_run :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-while-loop"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/while_loop", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 42)
}
@(test)
while_loop_diagnostics_cover_condition_update_and_scope :: proc(t: ^testing.T) {
text := `bad_condition :: func() void {
while 1 {}
}
bad_unresolved :: func() void {
while false : missing = 1 {}
}
bad_immutable :: func() void {
i :: 0
while false : i = i + 1 {}
}
bad_body_scope :: func() void {
running :: false
while running : i = 1 {
i u32 = 0
}
}
bad_declaration_update :: func() void {
while false : i u32 = 0 {}
}
bad_missing_update :: func() void {
while false : {}
}
main :: func() void {
bad_condition()
bad_unresolved()
bad_immutable()
bad_body_scope()
bad_declaration_update()
bad_missing_update()
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
non_bool := false
unresolved_missing := false
unresolved_body_local := false
immutable := false
disallowed := false
missing := false
for diagnostic in diagnostics.items {
non_bool = non_bool || strings.contains(diagnostic.message, "'while' condition must be a bool")
unresolved_missing = unresolved_missing || strings.contains(diagnostic.message, "cannot assign unresolved local 'missing'")
unresolved_body_local = unresolved_body_local || strings.contains(diagnostic.message, "cannot assign unresolved local 'i'")
immutable = immutable || strings.contains(diagnostic.message, "cannot assign immutable local 'i'")
disallowed = disallowed || strings.contains(diagnostic.message, "while update must be an assignment, sink, or expression statement")
missing = missing || strings.contains(diagnostic.message, "expected a while update statement after ':'")
}
testing.expect(t, non_bool)
testing.expect(t, unresolved_missing)
testing.expect(t, unresolved_body_local)
testing.expect(t, immutable)
testing.expect(t, disallowed)
testing.expect(t, missing)
}
@(test)
while_true_and_potential_fallthrough_have_distinct_return_analysis :: proc(t: ^testing.T) {
text := `forever :: func() i32 {
while true {}
}
maybe :: func(run bool) i32 {
while run {
return 1
}
}
main :: func() void {
if false {
_ = forever()
}
_ = maybe(false)
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
missing_return_count := 0
for diagnostic in diagnostics.items {
if strings.contains(diagnostic.message, "does not return a value") {
missing_return_count += 1
}
}
testing.expect_value(t, missing_return_count, 1)
}
@(test)
while_loop_allocas_are_emitted_in_the_entry_block :: proc(t: ^testing.T) {
text := `main :: func() i32 {
i u32 = 0
while i < 2 and true : i = i + 1 {
value u32 = i
_ = value
}
return 0
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(llvm_text)
testing.expect_value(t, len(diagnostics.items), 0)
first_loop_label := find_substring_offset(llvm_text, "bro_block_")
testing.expect(t, first_loop_label >= 0)
alloca_count := 0
for function in ir_module.functions {
if !function.is_main {
continue
}
for instruction, instruction_index in function.instructions {
if instruction.op != .Alloca {
continue
}
alloca_count += 1
needle := fmt.tprintf(" %%v%d = alloca ", instruction_index)
offset := find_substring_offset(llvm_text, needle)
testing.expect(t, offset >= 0 && offset < first_loop_label)
}
}
testing.expect(t, alloca_count >= 3)
}
+52
View File
@@ -0,0 +1,52 @@
# Milestone 5: boolean while loops with optional post-iteration updates.
return_before_update :: func() i32 {
i i32 = 0
while true : i = i + 1 {
return i
}
}
main :: func() i32 {
total i32 = 0
# ordinary condition and update
i u32 = 0
while i < 5 : i = i + 1 {
total = total + 2
}
# equivalent parenthesized header
j u32 = 0
while (j < 4) : (j = j + 1) {
total = total + 3
}
# nested loops and body-local storage
outer u32 = 0
while outer < 2 : outer = outer + 1 {
inner u32 = 0
while inner < 3 : inner = inner + 1 {
total = total + 2
}
}
# The body-local k shadows only inside the body. The update still targets
# the mutable k declared before the loop.
k u32 = 0
while k < 4 : k = k + 1 {
k u32 = 100
if k == 100 {
total = total + 2
}
}
# zero iterations
while false {
total = total + 100
}
# A return exits before the update clause.
total = total + return_before_update()
return total
}