Files
brolang/compiler/build.odin
T

342 lines
9.8 KiB
Odin

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:os"
import "core:os/os2"
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 {
project_root := root
owns_project_root := false
if len(project_root) == 0 {
found_root, found := find_build_root()
if !found {
fmt.eprintln("brolang build: could not find build.bro in the current directory or any parent")
return 2
}
project_root = found_root
owns_project_root = true
}
defer if owns_project_root {
delete(project_root)
}
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(project_root, &sources, &diagnostics, &symbols, a, a, cimport.Options{}, target.DEFAULT, project_root)
if !loaded {
source.print_all(&diagnostics)
fmt.eprintln("failed to load build root:", project_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({project_root, cfg.source_dir})
defer delete(program)
output, output_ok := build_output_path(project_root, cfg.output_name)
if !output_ok {
return 2
}
defer delete(output)
return compile_package(program, output, cfg.link_arguments, target.DEFAULT, cfg.c_options, project_root)
}
valid_output_name :: proc(name: string) -> bool {
return len(name) > 0 && name != "." && name != ".." &&
!strings.contains(name, "/") && !strings.contains(name, "\\")
}
build_output_path :: proc(project_root, name: string, allocator := context.allocator) -> (string, bool) {
if !valid_output_name(name) {
fmt.eprintfln("build.bro: config 'name' must be a plain executable name, got '%s'", name)
return "", false
}
build_dir, dir_error := filepath.join({project_root, "build"}, allocator)
if dir_error != nil {
return "", false
}
if err := os2.make_directory_all(build_dir); err != nil {
fmt.eprintfln("failed to create build directory '%s': %v", build_dir, err)
delete(build_dir, allocator)
return "", false
}
output, output_error := filepath.join({build_dir, name}, allocator)
delete(build_dir, allocator)
if output_error != nil {
return "", false
}
return output, true
}
find_build_root :: proc(allocator := context.allocator) -> (string, bool) {
current := os.get_current_directory(allocator)
if len(current) == 0 {
return "", false
}
defer delete(current, allocator)
return find_build_root_from(current, allocator)
}
find_build_root_from :: proc(start: string, allocator := context.allocator) -> (string, bool) {
current, current_ok := filepath.abs(start, allocator)
if !current_ok {
current = strings.clone(start, allocator)
}
for {
build_path, build_error := filepath.join({current, "build.bro"}, allocator)
if build_error != nil {
delete(current, allocator)
return "", false
}
found := os.exists(build_path)
delete(build_path, allocator)
if found {
return current, true
}
if current == "/" {
delete(current, allocator)
return "", false
}
parent := filepath.dir(current, allocator)
if parent == current {
delete(parent, allocator)
delete(current, allocator)
return "", false
}
delete(current, allocator)
current = parent
}
}
// 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
}