while loops
This commit is contained in:
+9
-7
@@ -12,11 +12,12 @@
|
||||
|
||||
### 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`
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
@@ -24,10 +25,12 @@
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
|
||||
@@ -40,6 +43,9 @@
|
||||
- file-local relative imports, aliases, and qualified member access
|
||||
- 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
|
||||
- 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
|
||||
- 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
|
||||
@@ -59,13 +65,9 @@
|
||||
### foreign functions and linking
|
||||
|
||||
- exporting brolang functions to c
|
||||
- additional target-specific C ABI lowering
|
||||
|
||||
### scalar and compound types
|
||||
|
||||
- tuples and native variadic functions
|
||||
- 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
|
||||
|
||||
@@ -95,9 +95,12 @@
|
||||
- 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`
|
||||
- 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 : 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)
|
||||
- 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`)
|
||||
@@ -109,8 +112,11 @@
|
||||
- `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
|
||||
|
||||
6. compound assignment
|
||||
6. compound assignment: `+=`, `-=`, `*=`, `/=`
|
||||
|
||||
7. enums (native and c interop) (see below)
|
||||
|
||||
8. distinct types (see below)
|
||||
|
||||
## 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.
|
||||
|
||||
# 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
|
||||
```
|
||||
|
||||
@@ -120,6 +120,7 @@ Stmt_Kind :: enum u8 {
|
||||
Return,
|
||||
Expression,
|
||||
If,
|
||||
While,
|
||||
}
|
||||
|
||||
Stmt :: struct {
|
||||
@@ -133,8 +134,11 @@ Stmt :: struct {
|
||||
// `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` 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,
|
||||
else_body: []Stmt_Id,
|
||||
update: Stmt_Id,
|
||||
diagnostic: source.Diagnostic_Id,
|
||||
}
|
||||
|
||||
|
||||
@@ -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_block_imports_used(checker, statement.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:
|
||||
}
|
||||
}
|
||||
@@ -1424,6 +1431,13 @@ infer_statements :: proc(
|
||||
infer_statements(checker, statement.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)
|
||||
@@ -2460,7 +2474,13 @@ build_expr :: proc(
|
||||
append(&stack, Build_Expr_Frame{expr=expr.left, expected=types.INVALID, template=ast.INVALID_FUNCTION})
|
||||
case .Add:
|
||||
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:
|
||||
if expr.left != ast.INVALID_EXPR {
|
||||
stack[frame_index].stage = 6
|
||||
@@ -2600,6 +2620,12 @@ build_expr :: proc(
|
||||
right_expected := types.INVALID
|
||||
if types.is_many_pointer(checker.module.exprs[last].type, &checker.module.types) {
|
||||
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})
|
||||
continue
|
||||
@@ -3067,6 +3093,39 @@ build_block :: proc(ctx: ^Build_Ctx, statements: []ast.Stmt_Id) -> []hir.Stmt_Id
|
||||
diagnostic = source.INVALID_DIAGNOSTIC,
|
||||
})
|
||||
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:
|
||||
append(&body, hir.stmt_id(len(checker.module.statements)))
|
||||
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),
|
||||
// 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
|
||||
// `then_body`/`else_body` slices handles nested ifs and `else if` chains.
|
||||
// `.If` terminates only when it has an `else` and both arms terminate. A literal
|
||||
// `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 {
|
||||
for id in stmts {
|
||||
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) {
|
||||
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
|
||||
|
||||
@@ -141,6 +141,7 @@ Stmt_Kind :: enum u8 {
|
||||
Sink,
|
||||
Trap,
|
||||
If,
|
||||
While,
|
||||
}
|
||||
|
||||
Stmt :: struct {
|
||||
@@ -151,8 +152,11 @@ Stmt :: struct {
|
||||
expr: Expr_Id,
|
||||
// `If` statements use `expr` as the condition and `then_body`/`else_body` as
|
||||
// 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,
|
||||
else_body: []Stmt_Id,
|
||||
update: Stmt_Id,
|
||||
diagnostic: source.Diagnostic_Id,
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ keyword_kind :: proc(text: string) -> token.Kind {
|
||||
case "and": return .Keyword_And
|
||||
case "or": return .Keyword_Or
|
||||
case "if": return .Keyword_If
|
||||
case "while": return .Keyword_While
|
||||
case "else": return .Keyword_Else
|
||||
case "true": return .Keyword_True
|
||||
case "false": return .Keyword_False
|
||||
@@ -109,8 +110,7 @@ lex :: proc(
|
||||
cursor += 1
|
||||
append_token(&stream, source_file, .Colon_Colon, start, cursor)
|
||||
} 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, .Invalid, start, cursor, diagnostic=id)
|
||||
append_token(&stream, source_file, .Colon, start, cursor)
|
||||
}
|
||||
case '=':
|
||||
start := cursor
|
||||
|
||||
+18
-1
@@ -497,6 +497,20 @@ float_predicate :: proc(predicate: ir.Compare_Predicate) -> string {
|
||||
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(
|
||||
emitter: ^Emitter,
|
||||
instructions: []ir.Instruction,
|
||||
@@ -706,7 +720,9 @@ emit_instruction_stream :: proc(
|
||||
emit_recovery_value(emitter, instruction_index, instruction, "invalid allocation type")
|
||||
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:
|
||||
if !valid_instruction(instructions, instruction.a) ||
|
||||
!valid_value(instructions, instruction.b, types.USIZE, &emitter.module.types) {
|
||||
@@ -1726,6 +1742,7 @@ emit_functions :: proc(emitter: ^Emitter) {
|
||||
continue
|
||||
}
|
||||
strings.write_string(&emitter.builder, ") {\nentry:\n")
|
||||
emit_entry_allocas(emitter, function.instructions)
|
||||
if function.calling_convention == .C {
|
||||
for param_type, index in function.param_types {
|
||||
if !types.is_record(param_type, &emitter.module.types) {
|
||||
|
||||
@@ -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,
|
||||
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,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -928,6 +928,9 @@ parse_statement :: proc(parser: ^Parser) -> ast.Stmt_Id {
|
||||
if current(parser).kind == .Keyword_If {
|
||||
return parse_if(parser)
|
||||
}
|
||||
if current(parser).kind == .Keyword_While {
|
||||
return parse_while(parser)
|
||||
}
|
||||
|
||||
if current(parser).kind == .Identifier || current(parser).kind == .Underscore {
|
||||
start_cursor := parser.cursor
|
||||
@@ -1123,6 +1126,96 @@ parse_if :: proc(parser: ^Parser) -> ast.Stmt_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) {
|
||||
advance(parser)
|
||||
if _, ok := allow(parser, .Left_Paren); !ok {
|
||||
|
||||
@@ -13,6 +13,7 @@ Kind :: enum u8 {
|
||||
String,
|
||||
Character,
|
||||
Underscore,
|
||||
Colon,
|
||||
Colon_Colon,
|
||||
Equal,
|
||||
Equal_Equal,
|
||||
@@ -53,6 +54,7 @@ Kind :: enum u8 {
|
||||
Keyword_And,
|
||||
Keyword_Or,
|
||||
Keyword_If,
|
||||
Keyword_While,
|
||||
Keyword_Else,
|
||||
Keyword_True,
|
||||
Keyword_False,
|
||||
|
||||
@@ -2293,6 +2293,18 @@ count_substring_occurrences :: proc(text, needle: string) -> int {
|
||||
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) {
|
||||
for &variable in result.variables {
|
||||
if variable.name == name {
|
||||
@@ -3905,3 +3917,166 @@ if_unwrap_binding_is_immutable :: proc(t: ^testing.T) {
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user