393 lines
11 KiB
Odin
393 lines
11 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)
|
|
}
|
|
|
|
load_build_config :: proc(project_root: string) -> (BuildConfig, bool) {
|
|
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 {}, false
|
|
}
|
|
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 {}, false
|
|
}
|
|
// 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 {}, false
|
|
}
|
|
return extract_build_config(&hir_module, &symbols)
|
|
}
|
|
|
|
project_root_for_command :: proc(root, command: string) -> (string, bool, bool) {
|
|
if len(root) > 0 {
|
|
return root, false, true
|
|
}
|
|
project_root, found := find_build_root()
|
|
if !found {
|
|
fmt.eprintfln("brolang %s: could not find build.bro in the current directory or any parent", command)
|
|
return "", false, false
|
|
}
|
|
return project_root, true, true
|
|
}
|
|
|
|
// run_build implements `brolang build [root]`: it reads build.bro and compiles
|
|
// the configured program package.
|
|
run_build :: proc(root: string) -> int {
|
|
project_root, owned, found := project_root_for_command(root, "build")
|
|
if !found {
|
|
return 2
|
|
}
|
|
defer if owned {delete(project_root)}
|
|
cfg, ok := load_build_config(project_root)
|
|
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)
|
|
}
|
|
|
|
run_tests :: proc(root: string) -> int {
|
|
project_root, owned, found := project_root_for_command(root, "test")
|
|
if !found {
|
|
return 2
|
|
}
|
|
defer if owned {delete(project_root)}
|
|
cfg, ok := load_build_config(project_root)
|
|
if !ok {
|
|
return 2
|
|
}
|
|
defer destroy_build_config(&cfg)
|
|
|
|
program := filepath.join({project_root, cfg.source_dir})
|
|
defer delete(program)
|
|
test_name := fmt.aprintf("%s-test", cfg.output_name)
|
|
defer delete(test_name)
|
|
output, output_ok := build_output_path(project_root, test_name)
|
|
if !output_ok {
|
|
return 2
|
|
}
|
|
defer delete(output)
|
|
status := compile_package(
|
|
program, output, cfg.link_arguments, target.DEFAULT, cfg.c_options, project_root, .Test,
|
|
)
|
|
if status != 0 {
|
|
return status
|
|
}
|
|
state, stdout, stderr, err := os2.process_exec(
|
|
os2.Process_Desc{command=[]string{output}},
|
|
context.allocator,
|
|
)
|
|
defer delete(stdout)
|
|
defer delete(stderr)
|
|
if len(stdout) > 0 {fmt.print(string(stdout))}
|
|
if len(stderr) > 0 {fmt.eprint(string(stderr))}
|
|
if err != nil {
|
|
fmt.eprintln("failed to run test executable:", err)
|
|
return 2
|
|
}
|
|
return 0 if state.exit_code == 0 else 1
|
|
}
|
|
|
|
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 os.exists(build_dir) {
|
|
if !os.is_dir(build_dir) {
|
|
fmt.eprintfln("build path exists and is not a directory: %s", build_dir)
|
|
delete(build_dir, allocator)
|
|
return "", false
|
|
}
|
|
} else 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, .Pointer_Cast, .Weaken_Slice, .Weaken_Pointer, .Decay_Array_Pointer, .Slice_Ptr,
|
|
.Widen, .Sum_Widen, .Sum_Project, .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
|
|
}
|