for loops

This commit is contained in:
2026-06-22 20:11:18 +02:00
parent 380b5943b3
commit 27f42dd253
15 changed files with 1369 additions and 22 deletions
+3
View File
@@ -30,7 +30,10 @@
- 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
- first-class exclusive and inclusive integer ranges: `start..end` and `start..=end`
- boolean `if` statements and `while` loops with optional post-iteration assignment/expression clauses
- `for` loops over ranges, arrays, slices, and pointers-to-arrays, with copy, pointer, and optional `usize` index captures
- `|@item|` pointer captures inherit pointee mutability from the iterable; arrays require an explicit pointer such as `&items`
### functions and packages
+7 -5
View File
@@ -101,11 +101,11 @@
- 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:
- ranges (implemented; see section below)
- for loops (implemented; operates on ranges, arrays, slices, and pointers-to-arrays). 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 (immutable) reference semantics, i.e. gets a `@T`)
- `for items |&mut item| { ... }` - capture just the `item` value in the array/slice (uses (mutable) reference semantics, i.e. gets a `@mut T`)
- `for (&items) |@item| { ... }` - capture a pointer to each array element; its `@T` / `@mut T` mutability follows the iterable
- `for items_slice |@item| { ... }` - slices already refer to backing storage and support pointer capture directly
- `for items |item, idx| { ... }` - capture `item` and its index index in the array/slice
- `for 0..10 |i| { ... }` - iterate over the range `0..10` (exclusive)
- `for 0..=10 |i| { ... }` - iterate over the range `0..10` (inclusive)
@@ -180,7 +180,9 @@ Ranges represent a sequence of values, commonly used in for loops, and is itself
# 0..n + 1 # ERROR: must parenthesize complex expressions
```
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. Range bounds are evaluated once, must have compatible concrete integer types, and descending ranges are empty.
For-loop captures are immutable and scoped to the loop body. Sequence index captures are `usize`. Pointer capture uses `|@item|`; arrays must be passed by pointer (for example `&items`), while slices can be used directly. Sentinel elements are not included in iteration.
# A word on distinct types
+8
View File
@@ -92,6 +92,7 @@ Expr_Kind :: enum u8 {
Ge,
And,
Or,
Range,
Call,
}
@@ -104,6 +105,7 @@ Expr :: struct {
left: Expr_Id,
right: Expr_Id,
diagnostic: source.Diagnostic_Id,
parenthesized: bool,
kind: Expr_Kind,
}
@@ -121,14 +123,17 @@ Stmt_Kind :: enum u8 {
Expression,
If,
While,
For,
}
Stmt :: struct {
kind: Stmt_Kind,
span: source.Span,
name: symbol.Id,
index_name: symbol.Id,
type: Type_Syntax,
immutable: bool,
pointer_capture: bool,
target: Expr_Id,
expr: Expr_Id,
// `If` statements use `expr` as the condition, `body` as the then-block, and
@@ -136,6 +141,9 @@ Stmt :: struct {
// `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.
// `For` statements use `expr` as the iterable, `name` as the item capture,
// `index_name` as the optional index capture, and `pointer_capture` to
// distinguish `|@item|` from copy capture.
body: []Stmt_Id,
else_body: []Stmt_Id,
update: Stmt_Id,
+208 -6
View File
@@ -611,7 +611,7 @@ mark_expr_imports_used :: proc(checker: ^Checker, expr_id: ast.Expr_Id, file: as
}
case .Negate, .Not, .Address, .Deref, .Field, .Unwrap, .Keyed:
append(&stack, expr.left)
case .Add, .Index, .Orelse, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or:
case .Add, .Index, .Orelse, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or, .Range:
append(&stack, expr.left, expr.right)
case .Invalid, .Integer, .Float, .String, .Bool, .None, .Name:
}
@@ -638,6 +638,9 @@ mark_block_imports_used :: proc(checker: ^Checker, statements: []ast.Stmt_Id, fi
update := [1]ast.Stmt_Id{statement.update}
mark_block_imports_used(checker, update[:], file)
}
case .For:
mark_expr_imports_used(checker, statement.expr, file)
mark_block_imports_used(checker, statement.body, file)
case .Invalid:
}
}
@@ -1028,6 +1031,23 @@ infer_compound_expr :: proc(
_ = infer_nested_expr(checker, expr.left, locals, pkg, file, demanded)
_ = infer_nested_expr(checker, expr.right, locals, pkg, file, demanded)
return types.BOOL
case .Range:
left := infer_nested_expr(checker, expr.left, locals, pkg, file, demanded)
right := infer_nested_expr(checker, expr.right, locals, pkg, file, demanded)
left_const := eval_constant(checker, expr.left)
right_const := eval_constant(checker, expr.right)
child := types.INVALID
if left_const.kind == .Value && right_const.kind != .Value {
child = right
} else if right_const.kind == .Value && left_const.kind != .Value {
child = left
} else {
child = types.widest(left, right)
}
if !types.is_concrete_integer(child) {
return types.INVALID
}
return types.range(store, child)
case .String:
return string_literal_type(checker, expr.integer)
case .Array:
@@ -1165,7 +1185,7 @@ infer_expr :: proc(
_ = pop(&stack)
case .String, .Array, .None, .Address, .Deref, .Index, .Slice,
.Field, .Unwrap, .Orelse, .Struct_Literal, .Keyed,
.Bool, .Not, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or:
.Bool, .Not, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or, .Range:
last = infer_compound_expr(checker, expr, locals, pkg, file, demanded)
_ = pop(&stack)
case .Name:
@@ -1413,7 +1433,7 @@ infer_statements :: proc(
returned := infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded)
if !types.is_valid(result^) {
result^ = returned
} else {
} else if !types.equal(result^, returned) {
result^ = types.widest(result^, returned)
}
}
@@ -1438,6 +1458,29 @@ infer_statements :: proc(
update := [1]ast.Stmt_Id{statement.update}
infer_statements(checker, update[:], locals, pkg, file, demanded, result)
}
case .For:
iterable_type := infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded)
capture_start := len(locals^)
capture_type := types.INVALID
if types.is_range(iterable_type, &checker.module.types) {
capture_type = types.child_type(iterable_type, &checker.module.types)
} else {
item, ok := sequence_item(iterable_type, &checker.module.types)
if ok {
capture_type = item.child
if statement.pointer_capture {
capture_type = types.pointer(&checker.module.types, item.child, item.mutable, false)
}
}
}
if symbol.is_valid(statement.name) {
append(locals, Infer_Local{name=statement.name, type=capture_type})
}
if symbol.is_valid(statement.index_name) {
append(locals, Infer_Local{name=statement.index_name, type=types.USIZE})
}
infer_statements(checker, statement.body, locals, pkg, file, demanded, result)
resize(locals, capture_start)
}
}
resize(locals, scope_start)
@@ -1899,6 +1942,19 @@ hir_is_location :: proc(checker: ^Checker, expr_id: hir.Expr_Id) -> bool {
return false
}
sequence_item :: proc(value: types.Type, store: ^types.Store) -> (types.Node, bool) {
item, ok := types.node(store, value)
if ok && (item.kind == .Array || item.kind == .Slice) {
return item, true
}
pointer, array, pointer_ok := types.array_pointer(value, store)
if pointer_ok {
array.mutable = pointer.mutable && array.mutable
return array, true
}
return {}, false
}
find_struct_field :: proc(checker: ^Checker, struct_type: types.Type, name: symbol.Id) -> (int, types.Field, bool) {
for field, index in types.fields_for(&checker.module.types, struct_type) {
if field.name == u32(name) {
@@ -2192,6 +2248,46 @@ build_compound_expr :: proc(
kind=.Orelse, span=expr.span, type=child, left=optional, right=fallback,
target=hir.INVALID_REF, diagnostic=source.INVALID_DIAGNOSTIC,
})
case .Range:
expected_child := types.INVALID
if types.is_range(expected, store) {
expected_child = types.child_type(expected, store)
}
left_const := eval_constant(checker, expr.left)
right_const := eval_constant(checker, expr.right)
left, right: hir.Expr_Id
if types.is_valid(expected_child) {
left = build_nested_expr(checker, expr.left, locals, global_reads, calls, expected_child, pkg, file)
right = build_nested_expr(checker, expr.right, locals, global_reads, calls, expected_child, pkg, file)
} else if right_const.kind == .Value && left_const.kind != .Value {
left = build_nested_expr(checker, expr.left, locals, global_reads, calls, types.INVALID, pkg, file)
right = build_nested_expr(checker, expr.right, locals, global_reads, calls, checker.module.exprs[left].type, pkg, file)
} else if left_const.kind == .Value && right_const.kind != .Value {
right = build_nested_expr(checker, expr.right, locals, global_reads, calls, types.INVALID, pkg, file)
left = build_nested_expr(checker, expr.left, locals, global_reads, calls, checker.module.exprs[right].type, pkg, file)
} else {
left = build_nested_expr(checker, expr.left, locals, global_reads, calls, types.INVALID, pkg, file)
right = build_nested_expr(checker, expr.right, locals, global_reads, calls, types.INVALID, pkg, file)
}
child := expected_child
if !types.is_valid(child) {
child = types.widest(checker.module.exprs[left].type, checker.module.exprs[right].type)
}
if !types.is_concrete_integer(child) {
id := source.add(checker.diagnostics, expr.span, "range bounds must be compatible concrete integers")
return invalid_hir_expr(checker, expr.span, id)
}
left = coerce_expr(checker, left, child, checker.module.exprs[left].span)
right = coerce_expr(checker, right, child, checker.module.exprs[right].span)
args := make([]hir.Expr_Id, 2, checker.allocator)
args[0] = left
args[1] = right
return add_hir_expr(checker, hir.Expr{
kind=.Range, span=expr.span, type=types.range(store, child),
integer=i64(expr.integer), args=args,
target=hir.INVALID_REF, left=hir.INVALID_EXPR, right=hir.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
})
case .Bool:
return add_hir_expr(checker, hir.Expr{
kind=.Bool, span=expr.span, type=types.BOOL, integer=i64(expr.integer),
@@ -2391,7 +2487,7 @@ build_expr :: proc(
switch expr.kind {
case .String, .Array, .None, .Address, .Deref, .Index, .Slice,
.Field, .Unwrap, .Orelse, .Struct_Literal, .Keyed,
.Bool, .Not, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or:
.Bool, .Not, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or, .Range:
last = build_compound_expr(
checker, expr, locals, global_reads, calls, frame.expected, pkg, file,
)
@@ -2849,11 +2945,16 @@ make_link_name :: proc(checker: ^Checker, id: Spec_Id) -> string {
return fmt.aprintf("%s", strings.to_string(builder), allocator = checker.allocator)
}
build_block :: proc(ctx: ^Build_Ctx, statements: []ast.Stmt_Id) -> []hir.Stmt_Id {
build_block :: proc(
ctx: ^Build_Ctx,
statements: []ast.Stmt_Id,
duplicate_scope_start := -1,
) -> []hir.Stmt_Id {
checker := ctx.checker
body: [dynamic]hir.Stmt_Id
body.allocator = checker.allocator
scope_start := len(ctx.locals^)
duplicate_start := scope_start if duplicate_scope_start < 0 else duplicate_scope_start
for statement_id in statements {
statement := checker.ast_module.statements[statement_id]
switch statement.kind {
@@ -2876,7 +2977,7 @@ build_block :: proc(ctx: ^Build_Ctx, statements: []ast.Stmt_Id) -> []hir.Stmt_Id
value = invalid_hir_expr(checker, statement.span, id)
value_type = types.INVALID
}
if _, found := find_build_local(ctx.locals^[scope_start:], statement.name); found {
if _, found := find_build_local(ctx.locals^[duplicate_start:], statement.name); found {
id := source.addf(
checker.diagnostics, statement.span,
"duplicate local '%s'", symbol_text(checker, statement.name),
@@ -3126,6 +3227,107 @@ 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 .For:
iterable := build_expr(
checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls,
types.INVALID, ctx.pkg, ctx.file,
)
iterable_type := checker.module.exprs[iterable].type
capture_type := types.I64
iterator_type := types.INVALID
valid_loop := checker.module.exprs[iterable].kind != .Invalid
diagnostic := source.INVALID_DIAGNOSTIC
is_range := types.is_range(iterable_type, &checker.module.types)
if is_range {
capture_type = types.child_type(iterable_type, &checker.module.types)
if statement.pointer_capture {
diagnostic = source.add(checker.diagnostics, statement.span, "range loops do not support pointer captures")
valid_loop = false
}
if symbol.is_valid(statement.index_name) {
diagnostic = source.add(checker.diagnostics, statement.span, "range loops do not support index captures")
valid_loop = false
}
} else {
item, ok := sequence_item(iterable_type, &checker.module.types)
if !ok {
diagnostic = source.add(
checker.diagnostics,
statement.span,
"for-loop iterable must be a range, array, slice, or pointer-to-array",
)
valid_loop = false
} else {
iterator_type = types.pointer(
&checker.module.types,
item.child,
item.mutable,
true,
item.has_sentinel,
item.sentinel,
)
capture_type = item.child
if statement.pointer_capture {
_, _, array_pointer_ok := types.array_pointer(iterable_type, &checker.module.types)
if !types.is_slice(iterable_type, &checker.module.types) && !array_pointer_ok {
diagnostic = source.add(
checker.diagnostics,
statement.span,
"pointer capture over an array requires a pointer-to-array such as '&items'",
)
valid_loop = false
}
capture_type = types.pointer(&checker.module.types, item.child, item.mutable, false)
}
}
}
capture_start := len(ctx.locals^)
item_local := hir.local_id(len(ctx.hir_locals^))
append(ctx.hir_locals, hir.Local{name=statement.name, type=capture_type, mutable=false})
append(ctx.locals, Build_Local{name=statement.name, type=capture_type, mutable=false, id=item_local})
index_local := hir.INVALID_LOCAL
if symbol.is_valid(statement.index_name) {
if statement.index_name == statement.name {
diagnostic = source.add(checker.diagnostics, statement.span, "for-loop captures must have distinct names")
valid_loop = false
} else {
index_local = hir.local_id(len(ctx.hir_locals^))
append(ctx.hir_locals, hir.Local{name=statement.index_name, type=types.USIZE, mutable=false})
append(ctx.locals, Build_Local{name=statement.index_name, type=types.USIZE, mutable=false, id=index_local})
}
}
loop_body := build_block(ctx, statement.body, capture_start)
resize(ctx.locals, capture_start)
append(&body, hir.stmt_id(len(checker.module.statements)))
if valid_loop {
append(&checker.module.statements, hir.Stmt{
kind=.For,
span=statement.span,
local=item_local,
index_local=index_local,
expr=iterable,
iterator_type=iterator_type,
pointer_capture=statement.pointer_capture,
then_body=loop_body,
update=hir.INVALID_STMT,
target=hir.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
})
} else {
delete(loop_body, checker.allocator)
append(&checker.module.statements, hir.Stmt{
kind=.Trap,
span=statement.span,
local=hir.INVALID_LOCAL,
index_local=hir.INVALID_LOCAL,
expr=hir.INVALID_EXPR,
target=hir.INVALID_EXPR,
diagnostic=diagnostic,
})
ctx.problematic^ = true
}
case .Invalid:
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
+8
View File
@@ -111,6 +111,7 @@ Expr_Kind :: enum u8 {
Ge,
And,
Or,
Range,
Call,
}
@@ -142,18 +143,25 @@ Stmt_Kind :: enum u8 {
Trap,
If,
While,
For,
}
Stmt :: struct {
kind: Stmt_Kind,
span: source.Span,
local: Local_Id,
index_local: Local_Id,
target: Expr_Id,
expr: Expr_Id,
iterator_type: types.Type,
pointer_capture: bool,
// `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.
// `For` statements use `expr` as the iterable, `local` as the item capture,
// `index_local` as the optional sequence index, and `iterator_type` as the
// normalized many-item pointer type for sequence iteration.
then_body: []Stmt_Id,
else_body: []Stmt_Id,
update: Stmt_Id,
+2
View File
@@ -84,6 +84,8 @@ Opcode :: enum u8 {
Slice,
Length,
Slice_Ptr,
Extract,
Select,
Unwrap,
Optional_Is_Some,
Optional_Value,
+4
View File
@@ -27,6 +27,7 @@ keyword_kind :: proc(text: string) -> token.Kind {
case "or": return .Keyword_Or
case "if": return .Keyword_If
case "while": return .Keyword_While
case "for": return .Keyword_For
case "else": return .Keyword_Else
case "true": return .Keyword_True
case "false": return .Keyword_False
@@ -162,6 +163,9 @@ lex :: proc(
if cursor < len(bytes) && bytes[cursor] == '.' {
cursor += 1
append_token(&stream, source_file, .Ellipsis, start, cursor)
} else if cursor < len(bytes) && bytes[cursor] == '=' {
cursor += 1
append_token(&stream, source_file, .Range_Inclusive, start, cursor)
} else {
append_token(&stream, source_file, .Range, start, cursor)
}
+59 -5
View File
@@ -146,6 +146,10 @@ llvm_type :: proc(value: types.Type, store: ^types.Store = nil) -> string {
return "ptr"
case .Slice:
return "{ ptr, i64 }"
case .Range:
item, _ := types.node(store, value)
child := llvm_type(item.child, store)
return fmt.tprintf("{{ %s, %s, i1 }}", child, child)
case .Array:
item, _ := types.node(store, value)
return fmt.tprintf("[%d x %s]", types.physical_count(value, store), llvm_type(item.child, store))
@@ -232,7 +236,8 @@ valid_value :: proc(
}
switch instructions[value_id].op {
case .Param, .Const, .String, .Aggregate, .None, .Optional_Some,
.Load_Global, .Function_Address, .Address_Of, .Load, .Slice, .Length, .Slice_Ptr, .Unwrap,
.Load_Global, .Function_Address, .Address_Of, .Load, .Slice, .Length, .Slice_Ptr,
.Extract, .Select, .Unwrap,
.Optional_Is_Some, .Optional_Value, .Orelse,
.Widen, .C_Vararg_Promote, .Weaken_Pointer, .Weaken_Slice, .Decay_Array_Pointer,
.Neg_Checked, .Add_Checked, .Pointer_Add, .Not, .Compare, .Call:
@@ -551,6 +556,8 @@ emit_instruction_stream :: proc(
expected_count = int(item.field_count)
} else if ok && item.kind == .Union {
expected_count = 1
} else if ok && item.kind == .Range {
expected_count = 3
} else {
emit_recovery_value(emitter, instruction_index, instruction, "invalid aggregate type")
continue
@@ -585,6 +592,8 @@ emit_instruction_stream :: proc(
element_type := item.child
if item.kind == .Struct {
element_type = types.fields_for(&emitter.module.types, instruction.type)[arg_index].type
} else if item.kind == .Range && arg_index == 2 {
element_type = types.BOOL
}
final := arg_index == total-1
if final {
@@ -924,6 +933,46 @@ emit_instruction_stream :: proc(
} else {
emit_recovery_value(emitter, instruction_index, instruction, "invalid container pointer")
}
case .Extract:
if !valid_instruction(instructions, instruction.a) {
emit_recovery_value(emitter, instruction_index, instruction, "invalid aggregate extraction")
continue
}
aggregate_type := instructions[instruction.a].type
item, ok := types.node(&emitter.module.types, aggregate_type)
field_index := int(instruction.integer)
expected_type := types.INVALID
if ok && item.kind == .Range && field_index >= 0 && field_index < 3 {
expected_type = types.BOOL if field_index == 2 else item.child
}
if !types.equal(expected_type, instruction.type) ||
!valid_value(instructions, instruction.a, aggregate_type, &emitter.module.types) {
emit_recovery_value(emitter, instruction_index, instruction, "invalid aggregate extraction")
continue
}
fmt.sbprintf(
&emitter.builder,
" %%v%d = extractvalue %s %%v%d, %d\n",
instruction_index,
llvm_type(aggregate_type, &emitter.module.types),
instruction.a,
field_index,
)
case .Select:
if len(instruction.args) != 2 ||
!valid_value(instructions, instruction.a, types.BOOL, &emitter.module.types) ||
!valid_value(instructions, instruction.args[0], instruction.type, &emitter.module.types) ||
!valid_value(instructions, instruction.args[1], instruction.type, &emitter.module.types) {
emit_recovery_value(emitter, instruction_index, instruction, "invalid selection")
continue
}
fmt.sbprintf(&emitter.builder, " %%v%d = select i1 ", instruction_index)
write_operand(&emitter.builder, instructions, instruction.a, types.BOOL, &emitter.module.types)
fmt.sbprintf(&emitter.builder, ", %s ", llvm_type(instruction.type, &emitter.module.types))
write_operand(&emitter.builder, instructions, instruction.args[0], instruction.type, &emitter.module.types)
fmt.sbprintf(&emitter.builder, ", %s ", llvm_type(instruction.type, &emitter.module.types))
write_operand(&emitter.builder, instructions, instruction.args[1], instruction.type, &emitter.module.types)
strings.write_string(&emitter.builder, "\n")
case .Unwrap:
optional_type := instructions[instruction.a].type if valid_instruction(instructions, instruction.a) else types.INVALID
item, ok := types.node(&emitter.module.types, optional_type)
@@ -1152,14 +1201,19 @@ emit_instruction_stream :: proc(
emit_trap_call(emitter, message)
fmt.sbprintf(&emitter.builder, " unreachable\noverflow_continue%d:\n", instruction_index)
case .Pointer_Add:
item, ok := types.node(&emitter.module.types, instruction.type)
if !ok || item.kind != .Pointer || !item.many ||
!valid_value(instructions, instruction.a, instruction.type, &emitter.module.types) ||
result_item, result_ok := types.node(&emitter.module.types, instruction.type)
base_type := instructions[instruction.a].type if valid_instruction(instructions, instruction.a) else types.INVALID
base_item, base_ok := types.node(&emitter.module.types, base_type)
if !result_ok || result_item.kind != .Pointer ||
!base_ok || base_item.kind != .Pointer || !base_item.many ||
result_item.child != base_item.child ||
result_item.mutable != base_item.mutable ||
!valid_value(instructions, instruction.a, base_type, &emitter.module.types) ||
!valid_value(instructions, instruction.b, types.USIZE, &emitter.module.types) {
emit_recovery_value(emitter, instruction_index, instruction, "invalid pointer offset")
continue
}
fmt.sbprintf(&emitter.builder, " %%v%d = getelementptr %s, ptr %%v%d, i64 ", instruction_index, llvm_type(item.child, &emitter.module.types), instruction.a)
fmt.sbprintf(&emitter.builder, " %%v%d = getelementptr %s, ptr %%v%d, i64 ", instruction_index, llvm_type(result_item.child, &emitter.module.types), instruction.a)
write_operand(&emitter.builder, instructions, instruction.b, types.USIZE, &emitter.module.types)
strings.write_string(&emitter.builder, "\n")
case .Call:
+357 -1
View File
@@ -150,6 +150,18 @@ lower_location :: proc(state: ^State, expr_id: hir.Expr_Id, for_write := false)
return ir.INVALID_INSTRUCTION
}
hir_expr_is_location :: proc(state: ^State, expr_id: hir.Expr_Id) -> bool {
if expr_id == hir.INVALID_EXPR || int(expr_id) >= len(state.hir_module.exprs) {
return false
}
#partial switch state.hir_module.exprs[expr_id].kind {
case .Local, .Global, .Deref, .Index, .Field:
return true
case:
return false
}
}
lower_compound_expr :: proc(state: ^State, expr_id: hir.Expr_Id) -> ir.Instruction_Id {
expr := state.hir_module.exprs[expr_id]
#partial switch expr.kind {
@@ -169,6 +181,30 @@ lower_compound_expr :: proc(state: ^State, expr_id: hir.Expr_Id) -> ir.Instructi
target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC,
})
case .Range:
args := make([]ir.Instruction_Id, 3, state.allocator)
args[0] = lower_nested_expr(state, expr.args[0])
args[1] = lower_nested_expr(state, expr.args[1])
args[2] = append_instruction(state, ir.Instruction{
op=.Const,
span=expr.span,
type=types.BOOL,
integer=expr.integer,
target=ir.INVALID_REF,
a=ir.INVALID_INSTRUCTION,
b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC,
})
return append_instruction(state, ir.Instruction{
op=.Aggregate,
span=expr.span,
type=expr.type,
args=args,
target=ir.INVALID_REF,
a=ir.INVALID_INSTRUCTION,
b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC,
})
case .None:
return append_instruction(state, ir.Instruction{
op=.None, span=expr.span, type=expr.type,
@@ -381,7 +417,7 @@ lower_expr :: proc(state: ^State, expr_id: hir.Expr_Id) -> ir.Instruction_Id {
target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, diagnostic=source.INVALID_DIAGNOSTIC,
})
_ = pop(&stack)
case .String, .Array, .Struct, .None, .Optional_Some, .Address, .Deref,
case .String, .Array, .Struct, .Range, .None, .Optional_Some, .Address, .Deref,
.Index, .Slice, .Field, .Length, .Slice_Ptr, .Unwrap, .Orelse,
.Not, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or:
last = lower_compound_expr(state, frame.expr)
@@ -762,6 +798,326 @@ 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 .For:
iterable_type := hir_module.exprs[statement.expr].type
if types.is_range(iterable_type, &hir_module.types) {
child := types.child_type(iterable_type, &hir_module.types)
range_value := lower_expr(state, statement.expr)
start := append_instruction(state, ir.Instruction{
op=.Extract, span=statement.span, type=child, integer=0,
target=ir.INVALID_REF, a=range_value, b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC,
})
end := append_instruction(state, ir.Instruction{
op=.Extract, span=statement.span, type=child, integer=1,
target=ir.INVALID_REF, a=range_value, b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC,
})
inclusive := append_instruction(state, ir.Instruction{
op=.Extract, span=statement.span, type=types.BOOL, integer=2,
target=ir.INVALID_REF, a=range_value, b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC,
})
current_slot := append_instruction(state, ir.Instruction{
op=.Alloca, span=statement.span, type=child,
target=ir.local_ref(ir.Local_Id(statement.local)),
a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC,
})
state.local_slots[statement.local] = current_slot
append_instruction(state, ir.Instruction{
op=.Store, span=statement.span, type=child,
target=ir.INVALID_REF, a=current_slot, b=start,
diagnostic=source.INVALID_DIAGNOSTIC,
})
condition_lbl := fresh_label(state)
body_lbl := fresh_label(state)
update_lbl := fresh_label(state)
exit_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,
})
current := append_instruction(state, ir.Instruction{
op=.Load, span=statement.span, type=child,
target=ir.INVALID_REF, a=current_slot, b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC,
})
less := append_instruction(state, ir.Instruction{
op=.Compare, span=statement.span, type=types.BOOL,
integer=i64(ir.Compare_Predicate.Lt),
target=ir.INVALID_REF, a=current, b=end,
diagnostic=source.INVALID_DIAGNOSTIC,
})
equal := append_instruction(state, ir.Instruction{
op=.Compare, span=statement.span, type=types.BOOL,
integer=i64(ir.Compare_Predicate.Eq),
target=ir.INVALID_REF, a=current, b=end,
diagnostic=source.INVALID_DIAGNOSTIC,
})
false_value := append_instruction(state, ir.Instruction{
op=.Const, span=statement.span, type=types.BOOL, integer=0,
target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC,
})
true_value := append_instruction(state, ir.Instruction{
op=.Const, span=statement.span, type=types.BOOL, integer=1,
target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC,
})
inclusive_args := make([]ir.Instruction_Id, 2, state.allocator)
inclusive_args[0] = equal
inclusive_args[1] = false_value
inclusive_equal := append_instruction(state, ir.Instruction{
op=.Select, span=statement.span, type=types.BOOL,
target=ir.INVALID_REF, a=inclusive, b=ir.INVALID_INSTRUCTION,
args=inclusive_args, diagnostic=source.INVALID_DIAGNOSTIC,
})
condition_args := make([]ir.Instruction_Id, 2, state.allocator)
condition_args[0] = true_value
condition_args[1] = inclusive_equal
condition := append_instruction(state, ir.Instruction{
op=.Select, span=statement.span, type=types.BOOL,
target=ir.INVALID_REF, a=less, b=ir.INVALID_INSTRUCTION,
args=condition_args, diagnostic=source.INVALID_DIAGNOSTIC,
})
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)
after_body := append_instruction(state, ir.Instruction{
op=.Load, span=statement.span, type=child,
target=ir.INVALID_REF, a=current_slot, b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC,
})
at_end := append_instruction(state, ir.Instruction{
op=.Compare, span=statement.span, type=types.BOOL,
integer=i64(ir.Compare_Predicate.Eq),
target=ir.INVALID_REF, a=after_body, b=end,
diagnostic=source.INVALID_DIAGNOSTIC,
})
append_instruction(state, ir.Instruction{
op=.Cond_Br, span=statement.span, type=types.VOID,
a=at_end, integer=exit_lbl, target=ir.Ref(u32(update_lbl)),
b=ir.INVALID_INSTRUCTION, diagnostic=source.INVALID_DIAGNOSTIC,
})
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,
})
one := append_instruction(state, ir.Instruction{
op=.Const, span=statement.span, type=child, integer=1,
target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC,
})
next := append_instruction(state, ir.Instruction{
op=.Add_Checked, span=statement.span, type=child,
target=ir.INVALID_REF, a=after_body, b=one,
diagnostic=source.INVALID_DIAGNOSTIC,
})
append_instruction(state, ir.Instruction{
op=.Store, span=statement.span, type=child,
target=ir.INVALID_REF, a=current_slot, b=next,
diagnostic=source.INVALID_DIAGNOSTIC,
})
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,
})
continue
}
item, item_ok := types.container(iterable_type, &hir_module.types)
if !item_ok {
append_instruction(state, ir.Instruction{
op=.Trap, span=statement.span, type=types.VOID,
target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION,
diagnostic=statement.diagnostic,
})
continue
}
iterable_value := ir.INVALID_INSTRUCTION
if types.is_array(iterable_type, &hir_module.types) {
if hir_expr_is_location(state, statement.expr) {
iterable_value = lower_location(state, statement.expr)
} else {
value := lower_expr(state, statement.expr)
iterable_value = append_instruction(state, ir.Instruction{
op=.Alloca, span=statement.span, type=iterable_type,
target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC,
})
append_instruction(state, ir.Instruction{
op=.Store, span=statement.span, type=iterable_type,
target=ir.INVALID_REF, a=iterable_value, b=value,
diagnostic=source.INVALID_DIAGNOSTIC,
})
}
} else {
iterable_value = lower_expr(state, statement.expr)
}
base := append_instruction(state, ir.Instruction{
op=.Slice_Ptr, span=statement.span, type=statement.iterator_type,
target=ir.INVALID_REF, a=iterable_value, b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC,
})
length := ir.INVALID_INSTRUCTION
if item.kind == .Array {
length = append_instruction(state, ir.Instruction{
op=.Const, span=statement.span, type=types.USIZE, integer=i64(item.count),
target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC,
})
} else {
length = append_instruction(state, ir.Instruction{
op=.Length, span=statement.span, type=types.USIZE,
target=ir.INVALID_REF, a=iterable_value, b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC,
})
}
counter_target := ir.INVALID_REF
if statement.index_local != hir.INVALID_LOCAL {
counter_target = ir.local_ref(ir.Local_Id(statement.index_local))
}
counter_slot := append_instruction(state, ir.Instruction{
op=.Alloca, span=statement.span, type=types.USIZE,
target=counter_target, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC,
})
if statement.index_local != hir.INVALID_LOCAL {
state.local_slots[statement.index_local] = counter_slot
}
zero := append_instruction(state, ir.Instruction{
op=.Const, span=statement.span, type=types.USIZE, integer=0,
target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC,
})
append_instruction(state, ir.Instruction{
op=.Store, span=statement.span, type=types.USIZE,
target=ir.INVALID_REF, a=counter_slot, b=zero,
diagnostic=source.INVALID_DIAGNOSTIC,
})
capture_type := state.func_locals[statement.local].type
capture_slot := append_instruction(state, ir.Instruction{
op=.Alloca, span=statement.span, type=capture_type,
target=ir.local_ref(ir.Local_Id(statement.local)),
a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC,
})
state.local_slots[statement.local] = capture_slot
condition_lbl := fresh_label(state)
body_lbl := fresh_label(state)
update_lbl := fresh_label(state)
exit_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,
})
index := append_instruction(state, ir.Instruction{
op=.Load, span=statement.span, type=types.USIZE,
target=ir.INVALID_REF, a=counter_slot, b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC,
})
condition := append_instruction(state, ir.Instruction{
op=.Compare, span=statement.span, type=types.BOOL,
integer=i64(ir.Compare_Predicate.Lt),
target=ir.INVALID_REF, a=index, b=length,
diagnostic=source.INVALID_DIAGNOSTIC,
})
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,
})
pointer_type := statement.iterator_type
if statement.pointer_capture {
pointer_type = capture_type
}
element_pointer := append_instruction(state, ir.Instruction{
op=.Pointer_Add, span=statement.span, type=pointer_type,
target=ir.INVALID_REF, a=base, b=index,
diagnostic=source.INVALID_DIAGNOSTIC,
})
captured := element_pointer
if !statement.pointer_capture {
captured = append_instruction(state, ir.Instruction{
op=.Load, span=statement.span, type=item.child,
target=ir.INVALID_REF, a=element_pointer, b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC,
})
}
append_instruction(state, ir.Instruction{
op=.Store, span=statement.span, type=capture_type,
target=ir.INVALID_REF, a=capture_slot, b=captured,
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,
})
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,
})
one := append_instruction(state, ir.Instruction{
op=.Const, span=statement.span, type=types.USIZE, integer=1,
target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC,
})
next := append_instruction(state, ir.Instruction{
op=.Add_Checked, span=statement.span, type=types.USIZE,
target=ir.INVALID_REF, a=index, b=one,
diagnostic=source.INVALID_DIAGNOSTIC,
})
append_instruction(state, ir.Instruction{
op=.Store, span=statement.span, type=types.USIZE,
target=ir.INVALID_REF, a=counter_slot, b=next,
diagnostic=source.INVALID_DIAGNOSTIC,
})
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,
})
}
}
}
+102 -1
View File
@@ -20,6 +20,7 @@ Parser :: struct {
file: ast.File_Id,
cursor: int,
delimiter_depth: int,
range_disabled: int,
// Suppresses `Name { ... }` struct-literal parsing at delimiter depth 0 so a
// control-flow condition like `if foo { ... }` does not swallow the block as a
// struct literal. Nested `(`/`[`/call-arg contexts (delimiter_depth > 0) still
@@ -630,6 +631,9 @@ parse_primary :: proc(parser: ^Parser, nesting: int) -> ast.Expr_Id {
if _, ok := allow(parser, .Right_Paren); !ok {
source.add(parser.diagnostics, current(parser).span, "expected ')'")
}
if expr != ast.INVALID_EXPR && int(expr) < len(parser.module.exprs) {
parser.module.exprs[expr].parenthesized = true
}
return expr
case .Invalid:
advance(parser)
@@ -649,6 +653,8 @@ parse_primary :: proc(parser: ^Parser, nesting: int) -> ast.Expr_Id {
infix_binding_power :: proc(kind: token.Kind) -> (left, right: int, ok: bool) {
#partial switch kind {
case .Range, .Range_Inclusive:
return 0, 1, true
case .Keyword_Orelse:
return 2, 3, true
case .Keyword_Or:
@@ -665,6 +671,7 @@ infix_binding_power :: proc(kind: token.Kind) -> (left, right: int, ok: bool) {
infix_expr_kind :: proc(kind: token.Kind) -> ast.Expr_Kind {
#partial switch kind {
case .Range, .Range_Inclusive: return .Range
case .Keyword_Orelse: return .Orelse
case .Keyword_Or: return .Or
case .Keyword_And: return .And
@@ -678,6 +685,18 @@ infix_expr_kind :: proc(kind: token.Kind) -> ast.Expr_Kind {
}
}
is_simple_range_bound :: proc(expr: ast.Expr) -> bool {
if expr.parenthesized {
return true
}
#partial switch expr.kind {
case .Integer, .Float, .String, .Bool, .Name:
return true
case:
return false
}
}
prefix_binding_power :: proc(kind: token.Kind) -> (right: int, ok: bool) {
#partial switch kind {
case .Minus, .Ampersand, .Bang:
@@ -762,7 +781,9 @@ parse_expression_bp :: proc(parser: ^Parser, minimum_binding_power, nesting: int
if _, ok := allow(parser, .Range); ok {
slicing = true
} else {
parser.range_disabled += 1
start_expr = parse_expression_bp(parser, 0, nesting+1)
parser.range_disabled -= 1
skip_newlines(parser)
if _, ok := allow(parser, .Range); ok {
slicing = true
@@ -770,7 +791,9 @@ parse_expression_bp :: proc(parser: ^Parser, minimum_binding_power, nesting: int
}
skip_newlines(parser)
if slicing && current(parser).kind != .Right_Bracket {
parser.range_disabled += 1
end_expr = parse_expression_bp(parser, 0, nesting+1)
parser.range_disabled -= 1
skip_newlines(parser)
}
end_token, ok := allow(parser, .Right_Bracket)
@@ -822,6 +845,10 @@ parse_expression_bp :: proc(parser: ^Parser, minimum_binding_power, nesting: int
continue
}
left_power, right_power, ok := infix_binding_power(current(parser).kind)
if ok && (current(parser).kind == .Range || current(parser).kind == .Range_Inclusive) &&
parser.range_disabled > 0 {
ok = false
}
if !ok || left_power < minimum_binding_power {
break
}
@@ -830,9 +857,26 @@ parse_expression_bp :: proc(parser: ^Parser, minimum_binding_power, nesting: int
right := parse_expression_bp(parser, right_power, nesting+1)
left_expr := parser.module.exprs[left]
right_expr := parser.module.exprs[right]
if operator.kind == .Range || operator.kind == .Range_Inclusive {
if !is_simple_range_bound(left_expr) {
source.add(
parser.diagnostics,
left_expr.span,
"range bounds with operators must be parenthesized",
)
}
if !is_simple_range_bound(right_expr) {
source.add(
parser.diagnostics,
right_expr.span,
"range bounds with operators must be parenthesized",
)
}
}
left = add_expr(parser, ast.Expr{
kind=infix_expr_kind(operator.kind),
span=span_from(left_expr.span, right_expr.span),
integer=1 if operator.kind == .Range_Inclusive else 0,
left=left,
right=right,
diagnostic=source.INVALID_DIAGNOSTIC,
@@ -931,6 +975,9 @@ parse_statement :: proc(parser: ^Parser) -> ast.Stmt_Id {
if current(parser).kind == .Keyword_While {
return parse_while(parser)
}
if current(parser).kind == .Keyword_For {
return parse_for(parser)
}
if current(parser).kind == .Identifier || current(parser).kind == .Underscore {
start_cursor := parser.cursor
@@ -1159,7 +1206,7 @@ parse_while_update :: proc(parser: ^Parser) -> ast.Stmt_Id {
statement := &parser.module.statements[update]
switch statement.kind {
case .Assignment, .Expression:
case .Invalid, .Declaration, .Return, .If, .While:
case .Invalid, .Declaration, .Return, .If, .While, .For:
diagnostic := source.add(
parser.diagnostics,
statement.span,
@@ -1187,6 +1234,60 @@ parse_while_update :: proc(parser: ^Parser) -> ast.Stmt_Id {
return update
}
parse_for :: proc(parser: ^Parser) -> ast.Stmt_Id {
start := advance(parser) // consume 'for'
skip_newlines(parser)
saved := parser.no_struct_literal
parser.no_struct_literal = true
iterable := parse_expression(parser)
parser.no_struct_literal = saved
skip_newlines(parser)
pointer_capture := false
item_name := symbol.INVALID
index_name := symbol.INVALID
if _, ok := allow(parser, .Pipe); !ok {
source.add(parser.diagnostics, current(parser).span, "expected '|' before for-loop captures")
} else {
if _, ok := allow(parser, .At); ok {
pointer_capture = true
}
item, item_ok := allow(parser, .Identifier)
if item_ok {
item_name = item.symbol
} else {
source.add(parser.diagnostics, current(parser).span, "expected a for-loop item capture")
}
if _, ok := allow(parser, .Comma); ok {
index, index_ok := allow(parser, .Identifier)
if index_ok {
index_name = index.symbol
} else {
source.add(parser.diagnostics, current(parser).span, "expected an index capture after ','")
}
}
if _, ok := allow(parser, .Pipe); !ok {
source.add(parser.diagnostics, current(parser).span, "expected '|' to close for-loop captures")
}
}
skip_newlines(parser)
body := parse_block(parser)
id := ast.stmt_id(len(parser.module.statements))
append(&parser.module.statements, ast.Stmt{
kind=.For,
span=span_from(start.span, previous(parser).span),
name=item_name,
index_name=index_name,
pointer_capture=pointer_capture,
expr=iterable,
body=body,
update=ast.INVALID_STMT,
diagnostic=source.INVALID_DIAGNOSTIC,
})
return id
}
parse_while :: proc(parser: ^Parser) -> ast.Stmt_Id {
start := advance(parser) // consume 'while'
skip_newlines(parser)
+2
View File
@@ -27,6 +27,7 @@ Kind :: enum u8 {
Minus,
Dot,
Range,
Range_Inclusive,
Ellipsis,
At,
Star,
@@ -55,6 +56,7 @@ Kind :: enum u8 {
Keyword_Or,
Keyword_If,
Keyword_While,
Keyword_For,
Keyword_Else,
Keyword_True,
Keyword_False,
+18 -4
View File
@@ -59,6 +59,7 @@ Kind :: enum u8 {
Array,
Pointer,
Slice,
Range,
Optional,
Function,
Named,
@@ -392,7 +393,7 @@ is_concrete_scalar :: proc(value: Type) -> bool {
is_concrete :: proc(value: Type, store: ^Store = nil) -> bool {
value_kind := kind(value, store)
if value_kind == .Scalar || value_kind == .Array || value_kind == .Pointer ||
value_kind == .Slice || value_kind == .Optional {
value_kind == .Slice || value_kind == .Range || value_kind == .Optional {
return true
}
if value_kind == .Struct || value_kind == .Union {
@@ -414,6 +415,10 @@ is_slice :: proc(value: Type, store: ^Store) -> bool {
return kind(value, store) == .Slice
}
is_range :: proc(value: Type, store: ^Store) -> bool {
return kind(value, store) == .Range
}
is_optional :: proc(value: Type, store: ^Store) -> bool {
return kind(value, store) == .Optional
}
@@ -486,7 +491,7 @@ is_runtime_value :: proc(value: Type, store: ^Store) -> bool {
if value_kind == .Scalar || value_kind == .Pointer {
return true
}
if value_kind == .Slice || value_kind == .Array || value_kind == .Optional {
if value_kind == .Slice || value_kind == .Array || value_kind == .Range || value_kind == .Optional {
return !contains_c_struct_by_value(value, store)
}
if value_kind == .Struct || value_kind == .Union {
@@ -521,7 +526,7 @@ contains_c_struct_by_value :: proc(value: Type, store: ^Store, depth := 0) -> bo
if item.kind == .Union {
return item.opaque || (item.c_layout && item.field_count == 0)
}
if item.kind == .Array || item.kind == .Slice || item.kind == .Optional {
if item.kind == .Array || item.kind == .Slice || item.kind == .Range || item.kind == .Optional {
return contains_c_struct_by_value(item.child, store, depth+1)
}
return false
@@ -665,6 +670,10 @@ slice :: proc(store: ^Store, child: Type, mutable: bool, has_sentinel := false,
})
}
range :: proc(store: ^Store, child: Type) -> Type {
return intern(store, Node{kind=.Range, child=child})
}
array :: proc(
store: ^Store,
child: Type,
@@ -805,6 +814,11 @@ size :: proc(value: Type, store: ^Store, selected := target.DEFAULT) -> u64 {
return u64(target.pointer_bits(selected)/8)
case .Slice:
return u64(target.pointer_bits(selected)/8*2)
case .Range:
child_size := size(child_type(value, store), store, selected)
child_align := u64(alignment_of(child_type(value, store), store, selected))
raw_size := child_size*2+1
return (raw_size+child_align-1)/child_align*child_align
case .Array:
item, _ := node(store, value)
return physical_count(value, store)*size(item.child, store, selected)
@@ -855,7 +869,7 @@ alignment_of :: proc(value: Type, store: ^Store, selected := target.DEFAULT) ->
return alignment(value, selected)
case .Pointer, .Slice:
return target.pointer_bits(selected)/8
case .Array, .Optional:
case .Array, .Range, .Optional:
return alignment_of(child_type(value, store), store, selected)
case .Function:
return 1
+469
View File
@@ -4080,3 +4080,472 @@ while_loop_allocas_are_emitted_in_the_entry_block :: proc(t: ^testing.T) {
}
testing.expect(t, alloca_count >= 3)
}
@(test)
for_loop_tokens_and_parser_capture_range_shape :: proc(t: ^testing.T) {
text := `main :: func() void {
for 0..4 |value| {
_ = value
}
items [1]mut i32 = [1]
for (&items) |@item, index| {
_ = item
_ = index
}
for 0..=1 |inclusive| {
_ = inclusive
}
}
`
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)
for_count := 0
range_count := 0
inclusive_count := 0
for tok in stream.items {
#partial switch tok.kind {
case .Keyword_For: for_count += 1
case .Range: range_count += 1
case .Range_Inclusive: inclusive_count += 1
case:
}
}
testing.expect_value(t, for_count, 3)
testing.expect_value(t, range_count, 1)
testing.expect_value(t, inclusive_count, 1)
module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&module)
testing.expect_value(t, len(diagnostics.items), 0)
function := module.functions[0]
first := module.statements[function.body[0]]
second := module.statements[function.body[2]]
third := module.statements[function.body[3]]
testing.expect_value(t, first.kind, ast.Stmt_Kind.For)
testing.expect(t, !first.pointer_capture)
testing.expect_value(t, module.exprs[first.expr].kind, ast.Expr_Kind.Range)
testing.expect_value(t, module.exprs[first.expr].integer, u64(0))
testing.expect_value(t, second.kind, ast.Stmt_Kind.For)
testing.expect(t, second.pointer_capture)
testing.expect(t, symbol.is_valid(second.index_name))
testing.expect_value(t, third.kind, ast.Stmt_Kind.For)
testing.expect_value(t, module.exprs[third.expr].integer, u64(1))
}
@(test)
range_bound_parenthesization_is_enforced :: proc(t: ^testing.T) {
text := `main :: func() void {
limit :: 3
for 0..limit + 1 |bad| {
_ = bad
}
for 0..(limit + 1) |good| {
_ = good
}
}
`
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)
module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&module)
found := 0
for diagnostic in diagnostics.items {
found += 1 if strings.contains(diagnostic.message, "range bounds with operators must be parenthesized") else 0
}
testing.expect_value(t, found, 1)
}
@(test)
parser_diagnoses_malformed_for_captures :: proc(t: ^testing.T) {
cases := [4]struct {
text: string,
needle: string,
}{
{`main :: func() void {
for [1] item {}
}
`, "expected '|' before for-loop captures"},
{`main :: func() void {
for [1] |@| {}
}
`, "expected a for-loop item capture"},
{`main :: func() void {
for [1] |item,| {}
}
`, "expected an index capture after ','"},
{`main :: func() void {
for [1] |item {}
}
`, "expected '|' to close for-loop captures"},
}
for test_case in cases {
source_file := source.Source{path="test.bro", text=test_case.text}
diagnostics := source.init_diagnostics(&source_file)
symbols := symbol.init_table()
stream := lexer.lex(&source_file, &diagnostics, &symbols)
module := parser.parse(&stream, &source_file, &diagnostics)
found := false
for diagnostic in diagnostics.items {
found = found || strings.contains(diagnostic.message, test_case.needle)
}
testing.expect(t, found)
ast.destroy_module(&module)
delete(stream.items)
symbol.destroy_table(&symbols)
source.destroy_diagnostics(&diagnostics)
}
}
@(test)
for_loops_compile_and_run :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-for-loop"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/for_loop", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 42)
}
@(test)
range_loop_edges_compile_and_run :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-for-loop-edges"
defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/for_loop_edges", output)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 42)
}
@(test)
for_loop_diagnostics_cover_iterables_captures_and_scope :: proc(t: ^testing.T) {
text := `bad_iterable :: func() void {
for 1 |item| {
_ = item
}
}
bad_array_pointer_capture :: func() void {
for [1] |@item| {
_ = item
}
}
bad_range_pointer_capture :: func() void {
for 0..1 |@item| {
_ = item
}
}
bad_range_index_capture :: func() void {
for 0..1 |item, index| {
_ = item
_ = index
}
}
bad_duplicate_capture :: func() void {
for [1] |item, item| {
_ = item
}
}
bad_capture_redeclaration :: func() void {
for [1] |item| {
item i32 = 2
_ = item
}
}
bad_capture_assignment :: func() void {
for [1] |item| {
item = 2
}
}
bad_immutable_pointer_capture :: func() void {
items :: [1]
for (&items) |@item| {
item^ = 2
}
}
bad_scope :: func() void {
for [1] |item| {
_ = item
}
_ = item
}
bad_integer_bounds :: func() void {
start i32 = 0
end u32 = 1
for start..end |item| {
_ = item
}
}
bad_float_bounds :: func() void {
for 0.0..1.0 |item| {
_ = item
}
}
main :: func() void {
bad_iterable()
bad_array_pointer_capture()
bad_range_pointer_capture()
bad_range_index_capture()
bad_duplicate_capture()
bad_capture_redeclaration()
bad_capture_assignment()
bad_immutable_pointer_capture()
bad_scope()
bad_integer_bounds()
bad_float_bounds()
}
`
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)
unsupported := false
array_pointer := false
range_pointer := false
range_index := false
duplicate_capture := false
redeclaration := false
immutable := false
immutable_pointer := false
scope := false
integer_bounds := 0
for diagnostic in diagnostics.items {
unsupported = unsupported || strings.contains(diagnostic.message, "for-loop iterable must be a range")
array_pointer = array_pointer || strings.contains(diagnostic.message, "pointer capture over an array requires")
range_pointer = range_pointer || strings.contains(diagnostic.message, "range loops do not support pointer captures")
range_index = range_index || strings.contains(diagnostic.message, "range loops do not support index captures")
duplicate_capture = duplicate_capture || strings.contains(diagnostic.message, "for-loop captures must have distinct names")
redeclaration = redeclaration || strings.contains(diagnostic.message, "duplicate local 'item'")
immutable = immutable || strings.contains(diagnostic.message, "cannot assign immutable local 'item'")
immutable_pointer = immutable_pointer || strings.contains(diagnostic.message, "assignment target is not writable")
scope = scope || strings.contains(diagnostic.message, "unresolved global 'item'")
integer_bounds += 1 if strings.contains(diagnostic.message, "range bounds must be compatible concrete integers") else 0
}
testing.expect(t, unsupported)
testing.expect(t, array_pointer)
testing.expect(t, range_pointer)
testing.expect(t, range_index)
testing.expect(t, duplicate_capture)
testing.expect(t, redeclaration)
testing.expect(t, immutable)
testing.expect(t, immutable_pointer)
testing.expect(t, scope)
testing.expect_value(t, integer_bounds, 2)
}
@(test)
for_pointer_capture_respects_pointer_and_array_mutability :: proc(t: ^testing.T) {
text := `readonly :: func() void {
values [1]mut i32 = [1]
items @[1]mut i32 = &values
items[0] = 7
for items |@item| {
item^ = 7
}
}
writable :: func() void {
values [1]mut i32 = [1]
items @mut [1]mut i32 = &values
items[0] = 7
for items |@item| {
item^ = 7
}
}
main :: func() void {
readonly()
writable()
}
`
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)
readonly_errors := 0
for diagnostic in diagnostics.items {
readonly_errors += 1 if strings.contains(diagnostic.message, "assignment target is not writable") else 0
}
testing.expect_value(t, readonly_errors, 2)
}
@(test)
pointer_field_passthrough_respects_pointee_mutability :: proc(t: ^testing.T) {
text := `Point :: struct {
x i32
}
readonly :: func(point @Point) i32 {
return point.x
}
bad_write :: func(point @Point) void {
point.x = 7
}
writable :: func(point @mut Point) void {
point.x += 1
}
main :: func() i32 {
point Point = Point { x = 41 }
writable(&point)
bad_write(&point)
return readonly(&point)
}
`
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)
readonly_errors := 0
for diagnostic in diagnostics.items {
readonly_errors += 1 if strings.contains(diagnostic.message, "assignment target is not writable") else 0
}
testing.expect_value(t, readonly_errors, 1)
}
@(test)
equal_range_returns_infer_a_usable_result_type :: proc(t: ^testing.T) {
text := `choose :: func(first bool) int {
if first {
return 0..1
}
return 2..3
}
main :: func() i32 {
total i32 = 0
for choose(false) |value| {
total = total + value
}
return total
}
`
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)
found := false
for function in hir_module.functions {
if symbol.resolve(&symbols, function.name) == "choose" {
found = true
testing.expect(t, types.is_range(function.result, &hir_module.types))
testing.expect_value(t, types.child_type(function.result, &hir_module.types), types.I8)
}
}
testing.expect(t, found)
testing.expect(t, strings.contains(llvm_text, "extractvalue"))
}
@(test)
for_loop_lowering_evaluates_once_and_avoids_index_bounds_checks :: proc(t: ^testing.T) {
text := `make_range :: func() int {
return 0..2
}
make_array :: func() int {
return [1, 2]
}
main :: func() i32 {
total i32 = 0
for make_range() |value| {
total = total + value
}
for make_array() |value| {
total = total + value
}
return total
}
`
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)
call_count := 0
extract_count := 0
select_count := 0
pointer_add_count := 0
index_address_count := 0
first_loop_label := find_substring_offset(llvm_text, "bro_block_")
testing.expect(t, first_loop_label >= 0)
for function in ir_module.functions {
if !function.is_main {
continue
}
for instruction, instruction_index in function.instructions {
#partial switch instruction.op {
case .Call: call_count += 1
case .Extract: extract_count += 1
case .Select: select_count += 1
case .Pointer_Add: pointer_add_count += 1
case .Index_Address: index_address_count += 1
case .Alloca:
needle := fmt.tprintf(" %%v%d = alloca ", instruction_index)
offset := find_substring_offset(llvm_text, needle)
testing.expect(t, offset >= 0 && offset < first_loop_label)
case:
}
}
}
testing.expect_value(t, call_count, 2)
testing.expect_value(t, extract_count, 3)
testing.expect_value(t, select_count, 2)
testing.expect(t, pointer_add_count >= 1)
testing.expect_value(t, index_address_count, 0)
testing.expect(t, !strings.contains(llvm_text, "index_ok"))
}
+52
View File
@@ -0,0 +1,52 @@
# Milestone 5: ranges and sequence for loops.
pass :: func(value int) int {
return value
}
main :: func() i32 {
total i32 = 0
items [3]mut i32 = [1, 2, 3]
for items |item, index| {
total = total + item
_ = index
}
for (&items) |@item| {
item^ = item^ + 1
}
view []mut i32 = (&items)[..]
for view |@item, index| {
item^ = item^ + 1
_ = index
}
for view |item| {
total = total + item
}
for [4, 5] |item| {
total = total + item
}
for "ab" |byte| {
_ = byte
total = total + 1
}
for 0..4 |value| {
total = total + value
}
for 1..=3 |value| {
total = total + value
}
once :: 0..1
for pass(once) |value| {
_ = value
total = total + 1
}
return total
}
+70
View File
@@ -0,0 +1,70 @@
make_range :: func(calls @mut i32, end usize) int {
calls^ = calls^ + 1
return 0..end
}
global_range :: 0..1
main :: func() i32 {
total i32 = 0
first u8 = 254
last u8 = 255
for first..=last |value| {
_ = value
total = total + 1
}
signed_start i8 = -2
signed_end i8 = 1
for signed_start..signed_end |value| {
_ = value
total = total + 1
}
limit usize = 3
for 0..(limit + 1) |value| {
_ = value
total = total + 1
}
for 0..2 |outer| {
_ = outer
for 0..3 |inner| {
_ = inner
total = total + 1
}
}
for 5..2 |value| {
_ = value
total = total + 100
}
empty [0]i32 = []
for empty |value| {
_ = value
total = total + 100
}
pointed i32 = 3
pointers [1]@mut i32 = [&pointed]
for pointers |pointer| {
total = total + pointer^
}
calls i32 = 0
for make_range(&calls, 2) |value| {
_ = value
total = total + 1
}
for global_range |value| {
_ = value
total = total + 1
}
if calls == 1 {
total = total + 21
}
return total
}