diff --git a/LANGUAGE.md b/LANGUAGE.md index 968ab1c..f8ee7ea 100644 --- a/LANGUAGE.md +++ b/LANGUAGE.md @@ -31,7 +31,7 @@ roadmap and milestone history. - UTF-8 string literals as immutable pointers to static zero-terminated byte arrays, plus raw backtick multiline strings - narrow immutable zero-terminated byte pointer/slice conversion to `*c_char` / `?*c_char` without general `u8`/`c_char` interchange - optionals with `none`, `orelse`, postfix `?`, conditional unwraps, guarded unwraps, and left-to-right short-circuiting multi-unwraps -- nominal distinct types with exact backing construction, native enums with optional explicit integer backing, contextual enum literals, and imported C enums as target-backed integer aliases +- nominal distinct types with exact backing construction, native enums with optional explicit integer backing and explicit backing-to-scalar casts, contextual enum literals, and imported C enums as target-backed integer aliases - source-order native structs, opaque nominal records with `Name :: opaque`, complete `c_struct { ... }`, keyed record literals, native untagged unions, and native tagged unions `union(Enum)` / `union(enum)` - void-payload tagged-union variants, anonymous struct payloads, contextual `.variant`, `.variant{payload}`, and `.variant{field = value}` construction - native sum composition with `A | B` for unbacked enums and tagged unions, using program-global `u16` variant ids @@ -123,6 +123,7 @@ The six spellings are reserved only as direct unqualified calls. A qualified cal - `std/mem` generic slice equality, allocator contract with raw byte operations, typed `empty` / `alloc` / `realloc` / `free`, overflow checks, zero-sized-type support, and failure-preserving reallocation - `std/arraylist` generic `ArrayList(T)` with direct `items` slice access, explicit capacity, allocator ownership, fallible reserve/append, clear, and deinit +- `std/io` explicit `Io` capabilities, `Reader`/`Writer` stream values, one-shot `read`/`write`, and allocation-free `write_all` ### compiler behavior @@ -130,6 +131,7 @@ The six spellings are reserved only as direct unqualified calls. A qualified cal - 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 the canonical `@std/io Io`; the injected form is called through a synthesized no-argument C entry point - replaceable dynamically loaded libclang C-import backend - C-header import caching by canonical path, target, include paths, and defines diff --git a/README.md b/README.md index 793b7df..9626f63 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,22 @@ odin build . -out:build/brolang ./build/prototype ``` +Programs may receive the system I/O capability explicitly. Readers and writers +pair that implementation with a stream; `main func() ...` remains valid. + +```bro +io :: import "@std/io" + +main func(system io.Io) void { + io.write_all(io.Writer { + impl = system, + stream = .stdout, + }, "hello\n") catch |_| { + return + } +} +``` + 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/TODO.md b/TODO.md index be8a802..641f126 100644 --- a/TODO.md +++ b/TODO.md @@ -807,7 +807,14 @@ ordinary float `/` remains the unchecked IEEE infinity/NaN escape hatch - migrated `std/mem`, `std/arraylist`, and the compound-assignment example to `div_trunc` -33. design io interface +33. explicit I/O provider (implemented) + - `main` may take one canonical `@std/io Io`; parameterless entry points remain valid + - the compiler supplies a file-hidden macOS provider through an external no-argument C wrapper + - readers and writers pair an explicit provider with `stdin`, `stdout`, or `stderr` + - `read` and `write` validate provider counts; `write_all` handles partial writes and no progress + - the system provider uses unbuffered libc `read`/`write`, retries interruption, and allocates nothing + +34. re-exports so `std` can re-export e.g. `ArrayList(T)`, so users can do `std.ArrayList(i32)` instead of `std.arraylist.ArrayList(i32)`, while still using `std.arraylist.append(&values, 420)`. thinking this would be nice ergonomically. ## A word on unchecked casts diff --git a/compiler/checker/checker.odin b/compiler/checker/checker.odin index db53a71..6f69e25 100644 --- a/compiler/checker/checker.odin +++ b/compiler/checker/checker.odin @@ -183,6 +183,8 @@ Checker :: struct { // build_globals, so the 1:1 module.globals <-> ast.globals index identity holds. anon_globals: [dynamic]hir.Global, main_symbol: symbol.Id, + io_main: bool, + io_provider_template: ast.Function_Id, sink_symbol: symbol.Id, type_symbol: symbol.Id, current_result: types.Type, @@ -894,6 +896,66 @@ find_template :: proc(checker: ^Checker, name: symbol.Id, pkg := ast.Package_Id( return find_function_symbol(checker.function_index, pkg, name, file) } +configure_io_main :: proc(checker: ^Checker) { + main_template := find_template(checker, checker.main_symbol, 0) + if main_template == ast.INVALID_FUNCTION { + return + } + main := checker.ast_module.functions[main_template] + if len(main.params) != 1 || main.params[0].comptime_value { + return + } + + parameter_type := types.resolve_alias( + type_from_syntax(checker, main.params[0].type, main.pkg, main.file), + &checker.module.types, + ) + io_name := symbol.intern(checker.symbols, "Io") + io_package := ast.INVALID_PACKAGE + io_type := types.INVALID + for import_item in checker.ast_module.imports { + if import_item.valid && import_item.path == "@std/io" { + candidate := types.find_named(&checker.module.types, u32(import_item.target), u32(io_name)) + if types.equal(parameter_type, candidate) { + io_package = import_item.target + io_type = candidate + break + } + } + } + if io_package == ast.INVALID_PACKAGE { + return + } + + provider_name := symbol.intern(checker.symbols, "_system") + provider := ast.INVALID_FUNCTION + for function, function_id in checker.ast_module.functions { + if function.pkg != io_package || function.name != provider_name { + continue + } + result := types.resolve_alias( + type_from_syntax(checker, function.result, function.pkg, function.file), + &checker.module.types, + ) + if function.has_body && !function.c_abi && len(function.params) == 0 && + !types.is_valid(function.error) && types.equal(result, io_type) { + provider = ast.function_id(function_id) + break + } + } + if provider == ast.INVALID_FUNCTION { + checker.template_diagnostics[main_template] = source.add( + checker.diagnostics, + main.span, + "@std/io does not provide the required '_system func() Io' startup implementation", + ) + return + } + + checker.io_main = true + checker.io_provider_template = provider +} + find_global :: proc(checker: ^Checker, name: symbol.Id, pkg := ast.Package_Id(0), file := ast.INVALID_FILE) -> ast.Global_Id { return find_global_symbol(checker.global_index, pkg, name, file) } @@ -2095,6 +2157,18 @@ validate_external_globals :: proc(checker: ^Checker) { } } +runtime_write_declaration_matches :: proc(checker: ^Checker, function: ast.Function) -> bool { + if function.variadic || len(function.params) != 3 || types.is_valid(function.error) { + return false + } + store := &checker.module.types + buffer := types.optional(store, types.pointer(store, types.ANYOPAQUE, false, true)) + return type_from_syntax(checker, function.params[0].type, function.pkg, function.file) == types.C_INT && + type_from_syntax(checker, function.params[1].type, function.pkg, function.file) == buffer && + type_from_syntax(checker, function.params[2].type, function.pkg, function.file) == types.C_ULONG && + type_from_syntax(checker, function.result, function.pkg, function.file) == types.C_LONG +} + validate_declarations :: proc(checker: ^Checker) { for function, function_id in checker.ast_module.functions { if len(function.unsupported_reason) > 0 { @@ -2261,6 +2335,14 @@ validate_declarations :: proc(checker: ^Checker) { "main must have a body", ) } + external_name := function.link_name if len(function.link_name) > 0 else symbol_text(checker, function.name) + if external_name == "write" && !runtime_write_declaration_matches(checker, function) { + checker.template_diagnostics[function_id] = source.add( + checker.diagnostics, + function.span, + "external C function 'write' conflicts with the compiler runtime declaration", + ) + } } mark_block_imports_used(checker, function.body, function.file) delete(locals) @@ -4070,6 +4152,9 @@ infer_all :: proc(checker: ^Checker) { if main_template != ast.INVALID_FUNCTION { ensure_spec(checker, main_template, nil) } + if checker.io_main { + ensure_spec(checker, checker.io_provider_template, nil) + } defaults_applied := false for { @@ -4196,6 +4281,9 @@ prune_specs :: proc(checker: ^Checker) { if main_template != ast.INVALID_FUNCTION { mark_spec_demanded(checker, find_spec(checker, main_template, nil), &stack) } + if checker.io_main { + mark_spec_demanded(checker, find_spec(checker, checker.io_provider_template, nil), &stack) + } for global in checker.ast_module.globals { if global.external { continue @@ -5192,7 +5280,11 @@ build_compound_expr :: proc( value := build_nested_expr(checker, expr.left, locals, global_reads, calls, types.INVALID, pkg, file) actual := checker.module.exprs[value].type valid_target := types.is_concrete_scalar(target) && !types.is_bool(target) - valid_actual := types.is_concrete_scalar(actual) && !types.is_bool(actual) + actual_repr := types.runtime_representation(actual, store) + actual_item, actual_item_ok := types.node(store, actual) + explicit_enum := actual_item_ok && actual_item.kind == .Enum && actual_item.explicit_backing + valid_actual := (types.is_concrete_scalar(actual) || explicit_enum) && + types.is_concrete_scalar(actual_repr) && !types.is_bool(actual_repr) if !valid_target || !valid_actual { id := source.addf( checker.diagnostics, @@ -6536,7 +6628,7 @@ 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 { + if function.pkg == 0 && function.name == checker.main_symbol && !checker.io_main { return fmt.aprintf("main", allocator = checker.allocator) } if function.generated { @@ -9151,6 +9243,7 @@ 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.io_main if !function.has_body { assert(spec.hir_id == hir.function_id(len(checker.module.functions))) append( @@ -9161,7 +9254,7 @@ build_function :: proc(checker: ^Checker, id: Spec_Id) { calling_convention = .C if function.c_abi else .Brolang, implementation = .Declaration, linkage = .External if function.c_abi else .Internal, - is_main = function.pkg == 0 && function.name == checker.main_symbol, + is_main = native_main, variadic = function.variadic, params = params[:], result = spec.result, @@ -9259,10 +9352,10 @@ build_function :: proc(checker: ^Checker, id: Spec_Id) { hir.Function { name = function.name, link_name = make_link_name(checker, id), - calling_convention = .C if function.c_abi || (function.pkg == 0 && function.name == checker.main_symbol) else .Brolang, + calling_convention = .C if function.c_abi || native_main else .Brolang, implementation = .Definition, - linkage = .External if function.c_abi || (function.pkg == 0 && function.name == checker.main_symbol) else .Internal, - is_main = function.pkg == 0 && function.name == checker.main_symbol, + linkage = .External if function.c_abi || native_main else .Internal, + is_main = native_main, variadic = function.variadic, params = params[:], result = spec.result, @@ -9710,6 +9803,7 @@ check :: proc( symbols = symbols, module = hir.init_module(selected, allocator), main_symbol = symbol.intern(symbols, "main"), + io_provider_template = ast.INVALID_FUNCTION, sink_symbol = symbol.intern(symbols, "_"), type_symbol = symbol.intern(symbols, "type"), target = selected, @@ -9823,6 +9917,7 @@ check :: proc( validate_type_nodes(&checker) validate_declarations(&checker) + configure_io_main(&checker) infer_all(&checker) validate_external_globals(&checker) prune_specs(&checker) @@ -9849,19 +9944,29 @@ check :: proc( synthesize_trap_main(&checker) } else { template := ast_module.functions[main_template] + valid_params := len(template.params) == 0 || checker.io_main && len(template.params) == 1 if main_declarations != 1 || !template.has_body || - len(template.params) != 0 || + !valid_params || !(template.result == types.VOID || template.result == types.I32 || template.result == types.INT) { id := checker.template_diagnostics[main_template] if id == source.INVALID_DIAGNOSTIC { id = source.add( diagnostics, template.span, - "main must be unique, have a body, take no parameters, and return void, i32, or int", + "main must be unique, have a body, take no parameters or one @std/io Io, and return void, i32, or int", ) } + checker.module.injected_main = hir.INVALID_FUNCTION + checker.module.io_provider = hir.INVALID_FUNCTION replace_main_with_trap(&checker, id) + } else if checker.io_main { + 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 { + checker.module.injected_main = checker.specs[main_spec].hir_id + checker.module.io_provider = checker.specs[provider_spec].hir_id + } } } diff --git a/compiler/hir/hir.odin b/compiler/hir/hir.odin index 6515449..2c5859d 100644 --- a/compiler/hir/hir.odin +++ b/compiler/hir/hir.odin @@ -259,6 +259,8 @@ Module :: struct { functions: [dynamic]Function, globals: [dynamic]Global, strings: [dynamic]string, + injected_main: Function_Id, + io_provider: Function_Id, types: types.Store, target: target.Target, allocator: mem.Allocator, @@ -267,6 +269,8 @@ Module :: struct { init_module :: proc(selected := target.DEFAULT, allocator := context.allocator) -> Module { module: Module module.target = selected + module.injected_main = INVALID_FUNCTION + module.io_provider = INVALID_FUNCTION module.types = types.init_store(allocator) module.types.selected = selected module.allocator = allocator diff --git a/compiler/llvm/llvm.odin b/compiler/llvm/llvm.odin index c97a487..36c4f2e 100644 --- a/compiler/llvm/llvm.odin +++ b/compiler/llvm/llvm.odin @@ -1648,18 +1648,23 @@ emit_instruction_stream :: proc( ) fmt.sbprintf(&emitter.builder, ", %s zeroinitializer\n", type_name) case .Scalar_Cast: - if !valid_instruction(instructions, instruction.a) || - !types.is_concrete_scalar(instructions[instruction.a].type) || - !types.is_concrete_scalar(instruction.type) || - types.is_bool(instructions[instruction.a].type) || - types.is_bool(instruction.type) { + if !valid_instruction(instructions, instruction.a) { emit_recovery_value(emitter, instruction_index, instruction, "invalid scalar cast operand") continue } from_type := instructions[instruction.a].type - from_bits := types.bits(from_type, emitter.module.target) + from_repr := types.runtime_representation(from_type, &emitter.module.types) + from_item, from_item_ok := types.node(&emitter.module.types, from_type) + explicit_enum := from_item_ok && from_item.kind == .Enum && from_item.explicit_backing + valid_from := (types.is_concrete_scalar(from_type) || explicit_enum) && + types.is_concrete_scalar(from_repr) && !types.is_bool(from_repr) + if !valid_from || !types.is_concrete_scalar(instruction.type) || types.is_bool(instruction.type) { + emit_recovery_value(emitter, instruction_index, instruction, "invalid scalar cast operand") + continue + } + from_bits := types.bits(from_repr, emitter.module.target) to_bits := types.bits(instruction.type, emitter.module.target) - from_float := types.is_float(from_type, emitter.module.target) + from_float := types.is_float(from_repr, emitter.module.target) to_float := types.is_float(instruction.type, emitter.module.target) if types.equal(from_type, instruction.type) || from_bits == to_bits && from_float == to_float { type_name := llvm_type(instruction.type, &emitter.module.types) @@ -1675,11 +1680,11 @@ emit_instruction_stream :: proc( case from_float && to_float: operation = "fpext" if from_bits < to_bits else "fptrunc" case !from_float && !to_float: - operation = "trunc" if from_bits > to_bits else ("sext" if types.is_signed(from_type, emitter.module.target) else "zext") + operation = "trunc" if from_bits > to_bits else ("sext" if types.is_signed(from_repr, emitter.module.target) else "zext") case from_float: operation = "fptosi" if types.is_signed(instruction.type, emitter.module.target) else "fptoui" case: - operation = "sitofp" if types.is_signed(from_type, emitter.module.target) else "uitofp" + operation = "sitofp" if types.is_signed(from_repr, emitter.module.target) else "uitofp" } fmt.sbprintf(&emitter.builder, " %%v%d = %s %s ", instruction_index, operation, llvm_type(from_type, &emitter.module.types)) write_operand(&emitter.builder, instructions, instruction.a, from_type, &emitter.module.types) @@ -2357,6 +2362,11 @@ emit_constructor :: proc(emitter: ^Emitter) { emit_functions :: proc(emitter: ^Emitter) { for function, function_index in emitter.module.functions { + // bro.trap already declares libc write. A demanded std/io binding shares + // that declaration instead of emitting an LLVM redefinition. + if function.implementation == .Declaration && function.link_name == "write" { + continue + } if function.implementation == .Declaration { duplicate := false for previous in emitter.module.functions[:function_index] { diff --git a/compiler/lower/lower.odin b/compiler/lower/lower.odin index fc7d965..0987d5a 100644 --- a/compiler/lower/lower.odin +++ b/compiler/lower/lower.odin @@ -1608,6 +1608,69 @@ lower_global_initializer :: proc(hir_module: ^hir.Module, global: hir.Global, al return state.instructions[:] } +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)) + provider_index, provider_ok := hir.index(hir_module.io_provider, hir.INVALID_FUNCTION, len(hir_module.functions)) + if !main_ok || !provider_ok { + 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, + }) + args := make([]ir.Instruction_Id, 1, allocator) + args[0] = provider_call + main_call := ir.instruction_id(len(instructions)) + append(&instructions, ir.Instruction{ + op=.Call, + type=hir_module.functions[main_index].result, + args=args, + target=ir.function_ref(ir.Function_Id(main_index)), + a=ir.INVALID_INSTRUCTION, + b=ir.INVALID_INSTRUCTION, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + if types.is_void(hir_module.functions[main_index].result) { + append(&instructions, ir.Instruction{ + op=.Return_Void, + type=types.VOID, + 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, + diagnostic=source.INVALID_DIAGNOSTIC, + }) + } + + 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, + instructions=instructions[:], + problematic=hir_module.functions[main_index].problematic || + hir_module.functions[provider_index].problematic, + }) +} + lower :: proc(hir_module: ^hir.Module, allocator := context.allocator) -> ir.Module { module := ir.init_module(hir_module.target, allocator) types.destroy_store(&module.types) @@ -1649,5 +1712,6 @@ lower :: proc(hir_module: ^hir.Module, allocator := context.allocator) -> ir.Mod problematic=function.problematic, }) } + append_injected_main(&module, hir_module, allocator) return module } diff --git a/compiler_tests.odin b/compiler_tests.odin index 7e1490b..d1ea338 100644 --- a/compiler_tests.odin +++ b/compiler_tests.odin @@ -2051,6 +2051,116 @@ bodyless_root_main_recovers_as_a_trap_definition :: proc(t: ^testing.T) { testing.expect(t, !strings.contains(llvm_text, "declare i32 @main()")) } +@(test) +milestone_33_injects_explicit_io_provider_and_runs_std_io :: proc(t: ^testing.T) { + sources := source.init_store() + defer source.destroy_store(&sources) + diagnostics := source.init_store_diagnostics(&sources) + defer source.destroy_diagnostics(&diagnostics) + symbols := symbol.init_table() + defer symbol.destroy_table(&symbols) + ast_module, loaded := loader.load( + "examples/programs/io", + &sources, + &diagnostics, + &symbols, + project_root_path=".", + ) + 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(t, loaded) + 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) + + output := "/tmp/brolang-test-io" + defer _ = os.remove(output) + status := compiler_core.compile_package( + "examples/programs/io", + 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, 0) + testing.expect_value(t, string(stdout), "io-ok\n") +} + +@(test) +milestone_33_rejects_non_io_and_extra_main_parameters :: proc(t: ^testing.T) { + cases := [?]string{ + "main func(value i32) void {}\n", + "main func(left, right i32) void {}\n", + } + for text in cases { + source_file := source.Source{path="test.bro", text=text} + diagnostics := source.init_diagnostics(&source_file) + symbols := symbol.init_table() + stream := lexer.lex(&source_file, &diagnostics, &symbols) + ast_module := parser.parse(&stream, &source_file, &diagnostics) + hir_module := checker.check(&ast_module, &diagnostics, &symbols) + + found := false + for diagnostic in diagnostics.items { + found = found || strings.contains( + diagnostic.message, + "take no parameters or one @std/io Io", + ) + } + testing.expect(t, found) + + hir.destroy_module(&hir_module) + ast.destroy_module(&ast_module) + delete(stream.items) + symbol.destroy_table(&symbols) + source.destroy_diagnostics(&diagnostics) + } +} + +@(test) +milestone_33_rejects_an_incompatible_runtime_write_declaration :: proc(t: ^testing.T) { + text := `write c_func(_ c_int, _ c_int, _ c_ulong) c_long +main func() void {} +` + 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) + + found := false + for diagnostic in diagnostics.items { + found = found || strings.contains( + diagnostic.message, + "external C function 'write' conflicts with the compiler runtime declaration", + ) + } + testing.expect(t, found) +} + @(test) literal_addition_trees_fold_with_contextual_types :: proc(t: ^testing.T) { text := `return_i16 func() i16 { @@ -9911,8 +10021,9 @@ main func() i32 { value Animal = .cat values [2]Animal :: [.dog, Animal.bird] number Nat = identity(.two) + ordinal c_int :: c_int(number) _ = variadic(0, number) - if take(value) == Animal.cat and values[0] != values[1] and number == Nat.two { + if take(value) == Animal.cat and values[0] != values[1] and number == Nat.two and ordinal == 2 { return 0 } return 1 @@ -9954,6 +10065,7 @@ main func() i32 { testing.expect_value(t, nat_members[0].value, i128(1)) testing.expect_value(t, nat_members[1].value, i128(2)) testing.expect_value(t, nat_members[2].value, i128(5)) + testing.expect(t, strings.contains(llvm_text, "zext i16")) testing.expect_value(t, types.runtime_representation(animal, &ast_module.type_store), types.U16) testing.expect_value(t, types.size(animal, &ast_module.type_store), u64(2)) testing.expect(t, hir_module.globals[0].is_static) diff --git a/examples/programs/io/main.bro b/examples/programs/io/main.bro new file mode 100644 index 0000000..6d7071d --- /dev/null +++ b/examples/programs/io/main.bro @@ -0,0 +1,124 @@ +io :: import "@std/io" + +_read_ok func(_ ?*mut anyopaque, _ io.ReadStream, buffer []mut u8) usize ! io.ReadError { + if buffer.len == 0 { + return 0 + } + buffer[0] = 'o' + if buffer.len == 1 { + return 1 + } + buffer[1] = 'k' + return 2 +} + +_read_too_much func(_ ?*mut anyopaque, _ io.ReadStream, buffer []mut u8) usize ! io.ReadError { + return buffer.len + 1 +} + +_read_eof func(_ ?*mut anyopaque, _ io.ReadStream, _ []mut u8) usize ! io.ReadError { + return 0 +} + +_write_short func(_ ?*mut anyopaque, _ io.WriteStream, bytes []u8) usize ! io.WriteError { + if bytes.len > 2 { + return 2 + } + return bytes.len +} + +_write_none func(_ ?*mut anyopaque, _ io.WriteStream, _ []u8) usize ! io.WriteError { + return 0 +} + +_write_too_much func(_ ?*mut anyopaque, _ io.WriteStream, bytes []u8) usize ! io.WriteError { + return bytes.len + 1 +} + +_ok_vtable io.IoVTable :: io.IoVTable { + read = _read_ok, + write = _write_short, +} + +_read_bad_vtable io.IoVTable :: io.IoVTable { + read = _read_too_much, + write = _write_short, +} + +_eof_vtable io.IoVTable :: io.IoVTable { + read = _read_eof, + write = _write_short, +} + +_write_none_vtable io.IoVTable :: io.IoVTable { + read = _read_ok, + write = _write_none, +} + +_write_bad_vtable io.IoVTable :: io.IoVTable { + read = _read_ok, + write = _write_too_much, +} + +reader_for func(vtable @io.IoVTable) io.Reader { + return io.Reader { + impl = io.Io {context = none, vtable = vtable}, + stream = .stdin, + } +} + +writer_for func(vtable @io.IoVTable) io.Writer { + return io.Writer { + impl = io.Io {context = none, vtable = vtable}, + stream = .stdout, + } +} + +rejects_bad_read func() bool { + buffer [1]mut u8 = [0] + _ = io.read(reader_for(&_read_bad_vtable), buffer[..]) catch |err| { + return err == .read_failed + } + return false +} + +rejects_no_progress func() bool { + io.write_all(writer_for(&_write_none_vtable), "x") catch |err| { + return err == .no_progress + } + return false +} + +rejects_bad_write func() bool { + _ = io.write(writer_for(&_write_bad_vtable), "x") catch |err| { + return err == .write_failed + } + return false +} + +main func(system io.Io) i32 { + buffer [2]mut u8 = [0, 0] + count usize :: io.read(reader_for(&_ok_vtable), buffer[..]) catch 0 + if count != 2 or buffer[0] != 'o' or buffer[1] != 'k' { + return 1 + } + eof usize :: io.read(reader_for(&_eof_vtable), buffer[..]) catch 1 + empty_read usize :: io.read(reader_for(&_read_bad_vtable), buffer[0..0]) catch 1 + empty_write usize :: io.write(writer_for(&_write_bad_vtable), "") catch 1 + if eof != 0 or empty_read != 0 or empty_write != 0 { + return 5 + } + io.write_all(writer_for(&_ok_vtable), "partial") catch |_| { + return 2 + } + if !rejects_bad_read() or !rejects_no_progress() or !rejects_bad_write() { + return 3 + } + io.write_all(io.Writer { + impl = system, + stream = .stdout, + }, "io-ok\n") catch |_| { + return 4 + } + return 0 +} diff --git a/ffi/c/posix.bro b/ffi/c/posix.bro new file mode 100644 index 0000000..7b4a190 --- /dev/null +++ b/ffi/c/posix.bro @@ -0,0 +1,3 @@ +read c_func(_ c_int, _ ?*mut anyopaque, _ c_ulong) c_long +write c_func(_ c_int, _ ?*anyopaque, _ c_ulong) c_long +__error c_func() *mut c_int diff --git a/std/io/io.bro b/std/io/io.bro new file mode 100644 index 0000000..bf51ec9 --- /dev/null +++ b/std/io/io.bro @@ -0,0 +1,122 @@ +c :: import "@ffi/c" + +ReadError :: enum { + read_failed +} + +WriteError :: enum { + write_failed + no_progress +} + +Io :: struct { + context ?*mut anyopaque + vtable @IoVTable +} + +IoVTable :: struct { + read @func(context ?*mut anyopaque, stream ReadStream, buffer []mut u8) usize ! ReadError + write @func(context ?*mut anyopaque, stream WriteStream, bytes []u8) usize ! WriteError +} + +ReadStream :: enum(c_int) { + stdin = 0 +} + +WriteStream :: enum(c_int) { + stdout = 1 + stderr = 2 +} + +Reader :: struct { + impl Io + stream ReadStream +} + +Writer :: struct { + impl Io + stream WriteStream +} + +read func(reader Reader, buffer []mut u8) usize ! ReadError { + if buffer.len == 0 { + return 0 + } + count usize :: try reader.impl.vtable.read(reader.impl.context, reader.stream, buffer) + if count > buffer.len { + return .read_failed + } + return count +} + +write func(writer Writer, bytes []u8) usize ! WriteError { + if bytes.len == 0 { + return 0 + } + count usize :: try writer.impl.vtable.write(writer.impl.context, writer.stream, bytes) + if count > bytes.len { + return .write_failed + } + return count +} + +write_all func(writer Writer, bytes []u8) void ! WriteError { + offset usize = 0 + while offset < bytes.len { + count usize :: write(writer, bytes[offset..]) catch |err| { + return err + } + if count == 0 { + return .no_progress + } + offset += count + } + return _ +} + +_system_read func(_ ?*mut anyopaque, stream ReadStream, buffer []mut u8) usize ! ReadError { + request usize = buffer.len + maximum usize :: usize(max_value(c_long)) + if request > maximum { + request = maximum + } + while true { + count c_long :: c.read(c_int(stream), buffer.ptr, c_ulong(request)) + if count >= 0 { + return usize(count) + } + if c.__error()^ != 4 { + return .read_failed + } + } +} + +_system_write func(_ ?*mut anyopaque, stream WriteStream, bytes []u8) usize ! WriteError { + fd c_int :: c_int(stream) + request usize = bytes.len + maximum usize :: usize(max_value(c_long)) + if request > maximum { + request = maximum + } + while true { + count c_long :: c.write(fd, bytes.ptr, c_ulong(request)) + if count >= 0 { + return usize(count) + } + if c.__error()^ != 4 { + return .write_failed + } + } +} + +_system_vtable IoVTable :: IoVTable { + read = _system_read, + write = _system_write, +} + +_system func() Io { + return Io { + context = none, + vtable = &_system_vtable, + } +}