add new and init to toolchain

This commit is contained in:
2026-07-04 17:38:45 +02:00
parent cdde49e68b
commit e7f69ffb5a
17 changed files with 711 additions and 63 deletions
+14 -10
View File
@@ -31,9 +31,12 @@ invocation in command-line order:
C preprocessing. C preprocessing.
Instead of passing these on the command line, a project can describe its build Instead of passing these on the command line, a project can describe its build
in Brolang itself. `brolang build [root]` (root defaults to the current in Brolang itself. `brolang new <project>` creates a project with local `std`
directory) reads a `config` constant from `root/build.bro` and compiles the and `ffi` copies; `brolang init` does the same for the current directory without
program package it names: overwriting existing files. `brolang build [root]` reads a `config` constant
from `root/build.bro` and compiles the program package it names. Without
`root`, it searches the current directory and parents for the nearest
`build.bro`. Build outputs are written to `root/build/<name>`.
```bro ```bro
b :: import "@std/build" b :: import "@std/build"
@@ -49,12 +52,12 @@ config :: b.BuildConfig{
} }
``` ```
`source` is the program package, relative to `build.bro`. The list fields map to `name` is a plain executable name, and `source` is the program package relative
the matching C options (`libraries``-l`, `lib_paths``-L`, `includes` to `build.bro`. The list fields map to the matching C options (`libraries`
`-I`, `defines` → C defines, `links` linker inputs) and, like those flags, `-l`, `lib_paths``-L`, `includes``-I`, `defines` → C defines, `links`
their paths are relative to the invocation directory. Lists take the address of linker inputs) and, like those flags, their paths are relative to the invocation
an array literal; empty lists are written `&[]`. See `examples/build/` for directory. Lists take the address of an array literal; empty lists are written
runnable projects. `&[]`. See `examples/build/` for runnable projects.
Relative `.h` imports create synthetic package namespaces backed by libclang: Relative `.h` imports create synthetic package namespaces backed by libclang:
@@ -146,11 +149,12 @@ specialization is demanded.
Every immediate `.bro` file in the input directory belongs to the root Every immediate `.bro` file in the input directory belongs to the root
package. Imports are relative directory paths and are local to the file that package. Imports are relative directory paths and are local to the file that
declares them: declares them. Imports beginning with `@` resolve from the project root:
```bro ```bro
import "../math" import "../math"
other_math :: import "../math" other_math :: import "../math"
heap :: import "@std/mem/heap"
value :: math.sum(other_math.value, 1) value :: math.sum(other_math.value, 1)
``` ```
+5 -2
View File
@@ -626,8 +626,9 @@
- typed allocation, allocator parameters, arenas/pools, build-mode heap policy, and escaping-allocation diagnostics remain deferred - typed allocation, allocator parameters, arenas/pools, build-mode heap policy, and escaping-allocation diagnostics remain deferred
26. import from project "root" (implemented) 26. import from project "root" (implemented)
- imports beginning with `@` resolve from the compiler process cwd / project root - imports beginning with `@` resolve from the project root
- `heap :: import "@std/mem/heap"` works from any package depth without `../../../` path math - `brolang build [root]` uses `root` as the project root; without `root`, it searches cwd and parents for `build.bro`
- direct `brolang <package-dir> -o <out>` defaults the project root to the package dir; `--root <dir>` overrides it
27. comptime integer value parameters (implemented; v1) 27. comptime integer value parameters (implemented; v1)
- `$N` marks an integer comptime parameter in a normal `func` signature: - `$N` marks an integer comptime parameter in a normal `func` signature:
@@ -715,6 +716,8 @@
`root/build.bro` (importing `@std/build`'s `BuildConfig`) and compiles the `root/build.bro` (importing `@std/build`'s `BuildConfig`) and compiles the
program package it names. The config is read from the checked HIR — build.bro program package it names. The config is read from the checked HIR — build.bro
is never lowered — so it is literal-only. is never lowered — so it is literal-only.
- `brolang new <project>` and `brolang init` create the default project layout
with local `std`, `ffi`, `vendor`, `source`, and `build.bro`
- enabled `&<array literal>` (Zig's `&.{...}`): the literal is promoted to an - enabled `&<array literal>` (Zig's `&.{...}`): the literal is promoted to an
anonymous global whose address decays to a slice, so list fields like anonymous global whose address decays to a slice, so list fields like
`libraries = &["raylib"]` work; empty lists are `&[]` `libraries = &["raylib"]` work; empty lists are `&[]`
+27
View File
@@ -13,6 +13,32 @@ append_owned :: proc(command: ^[dynamic]string, value: string, allocator: mem.Al
append(command, strings.clone(value, allocator)) append(command, strings.clone(value, allocator))
} }
macos_sdk_root :: proc(allocator := context.allocator) -> (string, bool) {
candidates := []string{
"/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk",
"/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk",
}
for path in candidates {
if os.exists(path) {
return strings.clone(path, allocator), true
}
}
return "", false
}
append_macos_sdk_paths :: proc(command: ^[dynamic]string, selected: target.Target, allocator: mem.Allocator) {
if selected.kind != .Aarch64_Macos {
return
}
sdk, ok := macos_sdk_root(allocator)
if !ok {
return
}
defer delete(sdk, allocator)
append(command, fmt.aprintf("-F%s/System/Library/Frameworks", sdk, allocator=allocator))
append(command, fmt.aprintf("-L%s/usr/lib", sdk, allocator=allocator))
}
build_command :: proc( build_command :: proc(
llvm_path, output_path: string, llvm_path, output_path: string,
link_arguments: []linker.Argument, link_arguments: []linker.Argument,
@@ -29,6 +55,7 @@ build_command :: proc(
append_owned(&command, target.name(selected), allocator) append_owned(&command, target.name(selected), allocator)
append_owned(&command, "-Wno-override-module", allocator) append_owned(&command, "-Wno-override-module", allocator)
append_owned(&command, "-Wno-unused-command-line-argument", allocator) append_owned(&command, "-Wno-unused-command-line-argument", allocator)
append_macos_sdk_paths(&command, selected, allocator)
append_owned(&command, llvm_path, allocator) append_owned(&command, llvm_path, allocator)
for path in c_options.include_paths { for path in c_options.include_paths {
append(&command, fmt.aprintf("-I%s", path, allocator=allocator)) append(&command, fmt.aprintf("-I%s", path, allocator=allocator))
+93 -4
View File
@@ -11,6 +11,8 @@ import "./target"
import "./types" import "./types"
import "core:fmt" import "core:fmt"
import vmem "core:mem/virtual" import vmem "core:mem/virtual"
import "core:os"
import "core:os/os2"
import "core:path/filepath" import "core:path/filepath"
import "core:strings" import "core:strings"
@@ -46,6 +48,21 @@ destroy_build_config :: proc(cfg: ^BuildConfig) {
// package the config names. build.bro is only checked (never lowered/emitted), // package the config names. build.bro is only checked (never lowered/emitted),
// so the config is read straight from the HIR. // so the config is read straight from the HIR.
run_build :: proc(root: string) -> int { 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() sources := source.init_store()
defer source.destroy_store(&sources) defer source.destroy_store(&sources)
diagnostics := source.init_store_diagnostics(&sources) diagnostics := source.init_store_diagnostics(&sources)
@@ -61,10 +78,10 @@ run_build :: proc(root: string) -> int {
defer vmem.arena_destroy(&arena) defer vmem.arena_destroy(&arena)
a := vmem.arena_allocator(&arena) a := vmem.arena_allocator(&arena)
ast_module, loaded := loader.load(root, &sources, &diagnostics, &symbols, a, a, cimport.Options{}, target.DEFAULT) ast_module, loaded := loader.load(project_root, &sources, &diagnostics, &symbols, a, a, cimport.Options{}, target.DEFAULT, project_root)
if !loaded { if !loaded {
source.print_all(&diagnostics) source.print_all(&diagnostics)
fmt.eprintln("failed to load build root:", root) fmt.eprintln("failed to load build root:", project_root)
return 2 return 2
} }
// check needs no `main`: it synthesizes a trap main and emits one benign // check needs no `main`: it synthesizes a trap main and emits one benign
@@ -82,9 +99,81 @@ run_build :: proc(root: string) -> int {
} }
defer destroy_build_config(&cfg) defer destroy_build_config(&cfg)
program := filepath.join({root, cfg.source_dir}) program := filepath.join({project_root, cfg.source_dir})
defer delete(program) defer delete(program)
return compile_package(program, cfg.output_name, cfg.link_arguments, target.DEFAULT, cfg.c_options) 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 // build_bro_has_errors reports whether checking build.bro produced any diagnostic
+14 -1
View File
@@ -616,9 +616,22 @@ add_label_shadow_diagnostic :: proc(ctx: ^Build_Ctx, span: source.Span, label: s
if !symbol.is_valid(label) { if !symbol.is_valid(label) {
return source.INVALID_DIAGNOSTIC return source.INVALID_DIAGNOSTIC
} }
yield_targets := ctx.yield_targets^[:]
if len(yield_targets) > 0 && yield_targets[len(yield_targets) - 1].label == label {
label_is_active_loop := false
for loop_label in ctx.loop_labels^[:] {
if loop_label == label {
label_is_active_loop = true
break
}
}
if !label_is_active_loop {
yield_targets = yield_targets[:len(yield_targets) - 1]
}
}
return add_shadow_diagnostic( return add_shadow_diagnostic(
ctx.checker, span, label, "label", ctx.checker, span, label, "label",
ctx.pkg, ctx.file, ctx.locals^[:], ctx.loop_labels^[:], ctx.yield_targets^[:], ctx.pkg, ctx.file, ctx.locals^[:], ctx.loop_labels^[:], yield_targets,
) )
} }
+2
View File
@@ -33,6 +33,7 @@ compile_package :: proc(
link_arguments: []linker.Argument = nil, link_arguments: []linker.Argument = nil,
selected := target.DEFAULT, selected := target.DEFAULT,
c_options := cimport.Options{}, c_options := cimport.Options{},
project_root := "",
) -> int { ) -> int {
sources := source.init_store() sources := source.init_store()
defer source.destroy_store(&sources) defer source.destroy_store(&sources)
@@ -75,6 +76,7 @@ compile_package :: proc(
vmem.arena_allocator(&parser_arena), vmem.arena_allocator(&parser_arena),
c_options, c_options,
selected, selected,
project_root if len(project_root) > 0 else input_path,
) )
if !loaded { if !loaded {
source.print_all(&diagnostics) source.print_all(&diagnostics)
+4 -2
View File
@@ -1416,11 +1416,13 @@ load :: proc(
allocator := context.allocator, allocator := context.allocator,
c_options := cimport.Options{}, c_options := cimport.Options{},
selected := target.DEFAULT, selected := target.DEFAULT,
project_root_path := "",
) -> (ast.Module, bool) { ) -> (ast.Module, bool) {
module := ast.init_module(allocator) module := ast.init_module(allocator)
project_root, project_root_ok := filepath.abs(".", allocator) project_root_source := project_root_path if len(project_root_path) > 0 else root_path
project_root, project_root_ok := filepath.abs(project_root_source, allocator)
if !project_root_ok { if !project_root_ok {
project_root = strings.clone(".", allocator) project_root = strings.clone(project_root_source, allocator)
} }
state := State{ state := State{
module=&module, module=&module,
+234 -10
View File
@@ -736,6 +736,8 @@ cli_parses_ordered_link_options_and_rejects_invalid_forms :: proc(t: ^testing.T)
"vendor/include", "vendor/include",
"--c-define", "--c-define",
"FEATURE=1", "FEATURE=1",
"--root",
".",
"--target", "--target",
"aarch64-macos", "aarch64-macos",
}) })
@@ -746,6 +748,7 @@ cli_parses_ordered_link_options_and_rejects_invalid_forms :: proc(t: ^testing.T)
testing.expect(t, valid) testing.expect(t, valid)
testing.expect_value(t, options.input_path, "app") testing.expect_value(t, options.input_path, "app")
testing.expect_value(t, options.output_path, "app.out") testing.expect_value(t, options.output_path, "app.out")
testing.expect_value(t, options.project_root, ".")
testing.expect_value(t, len(options.link_arguments), 4) testing.expect_value(t, len(options.link_arguments), 4)
testing.expect_value(t, options.link_arguments[0].kind, linker.Kind.Input) testing.expect_value(t, options.link_arguments[0].kind, linker.Kind.Input)
testing.expect_value(t, options.link_arguments[0].value, "native.c") testing.expect_value(t, options.link_arguments[0].value, "native.c")
@@ -759,6 +762,7 @@ cli_parses_ordered_link_options_and_rejects_invalid_forms :: proc(t: ^testing.T)
_, unknown_valid := parse_cli_args([]string{"brolang", "app", "-o", "out", "--unknown", "value"}) _, unknown_valid := parse_cli_args([]string{"brolang", "app", "-o", "out", "--unknown", "value"})
_, incomplete_valid := parse_cli_args([]string{"brolang", "app", "-o"}) _, incomplete_valid := parse_cli_args([]string{"brolang", "app", "-o"})
_, duplicate_output_valid := parse_cli_args([]string{"brolang", "app", "-o", "one", "-o", "two"}) _, duplicate_output_valid := parse_cli_args([]string{"brolang", "app", "-o", "one", "-o", "two"})
_, duplicate_root_valid := parse_cli_args([]string{"brolang", "app", "-o", "out", "--root", ".", "--root", "other"})
_, duplicate_empty_output_valid := parse_cli_args([]string{"brolang", "app", "-o", "", "-o", "two"}) _, duplicate_empty_output_valid := parse_cli_args([]string{"brolang", "app", "-o", "", "-o", "two"})
_, invalid_target := parse_cli_args([]string{"brolang", "app", "-o", "out", "--target", "x86_64-linux"}) _, invalid_target := parse_cli_args([]string{"brolang", "app", "-o", "out", "--target", "x86_64-linux"})
_, legacy_link := parse_cli_args([]string{"brolang", "app", "-o", "out", "--link", "native.c"}) _, legacy_link := parse_cli_args([]string{"brolang", "app", "-o", "out", "--link", "native.c"})
@@ -767,6 +771,7 @@ cli_parses_ordered_link_options_and_rejects_invalid_forms :: proc(t: ^testing.T)
testing.expect(t, !unknown_valid) testing.expect(t, !unknown_valid)
testing.expect(t, !incomplete_valid) testing.expect(t, !incomplete_valid)
testing.expect(t, !duplicate_output_valid) testing.expect(t, !duplicate_output_valid)
testing.expect(t, !duplicate_root_valid)
testing.expect(t, !duplicate_empty_output_valid) testing.expect(t, !duplicate_empty_output_valid)
testing.expect(t, !invalid_target) testing.expect(t, !invalid_target)
testing.expect(t, !legacy_link) testing.expect(t, !legacy_link)
@@ -2560,22 +2565,194 @@ build_command_is_recognized :: proc(t: ^testing.T) {
@(test) @(test)
build_subcommand_compiles_and_runs :: proc(t: ^testing.T) { build_subcommand_compiles_and_runs :: proc(t: ^testing.T) {
defer _ = os.remove("hello") output := "examples/build/hello/build/hello"
defer _ = os2.remove_all("examples/build/hello/build")
status := compiler_core.run_build("examples/build/hello") status := compiler_core.run_build("examples/build/hello")
testing.expect_value(t, status, 0) testing.expect_value(t, status, 0)
state := run_executable("./hello") state := run_executable(output)
testing.expect_value(t, state.exit_code, 0) testing.expect_value(t, state.exit_code, 0)
} }
@(test) @(test)
build_subcommand_links_c_source_via_list_field :: proc(t: ^testing.T) { build_subcommand_links_c_source_via_list_field :: proc(t: ^testing.T) {
defer _ = os.remove("manual_build") output := "examples/build/manual/build/manual_build"
defer _ = os2.remove_all("examples/build/manual/build")
status := compiler_core.run_build("examples/build/manual") status := compiler_core.run_build("examples/build/manual")
testing.expect_value(t, status, 0) testing.expect_value(t, status, 0)
state := run_executable("./manual_build") state := run_executable(output)
testing.expect_value(t, state.exit_code, 42) testing.expect_value(t, state.exit_code, 42)
} }
@(test)
new_project_creates_layout_and_builds :: proc(t: ^testing.T) {
root := "/tmp/brolang-test-new-project"
output := "/tmp/brolang-test-new-project/build/brolang-test-new-project"
_ = os2.remove_all(root)
defer _ = os2.remove_all(root)
testing.expect_value(t, run_new_project(root), 0)
testing.expect(t, os.is_dir("/tmp/brolang-test-new-project/source"))
testing.expect(t, os.is_dir("/tmp/brolang-test-new-project/std"))
testing.expect(t, os.is_dir("/tmp/brolang-test-new-project/ffi"))
testing.expect(t, os.is_dir("/tmp/brolang-test-new-project/vendor"))
testing.expect(t, os.exists("/tmp/brolang-test-new-project/build.bro"))
testing.expect(t, os.exists("/tmp/brolang-test-new-project/source/main.bro"))
status := compiler_core.run_build(root)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
init_project_creates_missing_layout_and_preserves_existing_files :: proc(t: ^testing.T) {
root := "/tmp/brolang-test-init-project"
output := "/tmp/brolang-test-init-project/build/brolang-test-init-project"
_ = os2.remove_all(root)
defer _ = os2.remove_all(root)
testing.expect(t, os2.make_directory_all(root) == nil)
testing.expect(t, init_project(root))
testing.expect(t, os.is_dir("/tmp/brolang-test-init-project/source"))
testing.expect(t, os.is_dir("/tmp/brolang-test-init-project/std"))
testing.expect(t, os.is_dir("/tmp/brolang-test-init-project/ffi"))
testing.expect(t, os.is_dir("/tmp/brolang-test-init-project/vendor"))
status := compiler_core.run_build(root)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
custom_build := "custom build"
custom_main := "custom main"
testing.expect(t, os.write_entire_file("/tmp/brolang-test-init-project/build.bro", transmute([]byte)custom_build))
testing.expect(t, os.write_entire_file("/tmp/brolang-test-init-project/source/main.bro", transmute([]byte)custom_main))
testing.expect(t, init_project(root))
build_data, build_ok := os.read_entire_file("/tmp/brolang-test-init-project/build.bro")
defer delete(build_data)
main_data, main_ok := os.read_entire_file("/tmp/brolang-test-init-project/source/main.bro")
defer delete(main_data)
testing.expect(t, build_ok && string(build_data) == custom_build)
testing.expect(t, main_ok && string(main_data) == custom_main)
}
@(test)
build_root_search_finds_nearest_parent_build_file :: proc(t: ^testing.T) {
root := "/tmp/brolang-test-build-search"
nested := "/tmp/brolang-test-build-search/source/nested"
_ = os2.remove_all(root)
defer _ = os2.remove_all(root)
testing.expect(t, os2.make_directory_all(nested) == nil)
build_text := "# root\n"
testing.expect(t, os.write_entire_file("/tmp/brolang-test-build-search/build.bro", transmute([]byte)build_text))
found, ok := compiler_core.find_build_root_from(nested)
defer delete(found)
testing.expect(t, ok)
testing.expect(t, strings.has_suffix(found, "/brolang-test-build-search"))
}
@(test)
build_subcommand_discovered_root_writes_to_root_build_dir :: proc(t: ^testing.T) {
root := "/tmp/brolang-test-build-nested"
nested := "/tmp/brolang-test-build-nested/source/nested"
output := "/tmp/brolang-test-build-nested/build/brolang-test-build-nested"
_ = os2.remove_all(root)
defer _ = os2.remove_all(root)
testing.expect(t, init_project(root))
testing.expect(t, os2.make_directory_all(nested) == nil)
found, ok := compiler_core.find_build_root_from(nested)
defer delete(found)
testing.expect(t, ok)
status := compiler_core.run_build(found)
testing.expect_value(t, status, 0)
testing.expect(t, os.exists(output))
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
build_subcommand_rejects_path_like_output_name :: proc(t: ^testing.T) {
root := "/tmp/brolang-test-build-invalid-name"
_ = os2.remove_all(root)
defer _ = os2.remove_all(root)
testing.expect(t, init_project(root))
text := `b :: import "@std/build"
config :: b.BuildConfig{
name = "bad/name",
source = "source",
libraries = &[],
lib_paths = &[],
includes = &[],
defines = &[],
links = &[],
}
`
testing.expect(t, os.write_entire_file("/tmp/brolang-test-build-invalid-name/build.bro", transmute([]byte)text))
status := compiler_core.run_build(root)
testing.expect_value(t, status, 2)
testing.expect(t, !os.exists("/tmp/brolang-test-build-invalid-name/build/bad/name"))
}
@(test)
project_root_imports_resolve_under_passed_root :: proc(t: ^testing.T) {
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)
ast_module, loaded := loader.load(
"examples/build/raylib/source",
&sources,
&diagnostics,
&symbols,
context.allocator,
context.allocator,
cimport.Options{},
target.DEFAULT,
"examples/build/raylib",
)
defer ast.destroy_module(&ast_module)
testing.expect(t, loaded)
found_vendor := false
for pkg in ast_module.packages {
found_vendor = found_vendor || strings.has_suffix(pkg.path, "/examples/build/raylib/vendor/raylib")
}
testing.expect(t, found_vendor)
}
@(test)
direct_compile_defaults_project_root_to_input_package_and_allows_override :: proc(t: ^testing.T) {
root := "/tmp/brolang-test-direct-root"
app := "/tmp/brolang-test-direct-root/app"
dep := "/tmp/brolang-test-direct-root/dep"
output := "/tmp/brolang-test-direct-root-out"
_ = os2.remove_all(root)
_ = os.remove(output)
defer _ = os2.remove_all(root)
defer _ = os.remove(output)
testing.expect(t, os2.make_directory_all(app) == nil)
testing.expect(t, os2.make_directory_all(dep) == nil)
main_text := "dep :: import \"@dep\"\nmain func() i32 { return dep.value }\n"
dep_text := "value i32 :: 7\n"
testing.expect(t, os.write_entire_file("/tmp/brolang-test-direct-root/app/main.bro", transmute([]byte)main_text))
testing.expect(t, os.write_entire_file("/tmp/brolang-test-direct-root/dep/dep.bro", transmute([]byte)dep_text))
testing.expect_value(t, compiler_core.compile_package(app, output), 1)
status := compiler_core.compile_package(app, output, nil, target.DEFAULT, cimport.Options{}, root)
testing.expect_value(t, status, 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 7)
}
@(test) @(test)
c_printf_accepts_a_string_literal :: proc(t: ^testing.T) { c_printf_accepts_a_string_literal :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-printf" output := "/tmp/brolang-test-printf"
@@ -2629,7 +2806,7 @@ milestone_24_rejects_invalid_forms :: proc(t: ^testing.T) {
milestone_25_heap_compiles_and_runs :: proc(t: ^testing.T) { milestone_25_heap_compiles_and_runs :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-heap" output := "/tmp/brolang-test-heap"
defer _ = os.remove(output) defer _ = os.remove(output)
status := compiler_core.compile_package("examples/programs/heap", output) status := compiler_core.compile_package("examples/programs/heap", output, nil, target.DEFAULT, cimport.Options{}, ".")
testing.expect_value(t, status, 0) testing.expect_value(t, status, 0)
state := run_executable(output) state := run_executable(output)
testing.expect_value(t, state.exit_code, 0) testing.expect_value(t, state.exit_code, 0)
@@ -2643,7 +2820,7 @@ milestone_25_heap_emits_libc_alloc_declarations :: proc(t: ^testing.T) {
defer source.destroy_diagnostics(&diagnostics) defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table() symbols := symbol.init_table()
defer symbol.destroy_table(&symbols) defer symbol.destroy_table(&symbols)
ast_module, loaded := loader.load("examples/programs/heap", &sources, &diagnostics, &symbols) ast_module, loaded := loader.load("examples/programs/heap", &sources, &diagnostics, &symbols, context.allocator, context.allocator, cimport.Options{}, target.DEFAULT, ".")
defer ast.destroy_module(&ast_module) defer ast.destroy_module(&ast_module)
testing.expect(t, loaded) testing.expect(t, loaded)
@@ -2818,6 +2995,35 @@ yield_compiles_and_runs :: proc(t: ^testing.T) {
testing.expect_value(t, state.exit_code, 42) testing.expect_value(t, state.exit_code, 42)
} }
@(test)
value_loop_label_does_not_shadow_own_yield_target :: proc(t: ^testing.T) {
text := `main func() i32 {
idx :: for 0..10 |i| hit: {
if (i == 3) yield :hit i
yield none
}
if idx |found| {
if (found == 3) return 0
return 1
}
return 2
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
testing.expect_value(t, len(diagnostics.items), 0)
}
@(test) @(test)
native_union_compiles_and_runs :: proc(t: ^testing.T) { native_union_compiles_and_runs :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-unions" output := "/tmp/brolang-test-unions"
@@ -4306,7 +4512,7 @@ backend_translates_link_arguments_without_reordering_them :: proc(t: ^testing.T)
command := backend.build_command("module.ll", "program", arguments, target.DEFAULT, c_options) command := backend.build_command("module.ll", "program", arguments, target.DEFAULT, c_options)
defer backend.destroy_command(command) defer backend.destroy_command(command)
expected := []string{ expected_prefix := []string{
"/usr/bin/env", "/usr/bin/env",
"zig", "zig",
"cc", "cc",
@@ -4314,6 +4520,24 @@ backend_translates_link_arguments_without_reordering_them :: proc(t: ^testing.T)
"aarch64-macos", "aarch64-macos",
"-Wno-override-module", "-Wno-override-module",
"-Wno-unused-command-line-argument", "-Wno-unused-command-line-argument",
}
testing.expect(t, len(command) >= len(expected_prefix))
for value, index in expected_prefix {
testing.expect_value(t, command[index], value)
}
tail_index := len(expected_prefix)
if sdk, ok := backend.macos_sdk_root(); ok {
defer delete(sdk)
frameworks := fmt.tprintf("-F%s/System/Library/Frameworks", sdk)
lib_dir := fmt.tprintf("-L%s/usr/lib", sdk)
testing.expect(t, len(command) >= tail_index + 2)
testing.expect_value(t, command[tail_index], frameworks)
testing.expect_value(t, command[tail_index + 1], lib_dir)
tail_index += 2
}
expected_tail := []string{
"module.ll", "module.ll",
"-Ivendor/include", "-Ivendor/include",
"-DFEATURE=1", "-DFEATURE=1",
@@ -4324,9 +4548,9 @@ backend_translates_link_arguments_without_reordering_them :: proc(t: ^testing.T)
"-o", "-o",
"program", "program",
} }
testing.expect_value(t, len(command), len(expected)) testing.expect_value(t, len(command), tail_index + len(expected_tail))
for value, index in expected { for value, index in expected_tail {
testing.expect_value(t, command[index], value) testing.expect_value(t, command[tail_index + index], value)
} }
} }
+10
View File
@@ -0,0 +1,10 @@
# Build configuration surface for `brolang build` (v0).
BuildConfig :: struct {
name []u8
source []u8
libraries [][]u8
lib_paths [][]u8
includes [][]u8
defines [][]u8
links [][]u8
}
+10
View File
@@ -0,0 +1,10 @@
# Build configuration surface for `brolang build` (v0).
BuildConfig :: struct {
name []u8
source []u8
libraries [][]u8
lib_paths [][]u8
includes [][]u8
defines [][]u8
links [][]u8
}
+1 -1
View File
@@ -1,5 +1,5 @@
# Illustrative build.bro for a raylib program (not run in CI: needs raylib # Illustrative build.bro for a raylib program (not run in CI: needs raylib
# installed and the game sources under ./game). # installed).
# #
# Generate the raylib bindings once with: # Generate the raylib bindings once with:
# brolang translate-c /opt/homebrew/Cellar/raylib/*/include/raylib.h > game/raylib.bro # brolang translate-c /opt/homebrew/Cellar/raylib/*/include/raylib.h > game/raylib.bro
+20 -20
View File
@@ -42,7 +42,7 @@ Ball :: struct {
# A frame's worth of player intent, as a tagged union. Each arm carries exactly # A frame's worth of player intent, as a tagged union. Each arm carries exactly
# the data that action needs (or `void` when it needs none). # the data that action needs (or `void` when it needs none).
Command :: union(enum) { Command :: union(enum) {
spawn Vector2 # spawn a shape at this point spawn rl.Vector2 # spawn a shape at this point
push struct { dx f32, dy f32 } # blow every shape this way push struct { dx f32, dy f32 } # blow every shape this way
clear void clear void
idle void idle void
@@ -55,11 +55,11 @@ SpawnError :: union(enum) {
} }
# value-match used as an expression source: each arm yields a Color. # value-match used as an expression source: each arm yields a Color.
color_for func(k Kind) Color { color_for func(k Kind) rl.Color {
col :: match k { col :: match k {
.circle: Color{ r = 235, g = 90, b = 90, a = 255 } .circle: rl.Color{ r = 235, g = 90, b = 90, a = 255 }
.square: Color{ r = 90, g = 205, b = 130, a = 255 } .square: rl.Color{ r = 90, g = 205, b = 130, a = 255 }
.triangle: Color{ r = 105, g = 160, b = 245, a = 255 } .triangle: rl.Color{ r = 105, g = 160, b = 245, a = 255 }
} }
return col return col
} }
@@ -76,19 +76,19 @@ next_kind func(k Kind) Kind {
# Read this frame's input into a single Command (contextual union construction: # Read this frame's input into a single Command (contextual union construction:
# `.clear`, `.spawn{...}`, `.push{...}` are built against the return type). # `.clear`, `.spawn{...}`, `.push{...}` are built against the return type).
read_command func() Command { read_command func() Command {
if rl.IsMouseButtonPressed(MOUSE_BUTTON_LEFT) return .spawn{ GetMousePosition() } if rl.IsMouseButtonPressed(rl.MOUSE_BUTTON_LEFT) return .spawn{ rl.GetMousePosition() }
if rl.IsKeyPressed(KEY_SPACE) return .clear if rl.IsKeyPressed(rl.KEY_SPACE) return .clear
fx f32 = 0.0 fx f32 = 0.0
fy f32 = 0.0 fy f32 = 0.0
if rl.IsKeyDown(KEY_A) fx -= FORCE if rl.IsKeyDown(rl.KEY_A) fx -= FORCE
if rl.IsKeyDown(KEY_D) fx += FORCE if rl.IsKeyDown(rl.KEY_D) fx += FORCE
if rl.IsKeyDown(KEY_W) fy -= FORCE if rl.IsKeyDown(rl.KEY_W) fy -= FORCE
if rl.IsKeyDown(KEY_S) fy += FORCE if rl.IsKeyDown(rl.KEY_S) fy += FORCE
if rl.IsKeyDown(KEY_LEFT) fx -= FORCE if rl.IsKeyDown(rl.KEY_LEFT) fx -= FORCE
if rl.IsKeyDown(KEY_RIGHT) fx += FORCE if rl.IsKeyDown(rl.KEY_RIGHT) fx += FORCE
if rl.IsKeyDown(KEY_UP) fy -= FORCE if rl.IsKeyDown(rl.KEY_UP) fy -= FORCE
if rl.IsKeyDown(KEY_DOWN) fy += FORCE if rl.IsKeyDown(rl.KEY_DOWN) fy += FORCE
moved :: fx != 0.0 or fy != 0.0 moved :: fx != 0.0 or fy != 0.0
if (moved) return .push{ dx = fx, dy = fy } if (moved) return .push{ dx = fx, dy = fy }
@@ -130,7 +130,7 @@ step func(b @mut Ball) void {
draw_ball func(b @Ball, highlight bool) void { draw_ball func(b @Ball, highlight bool) void {
col :: color_for(b.kind) col :: color_for(b.kind)
center Vector2 = Vector2{ x = b.x, y = b.y } center rl.Vector2 = rl.Vector2{ x = b.x, y = b.y }
match b.kind { match b.kind {
.circle: rl.DrawCircleV(center, b.radius, col) .circle: rl.DrawCircleV(center, b.radius, col)
.square: rl.DrawPoly(center, 4, b.radius, 45.0, col) .square: rl.DrawPoly(center, 4, b.radius, 45.0, col)
@@ -143,7 +143,7 @@ draw_ball func(b @Ball, highlight bool) void {
} }
main func() i32 { main func() i32 {
rl.SetConfigFlags(FLAG_MSAA_4X_HINT) rl.SetConfigFlags(rl.FLAG_MSAA_4X_HINT)
rl.InitWindow(W, H, "brolang — bouncing shapes") rl.InitWindow(W, H, "brolang — bouncing shapes")
defer rl.CloseWindow() # runs on every exit path out of main defer rl.CloseWindow() # runs on every exit path out of main
rl.SetTargetFPS(60) rl.SetTargetFPS(60)
@@ -210,9 +210,9 @@ main func() i32 {
# --- which shape is under the cursor? (optional via a value-loop) --- # --- which shape is under the cursor? (optional via a value-loop) ---
mouse :: rl.GetMousePosition() mouse :: rl.GetMousePosition()
sel :: for 0..(count) |i| blk: { sel :: for 0..(count) |i| hover: {
c Vector2 = Vector2{ x = balls[i].x, y = balls[i].y } c rl.Vector2 = rl.Vector2{ x = balls[i].x, y = balls[i].y }
if rl.CheckCollisionPointCircle(mouse, c, balls[i].radius) yield :blk i if rl.CheckCollisionPointCircle(mouse, c, balls[i].radius) yield :hover i
yield none yield none
} }
+10
View File
@@ -0,0 +1,10 @@
# Build configuration surface for `brolang build` (v0).
BuildConfig :: struct {
name []u8
source []u8
libraries [][]u8
lib_paths [][]u8
includes [][]u8
defines [][]u8
links [][]u8
}
-7
View File
@@ -1,7 +1,5 @@
heap :: import "@std/mem/heap" heap :: import "@std/mem/heap"
#printf c_func(fmt *c_char, ...) c_int
main func() i32 { main func() i32 {
memory ?*mut u8 = heap.alloc(4) memory ?*mut u8 = heap.alloc(4)
defer heap.free(memory) defer heap.free(memory)
@@ -11,11 +9,6 @@ main func() i32 {
bytes[1] = 20 bytes[1] = 20
bytes[2] = bytes[0] + bytes[1] bytes[2] = bytes[0] + bytes[1]
_ = printf("bytes[0]: %d\n", bytes[0])
_ = printf("bytes[1]: %d\n", bytes[1])
_ = printf("bytes[2]: %d\n", bytes[2])
_ = printf("bytes[3]: %d\n", bytes[3])
if (bytes[2] != 30) { if (bytes[2] != 30) {
return 2 return 2
} }
+265 -4
View File
@@ -14,6 +14,7 @@ import "core:strings"
Cli_Options :: struct { Cli_Options :: struct {
input_path: string, input_path: string,
output_path: string, output_path: string,
project_root: string,
link_arguments: []linker.Argument, link_arguments: []linker.Argument,
c_options: cimport.Options, c_options: cimport.Options,
target: target.Target, target: target.Target,
@@ -71,6 +72,11 @@ parse_cli_args :: proc(args: []string, allocator := context.allocator) -> (Cli_O
return {}, false return {}, false
} }
options.target = selected options.target = selected
case "--root":
if len(options.project_root) > 0 {
return {}, false
}
options.project_root = value
case: case:
return {}, false return {}, false
} }
@@ -87,13 +93,19 @@ parse_cli_args :: proc(args: []string, allocator := context.allocator) -> (Cli_O
print_usage :: proc() { print_usage :: proc() {
fmt.eprintln( fmt.eprintln(
"usage: brolang <package-directory> -o <executable> [--target aarch64-macos] [--c-link <path> | --c-library-path <dir> | --c-library <name> | --c-include-path <dir> | --c-define <name[=value]>]...", "usage: brolang <package-directory> -o <executable> [--root <project-root>] [--target aarch64-macos] [--c-link <path> | --c-library-path <dir> | --c-library <name> | --c-include-path <dir> | --c-define <name[=value]>]...",
) )
fmt.eprintln( fmt.eprintln(
" brolang translate-c|--translate-c <header.h> [--target aarch64-macos] [--c-include-path <dir> | --c-define <name[=value]>]...", " brolang translate-c|--translate-c <header.h> [--target aarch64-macos] [--c-include-path <dir> | --c-define <name[=value]>]...",
) )
fmt.eprintln( fmt.eprintln(
" brolang build [root] (reads root/build.bro; root defaults to the current directory)", " brolang build [root] (reads root/build.bro; writes root/build/name)",
)
fmt.eprintln(
" brolang new <project-path>",
)
fmt.eprintln(
" brolang init",
) )
} }
@@ -105,6 +117,230 @@ is_build_command :: proc(arg: string) -> bool {
return arg == "build" return arg == "build"
} }
is_new_command :: proc(arg: string) -> bool {
return arg == "new"
}
is_init_command :: proc(arg: string) -> bool {
return arg == "init"
}
template_root_valid :: proc(root: string) -> bool {
std_build, std_error := filepath.join({root, "std", "build", "build.bro"})
if std_error != nil {
return false
}
defer delete(std_build)
ffi_stdio, ffi_error := filepath.join({root, "ffi", "c", "stdio.bro"})
if ffi_error != nil {
return false
}
defer delete(ffi_stdio)
return os.exists(std_build) && os.exists(ffi_stdio)
}
find_template_root :: proc(allocator := context.allocator) -> (string, bool) {
if exe, exe_error := os2.get_executable_path(allocator); exe_error == nil {
defer delete(exe, allocator)
exe_dir := filepath.dir(exe, allocator)
defer delete(exe_dir, allocator)
if template_root_valid(exe_dir) {
return strings.clone(exe_dir, allocator), true
}
parent := filepath.dir(exe_dir, allocator)
defer delete(parent, allocator)
if template_root_valid(parent) {
return strings.clone(parent, allocator), true
}
}
cwd := os.get_current_directory(allocator)
defer delete(cwd, allocator)
if template_root_valid(cwd) {
return strings.clone(cwd, allocator), true
}
return "", false
}
ensure_directory :: proc(path: string) -> bool {
if os.exists(path) {
if os.is_dir(path) {
return true
}
fmt.eprintfln("path exists and is not a directory: %s", path)
return false
}
if err := os2.make_directory_all(path); err != nil {
fmt.eprintfln("failed to create directory '%s': %v", path, err)
return false
}
return true
}
ensure_child_directory :: proc(root, name: string) -> bool {
path, err := filepath.join({root, name})
if err != nil {
return false
}
defer delete(path)
return ensure_directory(path)
}
write_file_if_missing :: proc(path, text: string) -> bool {
if os.exists(path) {
if os.is_dir(path) {
fmt.eprintfln("path exists and is not a file: %s", path)
return false
}
return true
}
if !os.write_entire_file(path, transmute([]byte)text) {
fmt.eprintfln("failed to write file '%s'", path)
return false
}
return true
}
write_escaped_brolang_string :: proc(builder: ^strings.Builder, value: string) {
for b in transmute([]byte)value {
if b == '\\' || b == '"' {
strings.write_byte(builder, '\\')
}
strings.write_byte(builder, b)
}
}
default_build_bro :: proc(project_name: string, allocator := context.allocator) -> string {
name := project_name
if len(name) == 0 || name == "." || name == "/" {
name = "app"
}
builder := strings.builder_make()
defer strings.builder_destroy(&builder)
strings.write_string(&builder, "b :: import \"@std/build\"\n\nconfig :: b.BuildConfig{\n\tname = \"")
write_escaped_brolang_string(&builder, name)
strings.write_string(&builder, "\",\n\tsource = \"source\",\n\tlibraries = &[],\n\tlib_paths = &[],\n\tincludes = &[],\n\tdefines = &[],\n\tlinks = &[],\n}\n")
return strings.clone(strings.to_string(builder), allocator)
}
ensure_default_sources :: proc(root: string) -> bool {
source_dir, source_error := filepath.join({root, "source"})
if source_error != nil {
return false
}
defer delete(source_dir)
if !ensure_directory(source_dir) {
return false
}
main_path, main_error := filepath.join({source_dir, "main.bro"})
if main_error != nil {
return false
}
defer delete(main_path)
return write_file_if_missing(main_path, "main func() i32 {\n\treturn 0\n}\n")
}
copy_tree_if_missing :: proc(root, template_root, name: string) -> bool {
dst, dst_error := filepath.join({root, name})
if dst_error != nil {
return false
}
defer delete(dst)
if os.exists(dst) {
if os.is_dir(dst) {
return true
}
fmt.eprintfln("path exists and is not a directory: %s", dst)
return false
}
src, src_error := filepath.join({template_root, name})
if src_error != nil {
return false
}
defer delete(src)
if err := os2.copy_directory_all(dst, src); err != nil {
fmt.eprintfln("failed to copy '%s' to '%s': %v", src, dst, err)
return false
}
return true
}
init_project :: proc(root: string) -> bool {
if !ensure_directory(root) {
return false
}
if !ensure_default_sources(root) || !ensure_child_directory(root, "vendor") {
return false
}
needs_template := true
std_path, std_error := filepath.join({root, "std"})
ffi_path, ffi_error := filepath.join({root, "ffi"})
if std_error == nil && ffi_error == nil {
needs_template = !os.exists(std_path) || !os.exists(ffi_path)
}
if std_error == nil {
delete(std_path)
}
if ffi_error == nil {
delete(ffi_path)
}
template_root := ""
if needs_template {
ok: bool
template_root, ok = find_template_root()
if !ok {
fmt.eprintln("could not find bundled std/ffi sources")
return false
}
defer delete(template_root)
if !copy_tree_if_missing(root, template_root, "std") ||
!copy_tree_if_missing(root, template_root, "ffi") {
return false
}
}
build_path, build_error := filepath.join({root, "build.bro"})
if build_error != nil {
return false
}
defer delete(build_path)
project_name := filepath.base(root)
build_text := default_build_bro(project_name)
defer delete(build_text)
return write_file_if_missing(build_path, build_text)
}
new_project :: proc(path: string) -> bool {
if os.exists(path) {
fmt.eprintfln("project path already exists: %s", path)
return false
}
template_root, ok := find_template_root()
if !ok {
fmt.eprintln("could not find bundled std/ffi sources")
return false
}
defer delete(template_root)
if !ensure_directory(path) {
return false
}
if !init_project(path) {
return false
}
return true
}
run_init_project :: proc() -> int {
cwd := os.get_current_directory()
defer delete(cwd)
return 0 if init_project(cwd) else 2
}
run_new_project :: proc(path: string) -> int {
return 0 if new_project(path) else 2
}
zig_lib_dir_from_env_output :: proc(text: string, allocator := context.allocator) -> (string, bool) { zig_lib_dir_from_env_output :: proc(text: string, allocator := context.allocator) -> (string, bool) {
rest := text rest := text
prefix := ".lib_dir = \"" prefix := ".lib_dir = \""
@@ -241,9 +477,27 @@ run_translate_c :: proc(args: []string) -> int {
main :: proc() { main :: proc() {
if len(os2.args) >= 2 && is_build_command(os2.args[1]) { if len(os2.args) >= 2 && is_build_command(os2.args[1]) {
root := os2.args[2] if len(os2.args) >= 3 else "." if len(os2.args) > 3 {
print_usage()
os2.exit(2)
}
root := os2.args[2] if len(os2.args) == 3 else ""
os2.exit(compiler.run_build(root)) os2.exit(compiler.run_build(root))
} }
if len(os2.args) >= 2 && is_new_command(os2.args[1]) {
if len(os2.args) != 3 {
print_usage()
os2.exit(2)
}
os2.exit(run_new_project(os2.args[2]))
}
if len(os2.args) >= 2 && is_init_command(os2.args[1]) {
if len(os2.args) != 2 {
print_usage()
os2.exit(2)
}
os2.exit(run_init_project())
}
if len(os2.args) >= 2 && is_translate_c_command(os2.args[1]) { if len(os2.args) >= 2 && is_translate_c_command(os2.args[1]) {
os2.exit(run_translate_c(os2.args)) os2.exit(run_translate_c(os2.args))
} }
@@ -255,7 +509,14 @@ main :: proc() {
defer delete(options.link_arguments) defer delete(options.link_arguments)
defer delete(options.c_options.include_paths) defer delete(options.c_options.include_paths)
defer delete(options.c_options.defines) defer delete(options.c_options.defines)
status := compiler.compile_package(options.input_path, options.output_path, options.link_arguments, options.target, options.c_options) status := compiler.compile_package(
options.input_path,
options.output_path,
options.link_arguments,
options.target,
options.c_options,
options.project_root,
)
if status != 0 { if status != 0 {
os2.exit(status) os2.exit(status)
} }
+2 -2
View File
@@ -2,12 +2,12 @@
# #
# A project's `build.bro` imports this module and declares a top-level constant # A project's `build.bro` imports this module and declares a top-level constant
# named `config` of type `BuildConfig`. `brolang build [root]` type-checks # named `config` of type `BuildConfig`. `brolang build [root]` type-checks
# build.bro, reads the config, and compiles the program package it names. # build.bro, reads the config, and writes root/build/name.
# #
# Declarative and literal-only: one executable per build. List fields take an # Declarative and literal-only: one executable per build. List fields take an
# address-of an array literal (`&["raylib"]`); empty lists are written `&[]`. # address-of an array literal (`&["raylib"]`); empty lists are written `&[]`.
BuildConfig :: struct { BuildConfig :: struct {
name []u8 # output executable name name []u8 # output executable name under root/build
source []u8 # program package directory, relative to build.bro source []u8 # program package directory, relative to build.bro
libraries [][]u8 # library names to link (-l) libraries [][]u8 # library names to link (-l)
lib_paths [][]u8 # library search directories (-L) lib_paths [][]u8 # library search directories (-L)