diff --git a/TODO.md b/TODO.md index 4d75e5d..468f912 100644 --- a/TODO.md +++ b/TODO.md @@ -732,12 +732,23 @@ - deferred: build graph / steps / caching, multiple artifacts, computed paths (needs string building), struct field defaults to drop `&[]` on empty lists -29. basic `std/arraylist` implementation using the new `std/mem` typed allocation +29. fix bugs (implemented) + - `return _` was already the supported empty return for void functions; the original + bare-`return` report was stale + - catch value blocks may end by returning from the function instead of yielding when + every path exits + - implicit-conversion diagnostics render source-level composite and named types instead + of internal `` ids + - aliases resolve transparently in value contexts, including composed enum/union sums + - final open-constant defaults feed one last inference fixpoint before stale + specializations are pruned -30. disallow arbitrary integer division +30. basic `std/arraylist` implementation using the new `std/mem` typed allocation + +31. disallow arbitrary integer division - take inspiration from zig - see also below for a word on unchecked casts - - the user should be explicit about what they mean with integer division (e.g. `div`, `rem`) + - the user should be explicit about what they mean with integer division (e.g. `div`, `rem`, `trunc`) ## A word on unchecked casts diff --git a/compiler/checker/checker.odin b/compiler/checker/checker.odin index 5d1c8bd..0e0e79b 100644 --- a/compiler/checker/checker.odin +++ b/compiler/checker/checker.odin @@ -265,14 +265,100 @@ record_unused_locals :: proc( } } -// type_label renders a type for a diagnostic, resolving a named type (enum/union/struct/ -// distinct) to its declared source name; primitives and unnamed types fall back to -// `types.name` (which prints `` for anonymous nodes). -type_label :: proc(checker: ^Checker, value: types.Type) -> string { - if node, ok := types.node(&checker.module.types, value); ok && node.name != 0 { - return symbol_text(checker, symbol.Id(node.name)) +write_type_label :: proc(checker: ^Checker, builder: ^strings.Builder, value: types.Type) { + store := &checker.module.types + item, ok := types.node(store, value) + if !ok { + strings.write_string(builder, types.name(value)) + return } - return types.name(value) + if item.name != 0 { + strings.write_string(builder, symbol_text(checker, symbol.Id(item.name))) + return + } + + switch item.kind { + case .Array: + strings.write_byte(builder, '[') + if item.inferred_count { + strings.write_byte(builder, '_') + } else { + fmt.sbprintf(builder, "%d", item.count) + } + if item.has_sentinel { + fmt.sbprintf(builder, ";%d", item.sentinel) + } + strings.write_byte(builder, ']') + if item.mutable { + strings.write_string(builder, "mut ") + } + write_type_label(checker, builder, item.child) + case .Pointer: + if item.has_sentinel { + fmt.sbprintf(builder, "[*;%d]", item.sentinel) + } else { + strings.write_byte(builder, '*' if item.many else '@') + } + if item.mutable { + strings.write_string(builder, "mut ") + } + write_type_label(checker, builder, item.child) + case .Slice: + if item.has_sentinel { + fmt.sbprintf(builder, "[;%d]", item.sentinel) + } else { + strings.write_string(builder, "[]") + } + if item.mutable { + strings.write_string(builder, "mut ") + } + write_type_label(checker, builder, item.child) + case .Range: + strings.write_string(builder, "range(") + write_type_label(checker, builder, item.child) + strings.write_byte(builder, ')') + case .Optional: + strings.write_byte(builder, '?') + write_type_label(checker, builder, item.child) + case .Function: + strings.write_string(builder, "c_func(" if item.c_abi else "func(") + for param, index in types.params_for(store, value) { + if index > 0 { + strings.write_string(builder, ", ") + } + write_type_label(checker, builder, param.type) + } + if item.variadic { + if item.field_count > 0 { + strings.write_string(builder, ", ") + } + strings.write_string(builder, "...") + } + strings.write_string(builder, ") ") + write_type_label(checker, builder, item.child) + case .Fallible: + write_type_label(checker, builder, item.child) + strings.write_string(builder, " ! ") + write_type_label(checker, builder, item.extra) + case .Struct: + strings.write_string(builder, "struct") + case .Union: + strings.write_string(builder, "union") + case .Enum: + strings.write_string(builder, "enum") + case .Alias, .Distinct, .Named: + write_type_label(checker, builder, item.child) + case .Invalid, .Void, .Anyopaque, .Int_Constraint, .Float_Constraint, .Range_Constraint, .Scalar: + strings.write_string(builder, types.name(value)) + } +} + +// Render dynamic types using source syntax so diagnostics never expose internal +// type-store ids such as ``. +type_label :: proc(checker: ^Checker, value: types.Type) -> string { + builder := strings.builder_make(context.temp_allocator) + write_type_label(checker, &builder, value) + return strings.to_string(builder) } is_ptr_cast_call :: proc(checker: ^Checker, expr: ast.Expr) -> bool { @@ -460,6 +546,8 @@ type_from_syntax :: proc( store := &checker.module.types changed := false #partial switch item.kind { + case .Alias: + return type_from_syntax(checker, item.child, pkg, file, depth+1) case .Array: child := type_from_syntax(checker, item.child, pkg, file, depth+1) changed = changed || child != item.child @@ -3246,6 +3334,7 @@ infer_all :: proc(checker: ^Checker) { ensure_spec(checker, main_template, nil) } + defaults_applied := false for { changed := false checker.global_demands_dirty = false @@ -3334,22 +3423,30 @@ infer_all :: proc(checker: ^Checker) { changed = true } if !changed { + if !defaults_applied { + defaults_applied = true + defaulted := false + // No authoritative demand can still arrive. Assign final defaults, then + // continue the same fixpoint so dependent globals/specs observe them. + for global, index in checker.ast_module.globals { + if global.external || is_runtime_type(checker, checker.global_types[index]) { + continue + } + if checker.global_open_const[index] { + checker.global_types[index] = types.smallest_signed_for_literal(i64(checker.global_const_value[index])) + defaulted = true + } else if checker.global_open_float[index] { + checker.global_types[index] = types.F64 + defaulted = true + } + } + if defaulted { + continue + } + } break } } - - // Any open constant that no use ever demanded now takes its default: an integer - // constant the smallest signed type that holds its value, a float constant f64. - for global, index in checker.ast_module.globals { - if global.external || is_runtime_type(checker, checker.global_types[index]) { - continue - } - if checker.global_open_const[index] { - checker.global_types[index] = types.smallest_signed_for_literal(i64(checker.global_const_value[index])) - } else if checker.global_open_float[index] { - checker.global_types[index] = types.F64 - } - } } prune_specs :: proc(checker: ^Checker) { @@ -3604,8 +3701,8 @@ coerce_expr :: proc( checker.diagnostics, span, "cannot implicitly convert %s to %s", - types.name(actual), - types.name(expected), + type_label(checker, actual), + type_label(checker, expected), ) return invalid_hir_expr(checker, span, id, expected) } @@ -4533,7 +4630,7 @@ build_compound_expr :: proc( } handler: [dynamic]hir.Stmt_Id handler.allocator = checker.allocator - fallback, _ = build_value_source(ctx, &handler, expr.body, success, expr.span) + fallback, _ = build_value_source(ctx, &handler, expr.body, success, expr.span, allow_exit=true) body = handler[:] resize(ctx.locals, capture_start) } @@ -6683,18 +6780,17 @@ build_block :: proc( } // build_value_block builds a `{ ... yield v }` value block whose final statement -// must be a `yield`: it builds the leading statements inline (their own scope and -// defers), evaluates the yield expression in that scope, then — capturing the value -// first, like a function return — runs the block's defers and closes the scope. The -// resulting `value`/`value_type` are spliced into the enclosing declaration or -// assignment. `expected` is the binding's type (INVALID for an untyped `::`, where -// the yield's natural type is taken). Statements are appended to `body`. +// must be a `yield`. Catch handlers may instead exit on every path, in which case +// `allow_exit` leaves the fallback expression invalid. Otherwise it builds the leading +// statements inline, evaluates the yield in their scope, then captures the value before +// running defers. `expected` is the binding's type (INVALID for an untyped `::`). build_value_block :: proc( ctx: ^Build_Ctx, body: ^[dynamic]hir.Stmt_Id, body_stmts: []ast.Stmt_Id, expected: types.Type, span: source.Span, + allow_exit := false, ) -> (value: hir.Expr_Id, value_type: types.Type) { checker := ctx.checker n := len(body_stmts) @@ -6705,6 +6801,10 @@ build_value_block :: proc( for s in inner { append(body, s) } + if allow_exit && all_paths_exit(&checker.module, inner) { + delete(inner, checker.allocator) + return hir.INVALID_EXPR, expected + } delete(inner, checker.allocator) id := source.add(checker.diagnostics, span, "a value block must end with an explicit 'yield'") ctx.problematic^ = true @@ -6782,6 +6882,7 @@ build_value_source :: proc( span: source.Span, label := symbol.INVALID, value_control_flow := false, + allow_exit := false, ) -> (value: hir.Expr_Id, value_type: types.Type) { checker := ctx.checker if symbol.is_valid(label) { @@ -6797,7 +6898,7 @@ build_value_source :: proc( return build_value_match(ctx, body, body_stmts[0], expected, span) } } - return build_value_block(ctx, body, body_stmts, expected, span) + return build_value_block(ctx, body, body_stmts, expected, span, allow_exit) } // new_value_slot allocates a fresh, un-nameable mutable local to hold a value-if/loop diff --git a/compiler/hir/hir.odin b/compiler/hir/hir.odin index 1c18368..b99e0a9 100644 --- a/compiler/hir/hir.odin +++ b/compiler/hir/hir.odin @@ -132,7 +132,8 @@ Expr :: struct { integer: i64, args: []Expr_Id, // `Catch` block handlers use `body` for the handler statements and `target` - // for the optional captured error local. + // for the optional captured error local. A missing `right` means the handler + // exits on every path and therefore has no fallback value. body: []Stmt_Id, target: Ref, left: Expr_Id, diff --git a/compiler/lower/lower.odin b/compiler/lower/lower.odin index 975c83b..8f7ea4f 100644 --- a/compiler/lower/lower.odin +++ b/compiler/lower/lower.odin @@ -466,16 +466,18 @@ lower_compound_expr :: proc(state: ^State, expr_id: hir.Expr_Id) -> ir.Instructi } lower_statements(state, expr.body) } - fallback := lower_nested_expr(state, expr.right) - append_instruction(state, ir.Instruction{ - op=.Store, span=expr.span, type=success, - target=ir.INVALID_REF, a=slot, b=fallback, diagnostic=source.INVALID_DIAGNOSTIC, - }) - append_instruction(state, ir.Instruction{ - op=.Br, span=expr.span, type=types.VOID, integer=merge_lbl, - target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, - diagnostic=source.INVALID_DIAGNOSTIC, - }) + if expr.right != hir.INVALID_EXPR { + fallback := lower_nested_expr(state, expr.right) + append_instruction(state, ir.Instruction{ + op=.Store, span=expr.span, type=success, + target=ir.INVALID_REF, a=slot, b=fallback, diagnostic=source.INVALID_DIAGNOSTIC, + }) + append_instruction(state, ir.Instruction{ + op=.Br, span=expr.span, type=types.VOID, integer=merge_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=expr.span, type=types.VOID, integer=success_lbl, diff --git a/compiler_tests.odin b/compiler_tests.odin index c43c40c..df3d6f7 100644 --- a/compiler_tests.odin +++ b/compiler_tests.odin @@ -3961,6 +3961,77 @@ main func() i32 { testing.expect_value(t, compiler_core.compile_package(directory, output), 1) } +@(test) +terminating_catch_block_compiles_and_runs :: proc(t: ^testing.T) { + text := `Failure :: enum { + bad +} +may_fail func(fail bool) i32 ! Failure { + if (fail) return .bad + return 1 +} +recover func(fail bool) i32 { + value :: may_fail(fail) catch |_| { + return 40 + } + return value + 1 +} +main func() i32 { + return recover(false) + recover(true) - 42 +} +` + directory := "/tmp/brolang-test-terminating-catch" + main_path := "/tmp/brolang-test-terminating-catch/main.bro" + output := "/tmp/brolang-test-terminating-catch-output" + _ = os2.remove_all(directory) + defer _ = os2.remove_all(directory) + defer _ = os.remove(output) + testing.expect(t, os.make_directory(directory) == nil) + testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text)) + testing.expect_value(t, compiler_core.compile_package(directory, output), 0) + state := run_executable(output) + testing.expect_value(t, state.exit_code, 0) +} + +@(test) +conversion_diagnostics_render_source_types :: proc(t: ^testing.T) { + text := `Allocator :: struct { + marker i32 +} +take func(allocator Allocator, memory []mut u8) void {} +main func() void { + allocator Allocator = Allocator { marker = 0 } + data [1]mut u8 = [0] + take(data[..], allocator) +} +` + source_file := source.Source{path="type_labels.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) + + slice_to_allocator := false + allocator_to_slice := false + internal_type_id := false + for diagnostic in diagnostics.items { + slice_to_allocator = slice_to_allocator || + strings.contains(diagnostic.message, "cannot implicitly convert []mut u8 to Allocator") + allocator_to_slice = allocator_to_slice || + strings.contains(diagnostic.message, "cannot implicitly convert Allocator to []mut u8") + internal_type_id = internal_type_id || strings.contains(diagnostic.message, " max_value(usize) / element_size { + return .out_of_memory + } + + memory ?*mut u8 = raw_alloc(allocator, count * element_size, align_of(T)) + if memory |bytes| { + pointer *mut T :: ptr_cast(T, bytes) + return pointer[..count] + } + return .out_of_memory +} + +free func($T type, allocator Allocator, memory []mut T) void { + if memory.len != 0 and size_of(T) != 0 { + raw_free(allocator, ptr_cast(u8, memory.ptr), memory.len * size_of(T), align_of(T)) + } +} + +_malloc_alignment usize :: 16 # ponytail: aarch64-macos libc malloc alignment assumption. + +_power_of_two func(value usize) bool { + if value == 0 { + return false + } + + current usize = value + while current > 1 { + half usize = current / 2 + if half * 2 != current { + return false + } + current = half + } + + return true +} + +_c_alloc func(_ ?*mut anyopaque, size usize, alignment usize) ?*mut u8 { + if _power_of_two(alignment) == false { + return none + } + + if alignment <= _malloc_alignment { + return ptr_cast(u8, c.malloc(c_ulong(size))) + } + + memory [1]mut ?*mut anyopaque = [none] + status c_int = c.posix_memalign((&memory).ptr, c_ulong(alignment), c_ulong(size)) + if status != 0 { + return none + } + + return ptr_cast(u8, memory[0]) +} + +_c_realloc func(_ ?*mut anyopaque, memory ?*mut u8, old_size usize, new_size usize, alignment usize) ?*mut u8 { + if _power_of_two(alignment) == false { + return none + } + + if new_size == 0 { + c.free(memory) + return none + } + + if memory |old_memory| { + if alignment <= _malloc_alignment { + return ptr_cast(u8, c.realloc(old_memory, c_ulong(new_size))) + } + + new_memory ?*mut u8 = _c_alloc(none, new_size, alignment) + if new_memory |new_bytes| { + copy_size usize = old_size + if new_size < copy_size { + copy_size = new_size + } + i usize = 0 + while i < copy_size : i += 1 { + new_bytes[i] = old_memory[i] + } + c.free(old_memory) + } + return new_memory + } + + return _c_alloc(none, new_size, alignment) +} + +_c_free func(_ ?*mut anyopaque, memory ?*mut u8, _ usize, _ usize) void { + c.free(memory) +} + +_c_vtable AllocatorVTable :: AllocatorVTable { + alloc = _c_alloc, + realloc = _c_realloc, + free = _c_free, +} + +c_allocator Allocator :: Allocator { + context = none, + vtable = &_c_vtable, +}