diff --git a/LANGUAGE.md b/LANGUAGE.md index 258aa5e..eee5560 100644 --- a/LANGUAGE.md +++ b/LANGUAGE.md @@ -263,7 +263,7 @@ exactly once. Bare functions named `memcopy` or `memset` remain ordinary user fu - `std/arraylist` generic `ArrayList(T)` with direct `items` slice access, explicit capacity, allocator ownership, fallible reserve/append, clear, and deinit - `std/meta` reflection records plus `EnumFieldStruct(E, Field, default ?Field)`, implemented with `struct_type!`; it produces a record with one field per native enum member in declaration order, where outer `null` means no field default - `std/io` explicit `Io` capabilities, provider-bound `Reader`/`Writer` handles, existing-file open/close operations, allocation-free `write_all`, and comptime-expanded writer-first `print`; formatting supports natural `{}`, byte `{s}`, decimal `{d}`, integer `{b}` / `{o}` / `{x}` / `{X}`, byte-character `{c}`, scientific float `{e}`, recursively scalar-backed distinct values, and `{{` / `}}`, with malformed formats and incompatible tuple fields rejected at comptime -- entry points are either `main func() ...` or `main func(init process.Init) ...`; `std/process.Init` carries startup capabilities, currently only `io`, while the system provider remains hidden inside `std/io` +- entry points are either `main func() ...` or `main func(init process.Init) ...`; their success channel is `void`, `i32`, or `int` and may have an error channel; an unhandled entry error exits with status 1. `std/process.Init` carries startup capabilities, currently only `io`, while the system provider remains hidden inside `std/io` - `std/debug.print` is an allocation-free, failure-ignoring stderr escape hatch independent of `process.Init` - `std/testing` supplies fallible `expect`, expected-first `expect_equal`, and exact compile-time `expect_type`; direct calls through an alias of exactly `@std/testing` receive compiler-injected source locations diff --git a/compiler/checker/checker.odin b/compiler/checker/checker.odin index 711004b..df039a7 100644 --- a/compiler/checker/checker.odin +++ b/compiler/checker/checker.odin @@ -1305,6 +1305,9 @@ type_from_syntax :: proc( function_channel_type :: proc(checker: ^Checker, function: ast.Function) -> types.Type { result := type_from_syntax(checker, function.result, function.pkg, function.file) + if function.pkg == 0 && function.name == checker.main_symbol && result == types.INT { + result = types.I32 + } if types.is_valid(function.error) { return types.fallible(&checker.module.types, result, type_from_syntax(checker, function.error, function.pkg, function.file)) } @@ -4703,10 +4706,6 @@ ensure_spec :: proc( comptime_signature.allocator = checker.allocator append(&comptime_signature, ..comptime_values) result := function_channel_type(checker, function) - if function.pkg == 0 && function.name == checker.main_symbol && function.result == types.INT && - !types.is_valid(function.error) { - result = types.I32 - } index := spec_id(len(checker.specs)) append( &checker.specs, @@ -5825,12 +5824,7 @@ infer_expr :: proc( } } else { declared := function_channel_type(checker, function) - if function.pkg == 0 && function.name == checker.main_symbol && function.result == types.INT && - !types.is_valid(function.error) { - last = types.I32 - } else { - last = declared if is_runtime_type(checker, declared) || types.is_void(declared) || types.is_noreturn(declared) else types.INVALID - } + last = declared if is_runtime_type(checker, declared) || types.is_void(declared) || types.is_noreturn(declared) else types.INVALID } delete(stack[frame_index].args, checker.allocator) stack[frame_index].args = nil @@ -10402,7 +10396,10 @@ build_expr :: proc( make_link_name :: proc(checker: ^Checker, id: Spec_Id) -> string { spec := checker.specs[id] function := checker.ast_module.functions[spec.template] - if function.pkg == 0 && function.name == checker.main_symbol && checker.entry_point == .Plain { + native_main := function.pkg == 0 && function.name == checker.main_symbol && + checker.entry_point == .Plain && + types.kind(spec.result, &checker.module.types) != .Fallible + if native_main { return fmt.aprintf("main", allocator = checker.allocator) } if function.generated { @@ -13981,7 +13978,9 @@ build_function :: proc(checker: ^Checker, id: Spec_Id) { problematic := signature_diagnostic != source.INVALID_DIAGNOSTIC || checker.template_diagnostics[spec.template] != source.INVALID_DIAGNOSTIC - native_main := function.pkg == 0 && function.name == checker.main_symbol && checker.entry_point == .Plain + native_main := function.pkg == 0 && function.name == checker.main_symbol && + checker.entry_point == .Plain && + types.kind(spec.result, &checker.module.types) != .Fallible if !function.has_body { if unresolved_result && signature_diagnostic == source.INVALID_DIAGNOSTIC && checker.template_diagnostics[spec.template] == source.INVALID_DIAGNOSTIC { @@ -14814,18 +14813,23 @@ check :: proc( id = source.add( diagnostics, template.span, - "main must be unique, have a body, take no parameters or one @std/process Init, and return void, i32, or int", + "main must be unique, have a body, take no parameters or one @std/process Init, and return void, i32, or int, optionally with an error channel", ) } checker.module.injected_main = hir.INVALID_FUNCTION checker.module.io_provider = hir.INVALID_FUNCTION replace_main_with_trap(&checker, id) - } else if checker.entry_point == .Process { + } else { main_spec := find_spec(&checker, main_template, nil) - provider_spec := find_spec(&checker, checker.io_provider_template, nil) - if main_spec != INVALID_SPEC && provider_spec != INVALID_SPEC { + if checker.entry_point == .Process { + provider_spec := find_spec(&checker, checker.io_provider_template, nil) + if main_spec != INVALID_SPEC && provider_spec != INVALID_SPEC { + checker.module.injected_main = checker.specs[main_spec].hir_id + checker.module.io_provider = checker.specs[provider_spec].hir_id + } + } else if main_spec != INVALID_SPEC && + types.kind(checker.specs[main_spec].result, &checker.module.types) == .Fallible { checker.module.injected_main = checker.specs[main_spec].hir_id - checker.module.io_provider = checker.specs[provider_spec].hir_id } } } diff --git a/compiler/lower/lower.odin b/compiler/lower/lower.odin index 9524379..cc25325 100644 --- a/compiler/lower/lower.odin +++ b/compiler/lower/lower.odin @@ -1752,41 +1752,49 @@ lower_global_initializer :: proc(hir_module: ^hir.Module, global: hir.Global, al append_injected_main :: proc(module: ^ir.Module, hir_module: ^hir.Module, allocator: mem.Allocator) { main_index, main_ok := hir.index(hir_module.injected_main, hir.INVALID_FUNCTION, len(hir_module.functions)) + if !main_ok { + return + } + main_function := &hir_module.functions[main_index] provider_index, provider_ok := hir.index(hir_module.io_provider, hir.INVALID_FUNCTION, len(hir_module.functions)) - if !main_ok || !provider_ok { + param_index := -1 + if len(main_function.params) == 1 { + param_index, main_ok = hir.index(main_function.params[0], hir.INVALID_LOCAL, len(main_function.locals)) + if !main_ok || !provider_ok { + return + } + } else if len(main_function.params) != 0 { return } instructions: [dynamic]ir.Instruction instructions.allocator = allocator - provider_call := ir.instruction_id(len(instructions)) - append(&instructions, ir.Instruction{ - op=.Call, - type=hir_module.functions[provider_index].result, - target=ir.function_ref(ir.Function_Id(provider_index)), - a=ir.INVALID_INSTRUCTION, - b=ir.INVALID_INSTRUCTION, - diagnostic=source.INVALID_DIAGNOSTIC, - }) - main_function := &hir_module.functions[main_index] - param_index, param_ok := hir.index(main_function.params[0], hir.INVALID_LOCAL, len(main_function.locals)) - if !param_ok { - return + args: []ir.Instruction_Id + if param_index >= 0 { + provider_call := ir.instruction_id(len(instructions)) + append(&instructions, ir.Instruction{ + op=.Call, + type=hir_module.functions[provider_index].result, + target=ir.function_ref(ir.Function_Id(provider_index)), + a=ir.INVALID_INSTRUCTION, + b=ir.INVALID_INSTRUCTION, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + init_args := make([]ir.Instruction_Id, 1, allocator) + init_args[0] = provider_call + init_value := ir.instruction_id(len(instructions)) + append(&instructions, ir.Instruction{ + op=.Aggregate, + type=main_function.locals[param_index].type, + args=init_args, + target=ir.INVALID_REF, + a=ir.INVALID_INSTRUCTION, + b=ir.INVALID_INSTRUCTION, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + args = make([]ir.Instruction_Id, 1, allocator) + args[0] = init_value } - init_args := make([]ir.Instruction_Id, 1, allocator) - init_args[0] = provider_call - init_value := ir.instruction_id(len(instructions)) - append(&instructions, ir.Instruction{ - op=.Aggregate, - type=main_function.locals[param_index].type, - args=init_args, - target=ir.INVALID_REF, - a=ir.INVALID_INSTRUCTION, - b=ir.INVALID_INSTRUCTION, - diagnostic=source.INVALID_DIAGNOSTIC, - }) - args := make([]ir.Instruction_Id, 1, allocator) - args[0] = init_value main_call := ir.instruction_id(len(instructions)) append(&instructions, ir.Instruction{ op=.Call, @@ -1797,36 +1805,114 @@ append_injected_main :: proc(module: ^ir.Module, hir_module: ^hir.Module, alloca b=ir.INVALID_INSTRUCTION, diagnostic=source.INVALID_DIAGNOSTIC, }) - if types.is_void(hir_module.functions[main_index].result) { + + if types.kind(main_function.result, &hir_module.types) == .Fallible { + success := types.fallible_success(main_function.result, &hir_module.types) + channel_slot := ir.instruction_id(len(instructions)) append(&instructions, ir.Instruction{ - op=.Return_Void, - type=types.VOID, - target=ir.INVALID_REF, - a=ir.INVALID_INSTRUCTION, - b=ir.INVALID_INSTRUCTION, + op=.Alloca, type=main_function.result, target=ir.INVALID_REF, + a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, diagnostic=source.INVALID_DIAGNOSTIC, }) - } else { append(&instructions, ir.Instruction{ - op=.Return, - type=hir_module.functions[main_index].result, - target=ir.INVALID_REF, - a=main_call, - b=ir.INVALID_INSTRUCTION, + op=.Store, type=main_function.result, target=ir.INVALID_REF, + a=channel_slot, b=main_call, diagnostic=source.INVALID_DIAGNOSTIC, + }) + code := ir.instruction_id(len(instructions)) + append(&instructions, ir.Instruction{ + op=.Union_Tag, type=types.U16, target=ir.INVALID_REF, + a=channel_slot, b=ir.INVALID_INSTRUCTION, diagnostic=source.INVALID_DIAGNOSTIC, + }) + zero_tag := ir.instruction_id(len(instructions)) + append(&instructions, ir.Instruction{ + op=.Const, type=types.U16, integer=0, target=ir.INVALID_REF, + a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, diagnostic=source.INVALID_DIAGNOSTIC, }) + ok := ir.instruction_id(len(instructions)) + append(&instructions, ir.Instruction{ + op=.Compare, type=types.BOOL, integer=i64(ir.Compare_Predicate.Eq), + target=ir.INVALID_REF, a=code, b=zero_tag, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + append(&instructions, ir.Instruction{ + op=.Cond_Br, type=types.VOID, integer=1, target=ir.Ref(0), a=ok, + b=ir.INVALID_INSTRUCTION, diagnostic=source.INVALID_DIAGNOSTIC, + }) + append(&instructions, ir.Instruction{ + op=.Label, type=types.VOID, integer=0, target=ir.INVALID_REF, + a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + failure := ir.instruction_id(len(instructions)) + append(&instructions, ir.Instruction{ + op=.Const, type=types.I32, integer=1, target=ir.INVALID_REF, + a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + append(&instructions, ir.Instruction{ + op=.Return, type=types.I32, target=ir.INVALID_REF, a=failure, + b=ir.INVALID_INSTRUCTION, diagnostic=source.INVALID_DIAGNOSTIC, + }) + append(&instructions, ir.Instruction{ + op=.Label, type=types.VOID, integer=1, target=ir.INVALID_REF, + a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + success_value := ir.INVALID_INSTRUCTION + if types.is_void(success) { + success_value = ir.instruction_id(len(instructions)) + append(&instructions, ir.Instruction{ + op=.Const, type=types.I32, integer=0, target=ir.INVALID_REF, + a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + } else { + payload := ir.instruction_id(len(instructions)) + append(&instructions, ir.Instruction{ + op=.Field_Address, type=success, integer=0, target=ir.INVALID_REF, + a=channel_slot, b=ir.INVALID_INSTRUCTION, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + success_value = ir.instruction_id(len(instructions)) + append(&instructions, ir.Instruction{ + op=.Load, type=success, target=ir.INVALID_REF, a=payload, + b=ir.INVALID_INSTRUCTION, diagnostic=source.INVALID_DIAGNOSTIC, + }) + } + append(&instructions, ir.Instruction{ + op=.Return, type=types.I32, target=ir.INVALID_REF, a=success_value, + b=ir.INVALID_INSTRUCTION, diagnostic=source.INVALID_DIAGNOSTIC, + }) + } else { + exit_value := main_call + if types.is_void(main_function.result) { + exit_value = ir.instruction_id(len(instructions)) + append(&instructions, ir.Instruction{ + op=.Const, type=types.I32, integer=0, target=ir.INVALID_REF, + a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + } + append(&instructions, ir.Instruction{ + op=.Return, type=types.I32, target=ir.INVALID_REF, a=exit_value, + b=ir.INVALID_INSTRUCTION, diagnostic=source.INVALID_DIAGNOSTIC, + }) } + problematic := main_function.problematic + if provider_ok { + problematic = problematic || hir_module.functions[provider_index].problematic + } append(&module.functions, ir.Function{ link_name=fmt.aprintf("main", allocator=allocator), calling_convention=.C, implementation=.Definition, linkage=.External, is_main=true, - result=hir_module.functions[main_index].result, + result=types.I32, instructions=instructions[:], - problematic=hir_module.functions[main_index].problematic || - hir_module.functions[provider_index].problematic, + problematic=problematic, }) } diff --git a/compiler_tests.odin b/compiler_tests.odin index 4520e2e..04b4bf9 100644 --- a/compiler_tests.odin +++ b/compiler_tests.odin @@ -3129,6 +3129,79 @@ bodyless_root_main_recovers_as_a_trap_definition :: proc(t: ^testing.T) { testing.expect(t, !strings.contains(llvm_text, "declare i32 @main()")) } +@(test) +fallible_main_maps_success_and_error_channels_to_exit_codes :: proc(t: ^testing.T) { + Case :: struct { + name: string, + source: string, + exit_code: int, + } + cases := [?]Case{ + { + name="void-success", + source=`Error :: enum { failed } +succeed func() void ! Error {} +main func() void ! Error { try succeed() } +`, + exit_code=0, + }, + { + name="void-error", + source=`Error :: enum { failed } +fail func() void ! Error { return .failed } +main func() void ! Error { try fail() } +`, + exit_code=1, + }, + { + name="int-success", + source=`Error :: enum { failed } +value func() i32 ! Error { return 7 } +main func() int ! Error { return try value() } +`, + exit_code=7, + }, + { + name="process-error", + source=`process :: import "@std/process" +Error :: enum { failed } +fail func() void ! Error { return .failed } +main func(_ process.Init) void ! Error { try fail() } +`, + exit_code=1, + }, + } + for test_case in cases { + directory := fmt.tprintf("/tmp/brolang-test-fallible-main-%s", test_case.name) + output := fmt.tprintf("%s/app", directory) + _ = os2.remove_all(directory) + defer _ = os2.remove_all(directory) + testing.expect(t, os.make_directory(directory) == nil) + testing.expect(t, os.write_entire_file( + fmt.tprintf("%s/main.bro", directory), + transmute([]byte)test_case.source, + )) + status := compiler_core.compile_package( + directory, + output, + nil, + target.DEFAULT, + cimport.Options{}, + ".", + ) + testing.expect_value(t, status, 0) + state, stdout, stderr, _ := os2.process_exec( + os2.Process_Desc{command=[]string{output}}, + context.allocator, + ) + defer delete(stdout) + defer delete(stderr) + testing.expect_value(t, state.exit_code, test_case.exit_code) + testing.expect_value(t, len(stdout), 0) + testing.expect_value(t, len(stderr), 0) + } +} + @(test) milestone_33_injects_explicit_io_provider_and_runs_std_io :: proc(t: ^testing.T) { sources := source.init_store()