diff --git a/LANGUAGE.md b/LANGUAGE.md index ff6fb94..1468dc3 100644 --- a/LANGUAGE.md +++ b/LANGUAGE.md @@ -288,7 +288,8 @@ 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) ...`; 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` +- 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` contains exactly `io io.Io` and `args Args`, while the system provider remains hidden inside `std/io` +- `std/process.Args` contains exactly `values [][*;0]u8`, borrowing native `argv` and its `argc` count without allocation or copying; index 0 is the executable name when present. `argument_count(args Args) usize` returns the count, and `argument(args Args, index usize) ?[;0]u8` scans only the requested argument and returns a borrowed sentinel slice (including empty arguments), or null out of range; borrowed views remain valid while the supplied storage lives - `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 @@ -299,7 +300,7 @@ exactly once. Bare functions named `memcopy` or `memset` remain ordinary user fu - lazy semantic checking of demanded function specializations - static, eager runtime, mutable runtime, and deferred problematic globals with cycle diagnostics - demand-driven LLVM declarations for referenced foreign functions -- root `main` may be parameterless or accept canonical `@std/process Init`; the generated C entry point obtains the hidden system I/O provider and constructs the init value +- root `main` may be parameterless or accept canonical `@std/process Init`; the generated C entry point accepts native `argc`/`argv`, obtains the hidden system I/O provider, and constructs the init value using computed record layout. Both `Init` and `Args` must have their canonical auto-layout record shapes - `brolang test [root]` reuses `build.bro`, discovers only explicit test-import edges, skips the application entry point, and runs tests sequentially while continuing after assertion failures - replaceable dynamically loaded libclang C-import backend diff --git a/README.md b/README.md index e4ca10e..b779fbf 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,9 @@ odin build . -out:build/brolang ./build/prototype ``` -Programs may receive the system I/O capability explicitly. Standard-stream -helpers bind the provider, handle, and callback; `main func() ...` remains valid. +Programs may receive system I/O and process arguments explicitly through +`process.Init`; `main func() ...` remains valid. Standard-stream helpers bind the +provider, handle, and callback. ```bro io :: import "@std/io" @@ -23,6 +24,15 @@ main func(init process.Init) void { } ``` +`Init` contains `io io.Io` and `args process.Args`. `Args.values` is a borrowed +`[][*;0]u8` view of native `argv`, with its length taken from `argc`: it includes +the executable name at index 0 when present, without copying strings or the +pointer array. `process.argument_count(init.args)` returns that count, and +`process.argument(init.args, index)` returns a borrowed `?[;0]u8` (null when out +of range). The helper scans only the requested zero-terminated argument; empty +arguments remain empty slices. The slices are valid while their supplied +argument storage lives. + Bodyless `c_func` declarations bind exact external symbols and require concrete types. C primitives use atomic target-dependent names and remain semantically distinct from exact-width Brolang primitives: diff --git a/compiler/checker/checker.odin b/compiler/checker/checker.odin index 3b3a29a..9c18369 100644 --- a/compiler/checker/checker.odin +++ b/compiler/checker/checker.odin @@ -1756,16 +1756,47 @@ configure_entry_point :: proc(checker: ^Checker) { if io_package == ast.INVALID_PACKAGE { return } - init_item, init_ok := types.node(&checker.module.types, parameter_type) - init_fields := types.fields_for(&checker.module.types, parameter_type) - io_field_name := symbol.intern(checker.symbols, "io") - if !init_ok || init_item.kind != .Struct || init_item.tuple || init_item.c_layout || - len(init_fields) != 1 || init_fields[0].name != u32(io_field_name) || - !types.equal(types.resolve_alias(init_fields[0].type, &checker.module.types), io_type) { + args_name := symbol.intern(checker.symbols, "Args") + args_type := types.resolve_alias( + types.find_named(&checker.module.types, u32(process_package), u32(args_name)), + &checker.module.types, + ) + args_item, args_ok := types.node(&checker.module.types, args_type) + args_fields := types.fields_for(&checker.module.types, args_type) + values_name := symbol.intern(checker.symbols, "values") + argument_pointer := types.pointer(&checker.module.types, types.U8, false, true, true, 0) + argument_values := types.slice(&checker.module.types, argument_pointer, false) + if !args_ok || args_item.kind != .Struct || args_item.tuple || args_item.c_layout || + len(args_fields) != 1 || args_fields[0].name != u32(values_name) || + !types.equal(types.resolve_alias(args_fields[0].type, &checker.module.types), argument_values) { checker.template_diagnostics[main_template] = source.add( checker.diagnostics, main.span, - "@std/process Init must be an auto-layout record containing exactly 'io io.Io'", + "@std/process Args must be an auto-layout record containing exactly 'values [][*;0]u8'", + ) + return + } + + init_item, init_ok := types.node(&checker.module.types, parameter_type) + init_fields := types.fields_for(&checker.module.types, parameter_type) + io_field_name := symbol.intern(checker.symbols, "io") + args_field_name := symbol.intern(checker.symbols, "args") + io_field_ok, args_field_ok := false, false + for field in init_fields { + field_type := types.resolve_alias(field.type, &checker.module.types) + if field.name == u32(io_field_name) && types.equal(field_type, io_type) { + io_field_ok = true + } + if field.name == u32(args_field_name) && types.equal(field_type, args_type) { + args_field_ok = true + } + } + if !init_ok || init_item.kind != .Struct || init_item.tuple || init_item.c_layout || + len(init_fields) != 2 || !io_field_ok || !args_field_ok { + checker.template_diagnostics[main_template] = source.add( + checker.diagnostics, + main.span, + "@std/process Init must be an auto-layout record containing exactly 'io io.Io' and 'args Args'", ) return } diff --git a/compiler/lower/lower.odin b/compiler/lower/lower.odin index 472feed..1e193cc 100644 --- a/compiler/lower/lower.odin +++ b/compiler/lower/lower.odin @@ -1782,8 +1782,56 @@ append_injected_main :: proc(module: ^ir.Module, hir_module: ^hir.Module, alloca instructions: [dynamic]ir.Instruction instructions.allocator = allocator + param_types: []types.Type args: []ir.Instruction_Id if param_index >= 0 { + init_type := main_function.locals[param_index].type + init_fields := types.fields_for(&module.types, init_type) + io_index := 0 + if !types.equal( + types.resolve_alias(init_fields[0].type, &module.types), + types.resolve_alias(hir_module.functions[provider_index].result, &module.types), + ) { + io_index = 1 + } + args_index := 1 - io_index + args_type := types.resolve_alias(init_fields[args_index].type, &module.types) + values_type := types.resolve_alias(types.fields_for(&module.types, args_type)[0].type, &module.types) + argument_pointer := types.child_type(values_type, &module.types) + argv_type := types.pointer(&module.types, argument_pointer, false, true) + param_types = make([]types.Type, 2, allocator) + param_types[0] = types.C_INT + param_types[1] = argv_type + for param_type, index in param_types { + append(&instructions, ir.Instruction{ + op=.Param, type=param_type, target=ir.local_ref(ir.Local_Id(index)), + a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + } + count := ir.instruction_id(len(instructions)) + append(&instructions, ir.Instruction{ + op=.Scalar_Cast, type=types.USIZE, target=ir.INVALID_REF, + a=ir.Instruction_Id(0), b=ir.INVALID_INSTRUCTION, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + slice_args := make([]ir.Instruction_Id, 2, allocator) + slice_args[0] = ir.INVALID_INSTRUCTION + slice_args[1] = count + values := ir.instruction_id(len(instructions)) + append(&instructions, ir.Instruction{ + op=.Slice, type=values_type, args=slice_args, target=ir.INVALID_REF, + a=ir.Instruction_Id(1), b=ir.INVALID_INSTRUCTION, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + values_args := make([]ir.Instruction_Id, 1, allocator) + values_args[0] = values + process_args := ir.instruction_id(len(instructions)) + append(&instructions, ir.Instruction{ + op=.Aggregate, type=args_type, args=values_args, target=ir.INVALID_REF, + a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION, + diagnostic=source.INVALID_DIAGNOSTIC, + }) provider_call := ir.instruction_id(len(instructions)) append(&instructions, ir.Instruction{ op=.Call, @@ -1793,12 +1841,13 @@ append_injected_main :: proc(module: ^ir.Module, hir_module: ^hir.Module, alloca b=ir.INVALID_INSTRUCTION, diagnostic=source.INVALID_DIAGNOSTIC, }) - init_args := make([]ir.Instruction_Id, 1, allocator) - init_args[0] = provider_call + init_args := make([]ir.Instruction_Id, len(init_fields), allocator) + init_args[io_index] = provider_call + init_args[args_index] = process_args init_value := ir.instruction_id(len(instructions)) append(&instructions, ir.Instruction{ op=.Aggregate, - type=main_function.locals[param_index].type, + type=init_type, args=init_args, target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, @@ -1923,6 +1972,7 @@ append_injected_main :: proc(module: ^ir.Module, hir_module: ^hir.Module, alloca implementation=.Definition, linkage=.External, is_main=true, + param_types=param_types, result=types.I32, instructions=instructions[:], problematic=problematic, diff --git a/compiler_tests.odin b/compiler_tests.odin index 3216dff..2867559 100644 --- a/compiler_tests.odin +++ b/compiler_tests.odin @@ -3718,7 +3718,6 @@ milestone_33_injects_explicit_io_provider_and_runs_std_io :: proc(t: ^testing.T) testing.expect_value(t, len(diagnostics.items), 0) testing.expect(t, hir_module.injected_main != hir.INVALID_FUNCTION) testing.expect(t, hir_module.io_provider != hir.INVALID_FUNCTION) - testing.expect_value(t, count_substring_occurrences(llvm_text, "define i32 @main()"), 1) testing.expect_value(t, count_substring_occurrences(llvm_text, "define internal fastcc i32 @bro__p0__main__"), 1) testing.expect_value(t, count_substring_occurrences(llvm_text, "declare i64 @write("), 1) @@ -3935,7 +3934,6 @@ milestone_37_validates_process_init_and_hidden_system_provider :: proc(t: ^testi Case :: struct { io_source: string, process_source: string, - message: string, } cases := []Case{ { @@ -3943,18 +3941,54 @@ milestone_37_validates_process_init_and_hidden_system_provider :: proc(t: ^testi system func() Io { return Io {value = 0} } `, process_source=`io :: import "@std/io" -Init :: struct { io io.Io } +Args :: struct { values [][*;0]u8 } +Init :: struct { io io.Io, args Args } `, - message="does not provide the required '@hide system func() Io'", }, { io_source=`Io :: struct { value i32 } @hide system func() Io { return Io {value = 0} } `, process_source=`io :: import "@std/io" -Init :: struct { io io.Io, extra i32 } +Args :: struct { values [][*;0]u8 } +Init :: struct { io io.Io, args Args, extra i32 } +`, + }, + { + io_source=`Io :: struct { value i32 } +@hide system func() Io { return Io {value = 0} } +`, + process_source=`io :: import "@std/io" +Args :: struct { values [][*;0]u8 } +Init :: struct { io io.Io } +`, + }, + { + io_source=`Io :: struct { value i32 } +@hide system func() Io { return Io {value = 0} } +`, + process_source=`io :: import "@std/io" +Args :: struct { values [][*]u8 } +Init :: struct { io io.Io, args Args } +`, + }, + { + io_source=`Io :: struct { value i32 } +@hide system func() Io { return Io {value = 0} } +`, + process_source=`io :: import "@std/io" +Args :: struct { values [][*;0]u8, extra i32 } +Init :: struct { io io.Io, args Args } +`, + }, + { + io_source=`Io :: struct { value i32 } +@hide system func() Io { return Io {value = 0} } +`, + process_source=`io :: import "@std/io" +Args :: struct { values [][*;0]u8 } +Init :: struct { io io.Io, args i32 } `, - message="Init must be an auto-layout record containing exactly 'io io.Io'", }, } for test_case, index in cases { @@ -3985,12 +4019,8 @@ main func(init process.Init) void { _ = init } symbols := symbol.init_table() ast_module, loaded := loader.load(app, &sources, &diagnostics, &symbols, project_root_path=root) hir_module := checker.check(&ast_module, &diagnostics, &symbols) - found := false - for diagnostic in diagnostics.items { - found = found || strings.contains(diagnostic.message, test_case.message) - } testing.expect(t, loaded) - testing.expect(t, found) + testing.expect(t, len(diagnostics.items) > 0) hir.destroy_module(&hir_module) ast.destroy_module(&ast_module) symbol.destroy_table(&symbols) @@ -3999,6 +4029,144 @@ main func(init process.Init) void { _ = init } } } +@(test) +process_init_supplies_native_arguments_and_helpers_borrow_explicit_storage :: proc(t: ^testing.T) { + directory := "/tmp/brolang-test-process-args" + main_path := "/tmp/brolang-test-process-args/main.bro" + output := "/tmp/brolang-test-process-args/app" + text := `process :: import "@std/process" +io :: import "@std/io" +mem :: import "@std/mem" + +supplied_args_work func() bool { + text [3;0]mut u8 := ['r', 'u', 'n'] + pointers [1][*;0]u8 :: [(&text).ptr] + args :: process.Args { values = pointers[..] } + if process.argument_count(args) != 1 { return false } + if process.argument(args, 0) |value| { + text[0] = 'f' + if !mem.eql(value, "fun") { return false } + } else { return false } + if process.argument(args, 1) |_| { return false } + empty :: process.Args { values = pointers[0..0] } + if process.argument_count(empty) != 0 { return false } + if process.argument(empty, 0) |_| { return false } + return true +} + +main func(init process.Init) i32 { + count :: process.argument_count(init.args) + if count != 1 and count != 4 { return 1 } + if process.argument(init.args, 0) |executable| { + if !mem.eql(executable, "/tmp/brolang-test-process-args/app") { return 2 } + } else { return 3 } + if process.argument(init.args, count) |_| { return 4 } + if process.argument(init.args, maxval!(usize)) |_| { return 5 } + if count == 4 { + if process.argument(init.args, 1) |operation| { + if !mem.eql(operation, "run") { return 6 } + } else { return 7 } + if process.argument(init.args, 2) |empty| { + if empty.len != 0 { return 8 } + } else { return 9 } + if process.argument(init.args, 3) |spaced| { + if !mem.eql(spaced, "two words") { return 10 } + } else { return 11 } + } + if !supplied_args_work() { return 12 } + io.write_all(io.stdout(init.io), "args-ok\n") catch |_| { return 13 } + return 0 +} +` + _ = os2.remove_all(directory) + defer _ = os2.remove_all(directory) + 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, nil, target.DEFAULT, cimport.Options{}, ".", + ), 0) + commands := [][]string{ + {output}, + {output, "run", "", "two words"}, + } + for command in commands { + state, stdout, stderr, error := os2.process_exec( + os2.Process_Desc{command=command}, context.allocator, + ) + testing.expect(t, error == nil) + testing.expect_value(t, state.exit_code, 0) + testing.expect_value(t, string(stdout), "args-ok\n") + testing.expect_value(t, len(stderr), 0) + delete(stdout) + delete(stderr) + } + + plain_main := "main func() i32 { return 37 }\n" + testing.expect(t, os.write_entire_file(main_path, transmute([]byte)plain_main)) + testing.expect_value(t, compiler_core.compile_package(directory, output), 0) + for command in commands { + state, stdout, stderr, error := os2.process_exec( + os2.Process_Desc{command=command}, context.allocator, + ) + testing.expect(t, error == nil) + testing.expect_value(t, state.exit_code, 37) + testing.expect_value(t, len(stdout), 0) + testing.expect_value(t, len(stderr), 0) + delete(stdout) + delete(stderr) + } +} + +@(test) +process_init_startup_respects_reordered_fields :: proc(t: ^testing.T) { + root := "/tmp/brolang-test-process-reordered" + app := "/tmp/brolang-test-process-reordered/app" + output := "/tmp/brolang-test-process-reordered/program" + io_source := `Io :: struct { value i32 } +@hide system func() Io { return Io {value = 37} } +` + process_source := `io :: import "@std/io" +Args :: struct { values [][*;0]u8 } +Init :: struct { args Args, io io.Io } +` + main_source := `process :: import "@std/process" +main func(init process.Init) i32 { + if init.io.value != 37 or init.args.values.len != 3 { return 1 } + empty :: init.args.values[1] + if empty[0] != 0 { return 2 } + spaced :: init.args.values[2] + if spaced[0] != 'a' or spaced[1] != ' ' or spaced[2] != 'b' or spaced[3] != 0 { return 3 } + return 0 +} +` + _ = os2.remove_all(root) + defer _ = os2.remove_all(root) + testing.expect(t, os2.make_directory_all(app) == nil) + testing.expect(t, os2.make_directory_all(fmt.tprintf("%s/std/io", root)) == nil) + testing.expect(t, os2.make_directory_all(fmt.tprintf("%s/std/process", root)) == nil) + testing.expect(t, os.write_entire_file( + fmt.tprintf("%s/main.bro", app), transmute([]byte)main_source, + )) + testing.expect(t, os.write_entire_file( + fmt.tprintf("%s/std/io/io.bro", root), transmute([]byte)io_source, + )) + testing.expect(t, os.write_entire_file( + fmt.tprintf("%s/std/process/process.bro", root), transmute([]byte)process_source, + )) + testing.expect_value(t, compiler_core.compile_package( + app, output, nil, target.DEFAULT, cimport.Options{}, root, + ), 0) + state, stdout, stderr, error := os2.process_exec( + os2.Process_Desc{command=[]string{output, "", "a b"}}, context.allocator, + ) + defer delete(stdout) + defer delete(stderr) + testing.expect(t, error == nil) + testing.expect_value(t, state.exit_code, 0) + testing.expect_value(t, len(stdout), 0) + testing.expect_value(t, len(stderr), 0) +} + @(test) milestone_37_tuples_reflection_expand_for_and_debug_print_compile_and_run :: proc(t: ^testing.T) { sources := source.init_store() diff --git a/std/process/process.hon b/std/process/process.hon index 7bbeab0..e6d351b 100644 --- a/std/process/process.hon +++ b/std/process/process.hon @@ -2,4 +2,25 @@ import "@std/io" Init :: struct { io io.Io + args Args +} + +Args :: struct { + values [][*;0]u8 +} + +argument_count func(args Args) usize { + return args.values.len +} + +argument func(args Args, index usize) ?[;0]u8 { + if index >= argument_count(args) { + return null + } + pointer :: args.values[index] + length usize := 0 + while pointer[length] != 0 { + length += 1 + } + return pointer[..length;0] }