diff --git a/LANGUAGE.md b/LANGUAGE.md index 24d0f31..af04f88 100644 --- a/LANGUAGE.md +++ b/LANGUAGE.md @@ -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 diff --git a/TODO.md b/TODO.md index 6841bce..85bbf2e 100644 --- a/TODO.md +++ b/TODO.md @@ -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 diff --git a/compiler/ast/ast.odin b/compiler/ast/ast.odin index 45189c0..e49dca0 100644 --- a/compiler/ast/ast.odin +++ b/compiler/ast/ast.odin @@ -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, diff --git a/compiler/checker/checker.odin b/compiler/checker/checker.odin index f6c1334..ae0ccb9 100644 --- a/compiler/checker/checker.odin +++ b/compiler/checker/checker.odin @@ -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{ diff --git a/compiler/hir/hir.odin b/compiler/hir/hir.odin index d6f408f..188a8a0 100644 --- a/compiler/hir/hir.odin +++ b/compiler/hir/hir.odin @@ -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, diff --git a/compiler/ir/ir.odin b/compiler/ir/ir.odin index c930c37..b9cae36 100644 --- a/compiler/ir/ir.odin +++ b/compiler/ir/ir.odin @@ -84,6 +84,8 @@ Opcode :: enum u8 { Slice, Length, Slice_Ptr, + Extract, + Select, Unwrap, Optional_Is_Some, Optional_Value, diff --git a/compiler/lexer/lexer.odin b/compiler/lexer/lexer.odin index ea6f271..fd8331c 100644 --- a/compiler/lexer/lexer.odin +++ b/compiler/lexer/lexer.odin @@ -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) } diff --git a/compiler/llvm/llvm.odin b/compiler/llvm/llvm.odin index 45c9f5a..359a0a0 100644 --- a/compiler/llvm/llvm.odin +++ b/compiler/llvm/llvm.odin @@ -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: diff --git a/compiler/lower/lower.odin b/compiler/lower/lower.odin index 6d8143f..bd9b93b 100644 --- a/compiler/lower/lower.odin +++ b/compiler/lower/lower.odin @@ -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, + }) } } } diff --git a/compiler/parser/parser.odin b/compiler/parser/parser.odin index c7b8bff..27575ad 100644 --- a/compiler/parser/parser.odin +++ b/compiler/parser/parser.odin @@ -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) diff --git a/compiler/token/token.odin b/compiler/token/token.odin index 921a7c4..4060360 100644 --- a/compiler/token/token.odin +++ b/compiler/token/token.odin @@ -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, diff --git a/compiler/types/types.odin b/compiler/types/types.odin index 7624cf6..185a68e 100644 --- a/compiler/types/types.odin +++ b/compiler/types/types.odin @@ -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 diff --git a/compiler_tests.odin b/compiler_tests.odin index 8001b9d..e8500d4 100644 --- a/compiler_tests.odin +++ b/compiler_tests.odin @@ -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")) +} diff --git a/examples/programs/for_loop/main.bro b/examples/programs/for_loop/main.bro new file mode 100644 index 0000000..c63d038 --- /dev/null +++ b/examples/programs/for_loop/main.bro @@ -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 +} diff --git a/examples/programs/for_loop_edges/main.bro b/examples/programs/for_loop_edges/main.bro new file mode 100644 index 0000000..e616e43 --- /dev/null +++ b/examples/programs/for_loop_edges/main.bro @@ -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 +}