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 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, .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 `&` 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 }