io interface (first pass)

This commit is contained in:
2026-07-13 12:02:06 +02:00
parent 2ed333c70d
commit 288df082e2
11 changed files with 589 additions and 20 deletions
+3 -1
View File
@@ -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 - 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 - 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 - 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)` - 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 - 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 - 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/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/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 ### 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 - lazy semantic checking of demanded function specializations
- static, eager runtime, mutable runtime, and deferred problematic globals with cycle diagnostics - static, eager runtime, mutable runtime, and deferred problematic globals with cycle diagnostics
- demand-driven LLVM declarations for referenced foreign functions - 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 - replaceable dynamically loaded libclang C-import backend
- C-header import caching by canonical path, target, include paths, and defines - C-header import caching by canonical path, target, include paths, and defines
+16
View File
@@ -9,6 +9,22 @@ odin build . -out:build/brolang
./build/prototype ./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 Bodyless `c_func` declarations bind exact external symbols and require concrete
types. C primitives use atomic target-dependent names and remain semantically types. C primitives use atomic target-dependent names and remain semantically
distinct from exact-width Brolang primitives: distinct from exact-width Brolang primitives:
+8 -1
View File
@@ -807,7 +807,14 @@
ordinary float `/` remains the unchecked IEEE infinity/NaN escape hatch ordinary float `/` remains the unchecked IEEE infinity/NaN escape hatch
- migrated `std/mem`, `std/arraylist`, and the compound-assignment example to `div_trunc` - 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 ## A word on unchecked casts
+113 -8
View File
@@ -183,6 +183,8 @@ Checker :: struct {
// build_globals, so the 1:1 module.globals <-> ast.globals index identity holds. // build_globals, so the 1:1 module.globals <-> ast.globals index identity holds.
anon_globals: [dynamic]hir.Global, anon_globals: [dynamic]hir.Global,
main_symbol: symbol.Id, main_symbol: symbol.Id,
io_main: bool,
io_provider_template: ast.Function_Id,
sink_symbol: symbol.Id, sink_symbol: symbol.Id,
type_symbol: symbol.Id, type_symbol: symbol.Id,
current_result: types.Type, 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) 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 { 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) 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) { validate_declarations :: proc(checker: ^Checker) {
for function, function_id in checker.ast_module.functions { for function, function_id in checker.ast_module.functions {
if len(function.unsupported_reason) > 0 { if len(function.unsupported_reason) > 0 {
@@ -2261,6 +2335,14 @@ validate_declarations :: proc(checker: ^Checker) {
"main must have a body", "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) mark_block_imports_used(checker, function.body, function.file)
delete(locals) delete(locals)
@@ -4070,6 +4152,9 @@ infer_all :: proc(checker: ^Checker) {
if main_template != ast.INVALID_FUNCTION { if main_template != ast.INVALID_FUNCTION {
ensure_spec(checker, main_template, nil) ensure_spec(checker, main_template, nil)
} }
if checker.io_main {
ensure_spec(checker, checker.io_provider_template, nil)
}
defaults_applied := false defaults_applied := false
for { for {
@@ -4196,6 +4281,9 @@ prune_specs :: proc(checker: ^Checker) {
if main_template != ast.INVALID_FUNCTION { if main_template != ast.INVALID_FUNCTION {
mark_spec_demanded(checker, find_spec(checker, main_template, nil), &stack) 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 { for global in checker.ast_module.globals {
if global.external { if global.external {
continue continue
@@ -5192,7 +5280,11 @@ build_compound_expr :: proc(
value := build_nested_expr(checker, expr.left, locals, global_reads, calls, types.INVALID, pkg, file) value := build_nested_expr(checker, expr.left, locals, global_reads, calls, types.INVALID, pkg, file)
actual := checker.module.exprs[value].type actual := checker.module.exprs[value].type
valid_target := types.is_concrete_scalar(target) && !types.is_bool(target) 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 { if !valid_target || !valid_actual {
id := source.addf( id := source.addf(
checker.diagnostics, checker.diagnostics,
@@ -6536,7 +6628,7 @@ build_expr :: proc(
make_link_name :: proc(checker: ^Checker, id: Spec_Id) -> string { make_link_name :: proc(checker: ^Checker, id: Spec_Id) -> string {
spec := checker.specs[id] spec := checker.specs[id]
function := checker.ast_module.functions[spec.template] 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) return fmt.aprintf("main", allocator = checker.allocator)
} }
if function.generated { if function.generated {
@@ -9151,6 +9243,7 @@ build_function :: proc(checker: ^Checker, id: Spec_Id) {
problematic := signature_diagnostic != source.INVALID_DIAGNOSTIC || problematic := signature_diagnostic != source.INVALID_DIAGNOSTIC ||
checker.template_diagnostics[spec.template] != 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 { if !function.has_body {
assert(spec.hir_id == hir.function_id(len(checker.module.functions))) assert(spec.hir_id == hir.function_id(len(checker.module.functions)))
append( append(
@@ -9161,7 +9254,7 @@ build_function :: proc(checker: ^Checker, id: Spec_Id) {
calling_convention = .C if function.c_abi else .Brolang, calling_convention = .C if function.c_abi else .Brolang,
implementation = .Declaration, implementation = .Declaration,
linkage = .External if function.c_abi else .Internal, 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, variadic = function.variadic,
params = params[:], params = params[:],
result = spec.result, result = spec.result,
@@ -9259,10 +9352,10 @@ build_function :: proc(checker: ^Checker, id: Spec_Id) {
hir.Function { hir.Function {
name = function.name, name = function.name,
link_name = make_link_name(checker, id), 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, implementation = .Definition,
linkage = .External if function.c_abi || (function.pkg == 0 && function.name == checker.main_symbol) else .Internal, linkage = .External if function.c_abi || native_main else .Internal,
is_main = function.pkg == 0 && function.name == checker.main_symbol, is_main = native_main,
variadic = function.variadic, variadic = function.variadic,
params = params[:], params = params[:],
result = spec.result, result = spec.result,
@@ -9710,6 +9803,7 @@ check :: proc(
symbols = symbols, symbols = symbols,
module = hir.init_module(selected, allocator), module = hir.init_module(selected, allocator),
main_symbol = symbol.intern(symbols, "main"), main_symbol = symbol.intern(symbols, "main"),
io_provider_template = ast.INVALID_FUNCTION,
sink_symbol = symbol.intern(symbols, "_"), sink_symbol = symbol.intern(symbols, "_"),
type_symbol = symbol.intern(symbols, "type"), type_symbol = symbol.intern(symbols, "type"),
target = selected, target = selected,
@@ -9823,6 +9917,7 @@ check :: proc(
validate_type_nodes(&checker) validate_type_nodes(&checker)
validate_declarations(&checker) validate_declarations(&checker)
configure_io_main(&checker)
infer_all(&checker) infer_all(&checker)
validate_external_globals(&checker) validate_external_globals(&checker)
prune_specs(&checker) prune_specs(&checker)
@@ -9849,19 +9944,29 @@ check :: proc(
synthesize_trap_main(&checker) synthesize_trap_main(&checker)
} else { } else {
template := ast_module.functions[main_template] template := ast_module.functions[main_template]
valid_params := len(template.params) == 0 || checker.io_main && len(template.params) == 1
if main_declarations != 1 || if main_declarations != 1 ||
!template.has_body || !template.has_body ||
len(template.params) != 0 || !valid_params ||
!(template.result == types.VOID || template.result == types.I32 || template.result == types.INT) { !(template.result == types.VOID || template.result == types.I32 || template.result == types.INT) {
id := checker.template_diagnostics[main_template] id := checker.template_diagnostics[main_template]
if id == source.INVALID_DIAGNOSTIC { if id == source.INVALID_DIAGNOSTIC {
id = source.add( id = source.add(
diagnostics, diagnostics,
template.span, 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) 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
}
} }
} }
+4
View File
@@ -259,6 +259,8 @@ Module :: struct {
functions: [dynamic]Function, functions: [dynamic]Function,
globals: [dynamic]Global, globals: [dynamic]Global,
strings: [dynamic]string, strings: [dynamic]string,
injected_main: Function_Id,
io_provider: Function_Id,
types: types.Store, types: types.Store,
target: target.Target, target: target.Target,
allocator: mem.Allocator, allocator: mem.Allocator,
@@ -267,6 +269,8 @@ Module :: struct {
init_module :: proc(selected := target.DEFAULT, allocator := context.allocator) -> Module { init_module :: proc(selected := target.DEFAULT, allocator := context.allocator) -> Module {
module: Module module: Module
module.target = selected module.target = selected
module.injected_main = INVALID_FUNCTION
module.io_provider = INVALID_FUNCTION
module.types = types.init_store(allocator) module.types = types.init_store(allocator)
module.types.selected = selected module.types.selected = selected
module.allocator = allocator module.allocator = allocator
+19 -9
View File
@@ -1648,18 +1648,23 @@ emit_instruction_stream :: proc(
) )
fmt.sbprintf(&emitter.builder, ", %s zeroinitializer\n", type_name) fmt.sbprintf(&emitter.builder, ", %s zeroinitializer\n", type_name)
case .Scalar_Cast: case .Scalar_Cast:
if !valid_instruction(instructions, instruction.a) || 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) {
emit_recovery_value(emitter, instruction_index, instruction, "invalid scalar cast operand") emit_recovery_value(emitter, instruction_index, instruction, "invalid scalar cast operand")
continue continue
} }
from_type := instructions[instruction.a].type 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) 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) 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 { if types.equal(from_type, instruction.type) || from_bits == to_bits && from_float == to_float {
type_name := llvm_type(instruction.type, &emitter.module.types) type_name := llvm_type(instruction.type, &emitter.module.types)
@@ -1675,11 +1680,11 @@ emit_instruction_stream :: proc(
case from_float && to_float: case from_float && to_float:
operation = "fpext" if from_bits < to_bits else "fptrunc" operation = "fpext" if from_bits < to_bits else "fptrunc"
case !from_float && !to_float: 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: case from_float:
operation = "fptosi" if types.is_signed(instruction.type, emitter.module.target) else "fptoui" operation = "fptosi" if types.is_signed(instruction.type, emitter.module.target) else "fptoui"
case: 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)) 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) 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) { emit_functions :: proc(emitter: ^Emitter) {
for function, function_index in emitter.module.functions { 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 { if function.implementation == .Declaration {
duplicate := false duplicate := false
for previous in emitter.module.functions[:function_index] { for previous in emitter.module.functions[:function_index] {
+64
View File
@@ -1608,6 +1608,69 @@ lower_global_initializer :: proc(hir_module: ^hir.Module, global: hir.Global, al
return state.instructions[:] 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 { lower :: proc(hir_module: ^hir.Module, allocator := context.allocator) -> ir.Module {
module := ir.init_module(hir_module.target, allocator) module := ir.init_module(hir_module.target, allocator)
types.destroy_store(&module.types) types.destroy_store(&module.types)
@@ -1649,5 +1712,6 @@ lower :: proc(hir_module: ^hir.Module, allocator := context.allocator) -> ir.Mod
problematic=function.problematic, problematic=function.problematic,
}) })
} }
append_injected_main(&module, hir_module, allocator)
return module return module
} }
+113 -1
View File
@@ -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()")) 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) @(test)
literal_addition_trees_fold_with_contextual_types :: proc(t: ^testing.T) { literal_addition_trees_fold_with_contextual_types :: proc(t: ^testing.T) {
text := `return_i16 func() i16 { text := `return_i16 func() i16 {
@@ -9911,8 +10021,9 @@ main func() i32 {
value Animal = .cat value Animal = .cat
values [2]Animal :: [.dog, Animal.bird] values [2]Animal :: [.dog, Animal.bird]
number Nat = identity(.two) number Nat = identity(.two)
ordinal c_int :: c_int(number)
_ = variadic(0, 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 0
} }
return 1 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[0].value, i128(1))
testing.expect_value(t, nat_members[1].value, i128(2)) testing.expect_value(t, nat_members[1].value, i128(2))
testing.expect_value(t, nat_members[2].value, i128(5)) 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.runtime_representation(animal, &ast_module.type_store), types.U16)
testing.expect_value(t, types.size(animal, &ast_module.type_store), u64(2)) testing.expect_value(t, types.size(animal, &ast_module.type_store), u64(2))
testing.expect(t, hir_module.globals[0].is_static) testing.expect(t, hir_module.globals[0].is_static)
+124
View File
@@ -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
}
+3
View File
@@ -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
+122
View File
@@ -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,
}
}