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
+113 -8
View File
@@ -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
}
}
}
+4
View File
@@ -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
+19 -9
View File
@@ -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] {
+64
View File
@@ -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
}