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
+4
View File
@@ -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,
}
+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_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
+4
View File
@@ -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,
}
+2 -2
View File
@@ -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
View File
@@ -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) {
+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,
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 {
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 {
+2
View File
@@ -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,