diff --git a/TODO.md b/TODO.md index 32441a3..24eaef1 100644 --- a/TODO.md +++ b/TODO.md @@ -166,6 +166,23 @@ no hand-writable spelling and are emitted as `# unsupported in bindings:` comments (functions that reference an un-spellable union therefore keep a dangling reference) +12. `undefined` as inspired by zig (implemented): + - allow mutable local declarations with `undefined` + - undefined values are assigned a poison value (0xaa...) + - allows for something like: + ``` + a int = undefined + if (condition) { + a = 42 + } else { + a = -2 + } + ``` + - disallow: `b :: undefined` since assigning undefined to something that can't change defeats the purpose + - disallow assigning `undefined` after declaration; use optionals and `none` for values that intentionally move back to an empty state + +13. broaden type inference from surrounding context + ## A word on multi-unwrap Unwrap multiple optionals with `and`. This **short-circuits**: if the first optional is none, subsequent expressions are not evaluated. diff --git a/compiler/ast/ast.odin b/compiler/ast/ast.odin index 22e0ed3..a4f5b73 100644 --- a/compiler/ast/ast.odin +++ b/compiler/ast/ast.odin @@ -71,6 +71,7 @@ Expr_Kind :: enum u8 { Bool, Array, None, + Undefined, Name, Enum_Literal, Address, diff --git a/compiler/checker/checker.odin b/compiler/checker/checker.odin index dddb2de..c9e9a37 100644 --- a/compiler/checker/checker.odin +++ b/compiler/checker/checker.odin @@ -33,8 +33,11 @@ Spec :: struct { } Infer_Local :: struct { - name: symbol.Id, - type: types.Type, + name: symbol.Id, + type: types.Type, + declared: types.Type, + statement: ast.Stmt_Id, + mutable: bool, } Build_Local :: struct { @@ -53,6 +56,7 @@ Build_Ctx :: struct { pkg: ast.Package_Id, file: ast.File_Id, result: types.Type, + local_types: []types.Type, locals: ^[dynamic]Build_Local, hir_locals: ^[dynamic]hir.Local, global_reads: ^[dynamic]hir.Global_Id, @@ -262,6 +266,13 @@ is_runtime_type :: proc(checker: ^Checker, value: types.Type) -> bool { return types.is_runtime_value(value, &checker.module.types) } +is_undefined_expr :: proc(checker: ^Checker, expr_id: ast.Expr_Id) -> bool { + if expr_id == ast.INVALID_EXPR || int(expr_id) >= len(checker.ast_module.exprs) { + return false + } + return checker.ast_module.exprs[expr_id].kind == .Undefined +} + string_literal_type :: proc(checker: ^Checker, string_id: u64) -> types.Type { length: u64 if string_id < u64(len(checker.ast_module.strings)) { @@ -636,7 +647,7 @@ mark_expr_imports_used :: proc(checker: ^Checker, expr_id: ast.Expr_Id, file: as append(&stack, expr.left) case .Add, .Sub, .Mul, .Div, .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, .Enum_Literal: + case .Invalid, .Integer, .Float, .String, .Bool, .None, .Undefined, .Name, .Enum_Literal: } } } @@ -956,14 +967,21 @@ validate_type_nodes :: proc(checker: ^Checker) { } find_infer_local :: proc(locals: []Infer_Local, name: symbol.Id) -> types.Type { - for index := len(locals) - 1; index >= 0; index -= 1 { - if locals[index].name == name { - return locals[index].type - } + if index, ok := find_infer_local_index(locals, name); ok { + return locals[index].type } return types.INVALID } +find_infer_local_index :: proc(locals: []Infer_Local, name: symbol.Id) -> (int, bool) { + for index := len(locals) - 1; index >= 0; index -= 1 { + if locals[index].name == name { + return index, true + } + } + return -1, false +} + find_spec :: proc(checker: ^Checker, template: ast.Function_Id, actual_args: []types.Type) -> Spec_Id { function := checker.ast_module.functions[template] for spec, index in checker.specs { @@ -1123,6 +1141,8 @@ infer_compound_expr :: proc( return types.array(store, element, u64(len(expr.args)), false) case .None: return types.INVALID + case .Undefined: + return types.INVALID case .Enum_Literal: return types.INVALID case .Address: @@ -1247,7 +1267,7 @@ infer_expr :: proc( case .Float: last = types.F64 _ = pop(&stack) - case .String, .Array, .None, .Address, .Deref, .Index, .Slice, + case .String, .Array, .None, .Undefined, .Address, .Deref, .Index, .Slice, .Field, .Unwrap, .Orelse, .Struct_Literal, .Keyed, .Enum_Literal, .Bool, .Not, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or, .Range: last = infer_compound_expr(checker, expr, locals, pkg, file, demanded) @@ -1505,34 +1525,127 @@ flatten_conditional_unwrap_operands :: proc( append(operands, expr_id) } +record_infer_local_type :: proc(local: Infer_Local, local_types: []types.Type) { + if local.statement != ast.INVALID_STMT && int(local.statement) < len(local_types) { + local_types[local.statement] = local.type + } +} + +merge_infer_local_type :: proc( + checker: ^Checker, + local: ^Infer_Local, + inferred: types.Type, + local_types: []types.Type, +) -> bool { + if !is_runtime_type(checker, inferred) { + return false + } + if types.is_constraint(local.declared) { + if !types.is_concrete_integer(inferred) { + return false + } + if !is_runtime_type(checker, local.type) { + local.type = inferred + record_infer_local_type(local^, local_types) + return true + } + if types.equal(local.type, inferred) { + return false + } + merged := types.widest(local.type, inferred) + if types.is_concrete_integer(merged) { + local.type = merged + record_infer_local_type(local^, local_types) + return true + } + return false + } + if is_runtime_type(checker, local.declared) { + local.type = local.declared + record_infer_local_type(local^, local_types) + return false + } + if !is_runtime_type(checker, local.type) { + local.type = inferred + record_infer_local_type(local^, local_types) + return true + } + if types.equal(local.type, inferred) { + return false + } + merged := types.widest(local.type, inferred) + if types.is_concrete_scalar(merged) { + local.type = merged + record_infer_local_type(local^, local_types) + return true + } + return false +} + infer_statements :: proc( checker: ^Checker, statements: []ast.Stmt_Id, locals: ^[dynamic]Infer_Local, + local_types: []types.Type, pkg: ast.Package_Id, file: ast.File_Id, demanded: ^[dynamic]Spec_Id, result: ^types.Type, + result_hint := types.INVALID, ) { scope_start := len(locals^) for statement_id in statements { statement := checker.ast_module.statements[statement_id] #partial switch statement.kind { case .Declaration: - value_type := infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded) - declared_local := type_from_syntax(statement.type) + declared_local := resolve_inferred_array(checker, type_from_syntax(statement.type), statement.expr) + value_type := types.INVALID + if !is_undefined_expr(checker, statement.expr) { + value_type = infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded) + } if is_runtime_type(checker, declared_local) { value_type = declared_local } - append(locals, Infer_Local{name = statement.name, type = value_type}) - case .Assignment, .Expression: + local := Infer_Local{ + name=statement.name, + type=value_type, + declared=declared_local, + statement=statement_id, + mutable=!statement.immutable, + } + append(locals, local) + record_infer_local_type(local, local_types) + case .Assignment: + value_type := infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded) if statement.target != ast.INVALID_EXPR { _ = infer_expr(checker, statement.target, locals^[:], pkg, file, demanded) + target_expr := checker.ast_module.exprs[statement.target] + if target_expr.kind == .Name && !symbol.is_valid(target_expr.qualifier) { + if local_index, ok := find_infer_local_index(locals^[:], target_expr.name); ok && + locals^[local_index].mutable { + _ = merge_infer_local_type(checker, &locals^[local_index], value_type, local_types) + } + } + } else if statement.name != checker.sink_symbol { + if local_index, ok := find_infer_local_index(locals^[:], statement.name); ok && + locals^[local_index].mutable { + _ = merge_infer_local_type(checker, &locals^[local_index], value_type, local_types) + } } + case .Expression: _ = infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded) case .Return: if statement.expr != ast.INVALID_EXPR { returned := infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded) + if is_runtime_type(checker, result_hint) { + expr := checker.ast_module.exprs[statement.expr] + if expr.kind == .Name && !symbol.is_valid(expr.qualifier) { + if local_index, ok := find_infer_local_index(locals^[:], expr.name); ok { + _ = merge_infer_local_type(checker, &locals^[local_index], result_hint, local_types) + returned = result_hint + } + } + } if !types.is_valid(result^) { result^ = returned } else if !types.equal(result^, returned) { @@ -1558,27 +1671,27 @@ infer_statements :: proc( types.is_optional(operand_types[index], &checker.module.types) { capture_type = types.child_type(operand_types[index], &checker.module.types) } - append(locals, Infer_Local{name=capture, type=capture_type}) + append(locals, Infer_Local{name=capture, type=capture_type, declared=capture_type, statement=ast.INVALID_STMT}) } if statement.guard != ast.INVALID_EXPR { _ = infer_expr(checker, statement.guard, locals^[:], pkg, file, demanded) } - infer_statements(checker, statement.body, locals, pkg, file, demanded, result) + infer_statements(checker, statement.body, locals, local_types, pkg, file, demanded, result, result_hint) resize(locals, capture_start) - infer_statements(checker, statement.else_body, locals, pkg, file, demanded, result) + infer_statements(checker, statement.else_body, locals, local_types, pkg, file, demanded, result, result_hint) delete(operand_types, checker.allocator) delete(operands) } else { _ = infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded) - infer_statements(checker, statement.body, locals, pkg, file, demanded, result) - infer_statements(checker, statement.else_body, locals, pkg, file, demanded, result) + infer_statements(checker, statement.body, locals, local_types, pkg, file, demanded, result, result_hint) + infer_statements(checker, statement.else_body, locals, local_types, pkg, file, demanded, result, result_hint) } case .While: _ = infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded) - infer_statements(checker, statement.body, locals, pkg, file, demanded, result) + infer_statements(checker, statement.body, locals, local_types, pkg, file, demanded, result, result_hint) if statement.update != ast.INVALID_STMT { update := [1]ast.Stmt_Id{statement.update} - infer_statements(checker, update[:], locals, pkg, file, demanded, result) + infer_statements(checker, update[:], locals, local_types, pkg, file, demanded, result, result_hint) } case .For: iterable_type := infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded) @@ -1596,43 +1709,55 @@ infer_statements :: proc( } } if symbol.is_valid(statement.name) { - append(locals, Infer_Local{name=statement.name, type=capture_type}) + append(locals, Infer_Local{name=statement.name, type=capture_type, declared=capture_type, statement=ast.INVALID_STMT}) } if symbol.is_valid(statement.index_name) { - append(locals, Infer_Local{name=statement.index_name, type=types.USIZE}) + append(locals, Infer_Local{name=statement.index_name, type=types.USIZE, declared=types.USIZE, statement=ast.INVALID_STMT}) } - infer_statements(checker, statement.body, locals, pkg, file, demanded, result) + infer_statements(checker, statement.body, locals, local_types, pkg, file, demanded, result, result_hint) resize(locals, capture_start) } } resize(locals, scope_start) } -infer_spec_result :: proc(checker: ^Checker, id: Spec_Id, demanded: ^[dynamic]Spec_Id = nil) -> types.Type { +infer_spec_locals_and_result :: proc( + checker: ^Checker, + id: Spec_Id, + demanded: ^[dynamic]Spec_Id = nil, +) -> ([]types.Type, types.Type) { spec := checker.specs[id] function := checker.ast_module.functions[spec.template] declared := type_from_syntax(function.result) if function.pkg == 0 && function.name == checker.main_symbol && function.result == types.INT { declared = types.I32 } + result_hint := declared if is_runtime_type(checker, declared) else types.INVALID locals: [dynamic]Infer_Local locals.allocator = checker.allocator defer delete(locals) + local_types := make([]types.Type, len(checker.ast_module.statements), checker.allocator) for param, index in function.params { param_type := types.INVALID if index < len(spec.args) { param_type = spec.args[index] } - append(&locals, Infer_Local{name = param.name, type = param_type}) + append(&locals, Infer_Local{name=param.name, type=param_type, declared=param_type, statement=ast.INVALID_STMT}) } result := types.INVALID - infer_statements(checker, function.body, &locals, function.pkg, function.file, demanded, &result) + infer_statements(checker, function.body, &locals, local_types, function.pkg, function.file, demanded, &result, result_hint) if types.is_constraint(declared) { - return result + return local_types, result } - return declared + return local_types, declared +} + +infer_spec_result :: proc(checker: ^Checker, id: Spec_Id, demanded: ^[dynamic]Spec_Id = nil) -> types.Type { + local_types, result := infer_spec_locals_and_result(checker, id, demanded) + delete(local_types, checker.allocator) + return result } merge_inferred_type :: proc(store: ^types.Store, current: ^types.Type, inferred: types.Type) -> bool { @@ -2296,7 +2421,7 @@ build_compound_expr :: proc( infer_locals := make([]Infer_Local, len(locals), checker.allocator) defer delete(infer_locals, checker.allocator) for local, index in locals { - infer_locals[index] = Infer_Local{name=local.name, type=local.type} + infer_locals[index] = Infer_Local{name=local.name, type=local.type, declared=local.type, statement=ast.INVALID_STMT} } for arg in expr.args { actual := infer_nested_expr(checker, arg, infer_locals, pkg, file, nil) @@ -2332,6 +2457,13 @@ build_compound_expr :: proc( kind=.None, span=expr.span, type=expected, target=hir.INVALID_REF, left=hir.INVALID_EXPR, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC, }) + case .Undefined: + id := source.add( + checker.diagnostics, + expr.span, + "'undefined' is only valid as a mutable local declaration initializer", + ) + return invalid_hir_expr(checker, expr.span, id, expected) case .Enum_Literal: if !types.is_enum(expected, store) { id := source.addf( @@ -2762,7 +2894,7 @@ build_expr :: proc( continue } switch expr.kind { - case .String, .Array, .None, .Address, .Deref, .Index, .Slice, + case .String, .Array, .None, .Undefined, .Address, .Deref, .Index, .Slice, .Field, .Unwrap, .Orelse, .Struct_Literal, .Keyed, .Bool, .Not, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or, .Range, .Enum_Literal: @@ -3290,22 +3422,62 @@ build_block :: proc( switch statement.kind { case .Declaration: declared := resolve_inferred_array(checker, type_from_syntax(statement.type), statement.expr) - expected := types.INVALID - if is_runtime_type(checker, declared) { - expected = declared + if statement_id != ast.INVALID_STMT && int(statement_id) < len(ctx.local_types) && + is_runtime_type(checker, ctx.local_types[statement_id]) && + (types.is_constraint(declared) || is_undefined_expr(checker, statement.expr)) { + declared = ctx.local_types[statement_id] } - value := build_expr( - checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, - expected, ctx.pkg, ctx.file, - ) - value_type := checker.module.exprs[value].type - if is_runtime_type(checker, declared) { - value = coerce_expr(checker, value, declared, statement.span) + expected := types.INVALID + value := hir.INVALID_EXPR + value_type := types.INVALID + if is_undefined_expr(checker, statement.expr) { + if statement.immutable { + id := source.add( + checker.diagnostics, + statement.span, + "'undefined' requires a mutable local declaration", + ) + append(&body, hir.stmt_id(len(checker.module.statements))) + append(&checker.module.statements, hir.Stmt{ + kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR, + local = hir.INVALID_LOCAL, diagnostic = id, + }) + ctx.problematic^ = true + continue + } + if !is_runtime_type(checker, declared) { + id := source.addf( + checker.diagnostics, + statement.span, + "could not infer a concrete type for local '%s'", + symbol_text(checker, statement.name), + ) + append(&body, hir.stmt_id(len(checker.module.statements))) + append(&checker.module.statements, hir.Stmt{ + kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR, + local = hir.INVALID_LOCAL, diagnostic = id, + }) + ctx.problematic^ = true + continue + } + value_type = declared + } else { + if is_runtime_type(checker, declared) { + expected = declared + } + value = build_expr( + checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, + expected, ctx.pkg, ctx.file, + ) value_type = checker.module.exprs[value].type - } else if types.is_void(declared) { - id := source.add(checker.diagnostics, statement.span, "locals cannot have type void") - value = invalid_hir_expr(checker, statement.span, id) - value_type = types.INVALID + if is_runtime_type(checker, declared) { + value = coerce_expr(checker, value, declared, statement.span) + value_type = checker.module.exprs[value].type + } else if types.is_void(declared) { + id := source.add(checker.diagnostics, statement.span, "locals cannot have type void") + value = invalid_hir_expr(checker, statement.span, id) + value_type = types.INVALID + } } if _, found := find_build_local(ctx.locals^[duplicate_start:], statement.name); found { id := source.addf( @@ -3332,7 +3504,9 @@ build_block :: proc( kind = .Declaration, span = statement.span, local = local_id, expr = value, diagnostic = source.INVALID_DIAGNOSTIC, }) - ctx.problematic^ = ctx.problematic^ || checker.module.exprs[value].kind == .Invalid + if value != hir.INVALID_EXPR { + ctx.problematic^ = ctx.problematic^ || checker.module.exprs[value].kind == .Invalid + } case .Assignment: if statement.target != ast.INVALID_EXPR { target_expr := build_expr( @@ -3888,6 +4062,13 @@ build_function :: proc(checker: ^Checker, id: Spec_Id) { global_reads.allocator = checker.allocator calls: [dynamic]hir.Function_Id calls.allocator = checker.allocator + demanded: [dynamic]Spec_Id + demanded.allocator = checker.allocator + local_types, _ := infer_spec_locals_and_result(checker, id, &demanded) + defer { + delete(local_types, checker.allocator) + delete(demanded) + } for param, index in function.params { local_id := hir.local_id(len(hir_locals)) @@ -3946,6 +4127,7 @@ build_function :: proc(checker: ^Checker, id: Spec_Id) { pkg = function.pkg, file = function.file, result = spec.result, + local_types = local_types, locals = &locals, hir_locals = &hir_locals, global_reads = &global_reads, diff --git a/compiler/ir/ir.odin b/compiler/ir/ir.odin index dd33173..791db24 100644 --- a/compiler/ir/ir.odin +++ b/compiler/ir/ir.odin @@ -81,6 +81,7 @@ Opcode :: enum u8 { Field_Address, Load, Store, + Fill, Slice, Length, Slice_Ptr, diff --git a/compiler/lexer/lexer.odin b/compiler/lexer/lexer.odin index c365d90..733c330 100644 --- a/compiler/lexer/lexer.odin +++ b/compiler/lexer/lexer.odin @@ -25,6 +25,7 @@ keyword_kind :: proc(text: string) -> token.Kind { case "return": return .Keyword_Return case "mut": return .Keyword_Mut case "none": return .Keyword_None + case "undefined": return .Keyword_Undefined case "orelse": return .Keyword_Orelse case "and": return .Keyword_And case "or": return .Keyword_Or diff --git a/compiler/llvm/llvm.odin b/compiler/llvm/llvm.odin index 68a6f9e..0d1379c 100644 --- a/compiler/llvm/llvm.odin +++ b/compiler/llvm/llvm.odin @@ -260,7 +260,7 @@ valid_value :: proc( .Neg_Checked, .Add_Checked, .Sub_Checked, .Mul_Checked, .Div_Checked, .Pointer_Add, .Not, .Compare, .Call: return true case .Address_Global, .Alloca, .Index_Address, .Field_Address, .Orelse_Begin, - .Store, .Trap, .Label, .Br, .Cond_Br, .Return, .Return_Void: + .Store, .Fill, .Trap, .Label, .Br, .Cond_Br, .Return, .Return_Void: return false } return false @@ -964,6 +964,17 @@ emit_instruction_stream :: proc( fmt.sbprintf(&emitter.builder, " store %s ", llvm_type(instruction.type, &emitter.module.types)) write_operand(&emitter.builder, instructions, instruction.b, instruction.type, &emitter.module.types) fmt.sbprintf(&emitter.builder, ", ptr %%v%d\n", instruction.a) + case .Fill: + if !valid_address(instructions, instruction.a, instruction.type, &emitter.module.types) { + emit_recovery_value(emitter, instruction_index, instruction, "invalid fill slot") + continue + } + fmt.sbprintf( + &emitter.builder, + " call void @llvm.memset.p0.i64(ptr %%v%d, i8 -86, i64 %d, i1 false)\n", + instruction.a, + types.size(instruction.type, &emitter.module.types, emitter.module.target), + ) case .Slice: if !valid_instruction(instructions, instruction.a) { emit_recovery_value(emitter, instruction_index, instruction, "invalid slice container") @@ -1990,7 +2001,7 @@ emit_messages :: proc(emitter: ^Emitter) { } emit_declarations :: proc(emitter: ^Emitter) { - strings.write_string(&emitter.builder, "declare i64 @write(i32, ptr, i64)\ndeclare void @llvm.trap()\ndeclare void @llvm.memcpy.p0.p0.i64(ptr, ptr, i64, i1 immarg)\n") + strings.write_string(&emitter.builder, "declare i64 @write(i32, ptr, i64)\ndeclare void @llvm.trap()\ndeclare void @llvm.memcpy.p0.p0.i64(ptr, ptr, i64, i1 immarg)\ndeclare void @llvm.memset.p0.i64(ptr, i8, i64, i1 immarg)\n") widths := [?]int{8, 16, 32, 64} overflow_intrinsics := [?]string{"sadd", "uadd", "ssub", "usub", "smul", "umul"} for bits in widths { diff --git a/compiler/lower/lower.odin b/compiler/lower/lower.odin index 575693e..68660b9 100644 --- a/compiler/lower/lower.odin +++ b/compiler/lower/lower.odin @@ -613,7 +613,6 @@ lower_statements :: proc(state: ^State, statements: []hir.Stmt_Id) { statement := hir_module.statements[statement_id] switch statement.kind { case .Declaration: - value := lower_expr(state, statement.expr) if statement.local == hir.INVALID_LOCAL || int(statement.local) >= len(state.func_locals) { append_instruction(state, ir.Instruction{ op=.Trap, span=statement.span, type=types.VOID, @@ -632,6 +631,19 @@ lower_statements :: proc(state: ^State, statements: []hir.Stmt_Id) { diagnostic=source.INVALID_DIAGNOSTIC, }) state.local_slots[statement.local] = slot + if statement.expr == hir.INVALID_EXPR { + append_instruction(state, ir.Instruction{ + op=.Fill, + span=statement.span, + type=local.type, + target=ir.INVALID_REF, + a=slot, + b=ir.INVALID_INSTRUCTION, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + continue + } + value := lower_expr(state, statement.expr) append_instruction(state, ir.Instruction{ op=.Store, span=statement.span, diff --git a/compiler/parser/parser.odin b/compiler/parser/parser.odin index b398d67..b8796b4 100644 --- a/compiler/parser/parser.odin +++ b/compiler/parser/parser.odin @@ -576,6 +576,15 @@ parse_primary :: proc(parser: ^Parser, nesting: int) -> ast.Expr_Id { right=ast.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC, }) + case .Keyword_Undefined: + advance(parser) + return add_expr(parser, ast.Expr{ + kind=.Undefined, + span=tok.span, + left=ast.INVALID_EXPR, + right=ast.INVALID_EXPR, + diagnostic=source.INVALID_DIAGNOSTIC, + }) case .Keyword_True, .Keyword_False: advance(parser) return add_expr(parser, ast.Expr{ diff --git a/compiler/token/token.odin b/compiler/token/token.odin index e19bf7a..b059ad8 100644 --- a/compiler/token/token.odin +++ b/compiler/token/token.odin @@ -59,6 +59,7 @@ Kind :: enum u8 { Keyword_Return, Keyword_Mut, Keyword_None, + Keyword_Undefined, Keyword_Orelse, Keyword_And, Keyword_Or, diff --git a/compiler_tests.odin b/compiler_tests.odin index 41599a4..861c516 100644 --- a/compiler_tests.odin +++ b/compiler_tests.odin @@ -375,6 +375,32 @@ main :: func() void {} testing.expect(t, strings.contains(diagnostics.items[0].message, "followed by a newline")) } +@(test) +parser_accepts_undefined_expression :: proc(t: ^testing.T) { + text := `main :: func() void { + value i32 = undefined +} +` + 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_keyword := false + for tok in stream.items { + found_keyword = found_keyword || tok.kind == .Keyword_Undefined + } + statement := module.statements[module.functions[0].body[0]] + testing.expect_value(t, len(diagnostics.items), 0) + testing.expect(t, found_keyword) + testing.expect_value(t, module.exprs[statement.expr].kind, ast.Expr_Kind.Undefined) +} + @(test) pratt_parser_preserves_left_associative_addition_shape :: proc(t: ^testing.T) { source_file := source.Source{path="test.bro", text="value :: 1 + 2 + 3\nmain :: func() void {}\n"} @@ -5013,6 +5039,187 @@ compound_assignment_preserves_operation_and_rhs :: proc(t: ^testing.T) { testing.expect_value(t, module.exprs[statement.expr].integer, u64(5)) } +@(test) +undefined_inferred_local_lowers_to_fill :: proc(t: ^testing.T) { + text := `choose :: func(flag bool) i32 { + value int = undefined + if flag { + value = 42 + } else { + value = -2 + } + return value +} +main :: func() i32 { + return choose(true) +} +` + 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) + + choose_symbol := symbol.intern(&symbols, "choose") + value_symbol := symbol.intern(&symbols, "value") + found_value_i32 := false + fill_count := 0 + for function, function_index in hir_module.functions { + if function.name != choose_symbol { + continue + } + for local in function.locals { + found_value_i32 = found_value_i32 || local.name == value_symbol && local.type == types.I32 + } + for instruction in ir_module.functions[function_index].instructions { + fill_count += 1 if instruction.op == .Fill else 0 + } + } + + testing.expect_value(t, len(diagnostics.items), 0) + testing.expect(t, found_value_i32) + testing.expect_value(t, fill_count, 1) + testing.expect(t, strings.contains(llvm_text, "declare void @llvm.memset.p0.i64")) + testing.expect(t, strings.contains(llvm_text, "call void @llvm.memset.p0.i64")) + testing.expect(t, strings.contains(llvm_text, "i8 -86")) +} + +@(test) +local_int_inference_widens_from_assignments :: proc(t: ^testing.T) { + text := `wide :: func() int { + value int = 1 + value = 1000 + return value +} +main :: func() void { + _ = wide() +} +` + 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) + + wide_symbol := symbol.intern(&symbols, "wide") + value_symbol := symbol.intern(&symbols, "value") + found_value_i16 := false + found_result_i16 := false + for function in hir_module.functions { + if function.name != wide_symbol { + continue + } + found_result_i16 = function.result == types.I16 + for local in function.locals { + found_value_i16 = found_value_i16 || local.name == value_symbol && local.type == types.I16 + } + } + + testing.expect_value(t, len(diagnostics.items), 0) + testing.expect(t, found_result_i16) + testing.expect(t, found_value_i16) +} + +@(test) +undefined_accepts_concrete_runtime_annotations :: proc(t: ^testing.T) { + text := `Point :: struct { + x i32 + y i32 +} +main :: func() void { + point Point = undefined + pointer @i32 = undefined + maybe ?i32 = undefined +} +` + 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) + + main_symbol := symbol.intern(&symbols, "main") + fill_count := 0 + for function, function_index in hir_module.functions { + if function.name != main_symbol { + continue + } + for instruction in ir_module.functions[function_index].instructions { + fill_count += 1 if instruction.op == .Fill else 0 + } + } + + testing.expect_value(t, len(diagnostics.items), 0) + testing.expect_value(t, fill_count, 3) +} + +@(test) +undefined_rejects_non_declaration_uses_and_unresolved_inference :: proc(t: ^testing.T) { + text := `global :: undefined +main :: func() void { + immutable :: undefined + typed_immutable int :: undefined + unresolved int = undefined + existing i32 = 1 + existing = undefined + mismatch int = undefined + mismatch = 1 + mismatch = 1.0 +} +` + source_file := source.Source{path="test.bro", text=text} + diagnostics := source.init_diagnostics(&source_file) + defer source.destroy_diagnostics(&diagnostics) + symbols := symbol.init_table() + defer symbol.destroy_table(&symbols) + stream := lexer.lex(&source_file, &diagnostics, &symbols) + defer delete(stream.items) + ast_module := parser.parse(&stream, &source_file, &diagnostics) + defer ast.destroy_module(&ast_module) + hir_module := checker.check(&ast_module, &diagnostics, &symbols) + defer hir.destroy_module(&hir_module) + + immutable_count := 0 + found_unresolved := false + found_assignment := false + found_incompatible := false + for diagnostic in diagnostics.items { + immutable_count += 1 if strings.contains(diagnostic.message, "'undefined' requires a mutable local declaration") else 0 + found_unresolved = found_unresolved || strings.contains(diagnostic.message, "could not infer a concrete type for local 'unresolved'") + found_assignment = found_assignment || strings.contains(diagnostic.message, "'undefined' is only valid as a mutable local declaration initializer") + found_incompatible = found_incompatible || strings.contains(diagnostic.message, "cannot implicitly convert f64 to i8") + } + + testing.expect(t, immutable_count >= 2) + testing.expect(t, found_unresolved) + testing.expect(t, found_assignment) + testing.expect(t, found_incompatible) +} + @(test) compound_assignment_evaluates_lvalue_once :: proc(t: ^testing.T) { // A compound assignment to an indexed lvalue must compute the element address