build system (first pass)
This commit is contained in:
@@ -0,0 +1,252 @@
|
||||
package compiler
|
||||
|
||||
import "./checker"
|
||||
import "./cimport"
|
||||
import "./hir"
|
||||
import "./linker"
|
||||
import "./loader"
|
||||
import "./source"
|
||||
import "./symbol"
|
||||
import "./target"
|
||||
import "./types"
|
||||
import "core:fmt"
|
||||
import vmem "core:mem/virtual"
|
||||
import "core:path/filepath"
|
||||
import "core:strings"
|
||||
|
||||
// BuildConfig is the native, extracted form of std/build's BuildConfig: all
|
||||
// strings are cloned into context.allocator so they outlive the build module's
|
||||
// arena (freed at the end of run_build). Free with destroy_build_config.
|
||||
BuildConfig :: struct {
|
||||
output_name: string,
|
||||
source_dir: string,
|
||||
link_arguments: []linker.Argument,
|
||||
c_options: cimport.Options,
|
||||
}
|
||||
|
||||
destroy_build_config :: proc(cfg: ^BuildConfig) {
|
||||
delete(cfg.output_name)
|
||||
delete(cfg.source_dir)
|
||||
for arg in cfg.link_arguments {
|
||||
delete(arg.value)
|
||||
}
|
||||
delete(cfg.link_arguments)
|
||||
for path in cfg.c_options.include_paths {
|
||||
delete(path)
|
||||
}
|
||||
delete(cfg.c_options.include_paths)
|
||||
for define in cfg.c_options.defines {
|
||||
delete(define)
|
||||
}
|
||||
delete(cfg.c_options.defines)
|
||||
}
|
||||
|
||||
// run_build implements `brolang build [root]`: it loads and type-checks
|
||||
// `root/build.bro`, reads its `config` constant, and compiles the program
|
||||
// package the config names. build.bro is only checked (never lowered/emitted),
|
||||
// so the config is read straight from the HIR.
|
||||
run_build :: proc(root: string) -> int {
|
||||
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)
|
||||
|
||||
arena: vmem.Arena
|
||||
if err := vmem.arena_init_growing(&arena); err != nil {
|
||||
fmt.eprintln("failed to initialize build arena:", err)
|
||||
return 2
|
||||
}
|
||||
defer vmem.arena_destroy(&arena)
|
||||
a := vmem.arena_allocator(&arena)
|
||||
|
||||
ast_module, loaded := loader.load(root, &sources, &diagnostics, &symbols, a, a, cimport.Options{}, target.DEFAULT)
|
||||
if !loaded {
|
||||
source.print_all(&diagnostics)
|
||||
fmt.eprintln("failed to load build root:", root)
|
||||
return 2
|
||||
}
|
||||
// check needs no `main`: it synthesizes a trap main and emits one benign
|
||||
// "missing main" diagnostic, which is expected for build.bro. Suppress that
|
||||
// one but surface any real errors in build.bro (and fail on them).
|
||||
hir_module := checker.check(&ast_module, &diagnostics, &symbols, target.DEFAULT, a)
|
||||
if build_bro_has_errors(&diagnostics) {
|
||||
source.print_all(&diagnostics)
|
||||
return 2
|
||||
}
|
||||
|
||||
cfg, ok := extract_build_config(&hir_module, &symbols)
|
||||
if !ok {
|
||||
return 2
|
||||
}
|
||||
defer destroy_build_config(&cfg)
|
||||
|
||||
program := filepath.join({root, cfg.source_dir})
|
||||
defer delete(program)
|
||||
return compile_package(program, cfg.output_name, cfg.link_arguments, target.DEFAULT, cfg.c_options)
|
||||
}
|
||||
|
||||
// build_bro_has_errors reports whether checking build.bro produced any diagnostic
|
||||
// other than the benign "missing or unusable main function" (build.bro has no
|
||||
// main by design; that one is emitted with an empty span).
|
||||
build_bro_has_errors :: proc(diagnostics: ^source.Diagnostics) -> bool {
|
||||
for item in diagnostics.items {
|
||||
if item.span == (source.Span{}) && item.message == "missing or unusable main function" {
|
||||
continue
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// extract_build_config finds the top-level `config` constant and reads its
|
||||
// BuildConfig{...} fields out of the HIR. All returned strings are cloned into
|
||||
// context.allocator.
|
||||
extract_build_config :: proc(m: ^hir.Module, symbols: ^symbol.Table) -> (BuildConfig, bool) {
|
||||
config_id := symbol.intern(symbols, "config")
|
||||
config_expr := hir.INVALID_EXPR
|
||||
found := false
|
||||
for g in m.globals {
|
||||
if g.name == config_id {
|
||||
config_expr = g.expr
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
fmt.eprintln("build.bro: missing top-level 'config' constant")
|
||||
return {}, false
|
||||
}
|
||||
root := unwrap_coercions(m, config_expr)
|
||||
if root == hir.INVALID_EXPR || m.exprs[root].kind != .Struct {
|
||||
fmt.eprintln("build.bro: 'config' must be a BuildConfig{...} literal")
|
||||
return {}, false
|
||||
}
|
||||
args := m.exprs[root].args
|
||||
fields := types.fields_for(&m.types, m.exprs[root].type)
|
||||
|
||||
cfg: BuildConfig
|
||||
links: [dynamic]linker.Argument
|
||||
includes: [dynamic]string
|
||||
defines: [dynamic]string
|
||||
|
||||
for field, i in fields {
|
||||
if i >= len(args) {
|
||||
break
|
||||
}
|
||||
switch symbol.resolve(symbols, symbol.Id(field.name)) {
|
||||
case "name":
|
||||
if s, sok := read_string(m, args[i]); sok {
|
||||
cfg.output_name = strings.clone(s)
|
||||
}
|
||||
case "source":
|
||||
if s, sok := read_string(m, args[i]); sok {
|
||||
cfg.source_dir = strings.clone(s)
|
||||
}
|
||||
case "libraries":
|
||||
list := read_string_list(m, args[i])
|
||||
for v in list {
|
||||
append(&links, linker.Argument{kind = .Library, value = strings.clone(v)})
|
||||
}
|
||||
delete(list)
|
||||
case "lib_paths":
|
||||
list := read_string_list(m, args[i])
|
||||
for v in list {
|
||||
append(&links, linker.Argument{kind = .Library_Path, value = strings.clone(v)})
|
||||
}
|
||||
delete(list)
|
||||
case "links":
|
||||
list := read_string_list(m, args[i])
|
||||
for v in list {
|
||||
append(&links, linker.Argument{kind = .Input, value = strings.clone(v)})
|
||||
}
|
||||
delete(list)
|
||||
case "includes":
|
||||
list := read_string_list(m, args[i])
|
||||
for v in list {
|
||||
append(&includes, strings.clone(v))
|
||||
}
|
||||
delete(list)
|
||||
case "defines":
|
||||
list := read_string_list(m, args[i])
|
||||
for v in list {
|
||||
append(&defines, strings.clone(v))
|
||||
}
|
||||
delete(list)
|
||||
}
|
||||
}
|
||||
|
||||
cfg.link_arguments = links[:]
|
||||
cfg.c_options.include_paths = includes[:]
|
||||
cfg.c_options.defines = defines[:]
|
||||
|
||||
if len(cfg.output_name) == 0 || len(cfg.source_dir) == 0 {
|
||||
fmt.eprintln("build.bro: config requires non-empty 'name' and 'source'")
|
||||
destroy_build_config(&cfg)
|
||||
return {}, false
|
||||
}
|
||||
return cfg, true
|
||||
}
|
||||
|
||||
// unwrap_coercions strips implicit coercion wrappers (each stores its inner
|
||||
// expr in `.left`) to reach the underlying value expression.
|
||||
unwrap_coercions :: proc(m: ^hir.Module, id: hir.Expr_Id) -> hir.Expr_Id {
|
||||
cur := id
|
||||
for cur != hir.INVALID_EXPR && int(cur) < len(m.exprs) {
|
||||
#partial switch m.exprs[cur].kind {
|
||||
case .Retype, .Weaken_Slice, .Weaken_Pointer, .Decay_Array_Pointer, .Slice_Ptr,
|
||||
.Widen, .Sum_Widen, .Optional_Some, .C_Coerce, .Scalar_Cast:
|
||||
cur = m.exprs[cur].left
|
||||
case:
|
||||
return cur
|
||||
}
|
||||
}
|
||||
return cur
|
||||
}
|
||||
|
||||
read_string :: proc(m: ^hir.Module, id: hir.Expr_Id) -> (string, bool) {
|
||||
e := unwrap_coercions(m, id)
|
||||
if e == hir.INVALID_EXPR || m.exprs[e].kind != .String {
|
||||
return "", false
|
||||
}
|
||||
sid := m.exprs[e].integer
|
||||
if sid < 0 || int(sid) >= len(m.strings) {
|
||||
return "", false
|
||||
}
|
||||
return m.strings[int(sid)], true
|
||||
}
|
||||
|
||||
// read_string_list reads a `&[...]` list field: the value is an address of an
|
||||
// anonymous global array (see checker `&<array literal>` promotion), whose
|
||||
// elements are strings. Returned strings alias m.strings; callers clone them.
|
||||
// The returned slice is owned by the caller (delete it).
|
||||
read_string_list :: proc(m: ^hir.Module, id: hir.Expr_Id) -> []string {
|
||||
addr := unwrap_coercions(m, id)
|
||||
if addr == hir.INVALID_EXPR || m.exprs[addr].kind != .Address {
|
||||
return nil
|
||||
}
|
||||
g := unwrap_coercions(m, m.exprs[addr].left)
|
||||
if g == hir.INVALID_EXPR || m.exprs[g].kind != .Global {
|
||||
return nil
|
||||
}
|
||||
gid := hir.as_global(m.exprs[g].target)
|
||||
if gid == hir.INVALID_GLOBAL || int(gid) >= len(m.globals) {
|
||||
return nil
|
||||
}
|
||||
arr := m.globals[gid].expr
|
||||
if arr == hir.INVALID_EXPR || m.exprs[arr].kind != .Array {
|
||||
return nil
|
||||
}
|
||||
elems := m.exprs[arr].args
|
||||
out := make([]string, len(elems))
|
||||
for a, i in elems {
|
||||
s, ok := read_string(m, a)
|
||||
if !ok {
|
||||
delete(out)
|
||||
return nil
|
||||
}
|
||||
out[i] = s
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -152,6 +152,10 @@ Checker :: struct {
|
||||
infer_stack: [dynamic]Infer_Frame,
|
||||
build_stack: [dynamic]Build_Expr_Frame,
|
||||
cycle_stack: [dynamic]Cycle_Frame,
|
||||
// Anonymous globals synthesized for `&<array literal>` (Zig's `&.{...}`). Staged
|
||||
// here during global/function building and flushed into module.globals AFTER
|
||||
// build_globals, so the 1:1 module.globals <-> ast.globals index identity holds.
|
||||
anon_globals: [dynamic]hir.Global,
|
||||
main_symbol: symbol.Id,
|
||||
sink_symbol: symbol.Id,
|
||||
type_symbol: symbol.Id,
|
||||
@@ -3792,6 +3796,46 @@ build_compound_expr :: proc(
|
||||
diagnostic=source.INVALID_DIAGNOSTIC,
|
||||
})
|
||||
case .Address:
|
||||
// `&<array literal>` (Zig's `&.{...}`): the operand is an rvalue with no
|
||||
// address, so promote it to an anonymous global constant and take *its*
|
||||
// address. Reuses the existing non-scalar-global storage path; only the
|
||||
// stable global address enters the expression, so it never dangles. The
|
||||
// resulting `*[N]T` then decays to a slice via the usual coercion.
|
||||
if expr.left != ast.INVALID_EXPR && checker.ast_module.exprs[expr.left].kind == .Array {
|
||||
operand := checker.ast_module.exprs[expr.left]
|
||||
// Propagate an element-expected type through `&` so literal elements
|
||||
// coerce to the target slice's element type (e.g. string -> []u8).
|
||||
// Without this, `&["x"]` infers `*[1]*[N:0]u8`, which won't decay to
|
||||
// `[][]u8` because can_decay_array_pointer requires child equality.
|
||||
element := types.INVALID
|
||||
if node, ok := types.node(store, expected); ok && (node.kind == .Slice || node.kind == .Array) {
|
||||
element = node.child
|
||||
}
|
||||
synth_expected := types.INVALID
|
||||
if types.is_valid(element) {
|
||||
synth_expected = types.array(store, element, u64(len(operand.args)), false)
|
||||
}
|
||||
value := build_nested_expr(checker, expr.left, locals, global_reads, calls, synth_expected, pkg, file)
|
||||
array_type := checker.module.exprs[value].type
|
||||
hidden_id := hir.Global_Id(len(checker.ast_module.globals) + len(checker.anon_globals))
|
||||
append(&checker.anon_globals, hir.Global{
|
||||
name = symbol.intern(checker.symbols, "__anon.array"),
|
||||
type = array_type,
|
||||
expr = value,
|
||||
writable = false,
|
||||
external = false,
|
||||
diagnostic = source.INVALID_DIAGNOSTIC,
|
||||
})
|
||||
add_unique_global(global_reads, hidden_id)
|
||||
global_ref := add_hir_expr(checker, hir.Expr{
|
||||
kind=.Global, span=expr.span, type=array_type, target=hir.global_ref(hidden_id),
|
||||
left=hir.INVALID_EXPR, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC,
|
||||
})
|
||||
return add_hir_expr(checker, hir.Expr{
|
||||
kind=.Address, span=expr.span, type=types.pointer(store, array_type, false, false),
|
||||
left=global_ref, target=hir.INVALID_REF, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC,
|
||||
})
|
||||
}
|
||||
value := build_nested_expr(checker, expr.left, locals, global_reads, calls, types.INVALID, pkg, file)
|
||||
if !hir_is_location(checker, value) {
|
||||
id := source.add(checker.diagnostics, expr.span, "'&' requires an addressable location")
|
||||
@@ -8087,6 +8131,7 @@ check :: proc(
|
||||
checker.infer_stack.allocator = allocator
|
||||
checker.build_stack.allocator = allocator
|
||||
checker.cycle_stack.allocator = allocator
|
||||
checker.anon_globals.allocator = allocator
|
||||
build_symbol_indexes(&checker)
|
||||
checker.global_types = make([]types.Type, len(ast_module.globals), allocator)
|
||||
checker.global_demands = make([]types.Type, len(ast_module.globals), allocator)
|
||||
@@ -8167,6 +8212,12 @@ check :: proc(
|
||||
for index := 0; index < len(checker.specs); index += 1 {
|
||||
build_function(&checker, spec_id(index))
|
||||
}
|
||||
// Flush anonymous globals synthesized for `&<array literal>`. Appended only now
|
||||
// (after every ast global was built at its identity-mapped index) so their ids,
|
||||
// pre-assigned as len(ast.globals)+stage_index, land exactly.
|
||||
for anon in checker.anon_globals {
|
||||
append(&checker.module.globals, anon)
|
||||
}
|
||||
propagate_global_reads(&checker)
|
||||
|
||||
main_template := find_template(&checker, checker.main_symbol, 0)
|
||||
|
||||
Reference in New Issue
Block a user