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.
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
directory) reads a `config` constant from `root/build.bro` and compiles the
program package it names:
in Brolang itself. `brolang new <project>` creates a project with local `std`
and `ffi` copies; `brolang init` does the same for the current directory without
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
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
the matching C options (`libraries``-l`, `lib_paths``-L`, `includes`
`-I`, `defines` → C defines, `links` linker inputs) and, like those flags,
their paths are relative to the invocation directory. Lists take the address of
an array literal; empty lists are written `&[]`. See `examples/build/` for
runnable projects.
`name` is a plain executable name, and `source` is the program package relative
to `build.bro`. The list fields map to the matching C options (`libraries`
`-l`, `lib_paths``-L`, `includes``-I`, `defines` → C defines, `links`
linker inputs) and, like those flags, their paths are relative to the invocation
directory. Lists take the address of an array literal; empty lists are written
`&[]`. See `examples/build/` for runnable projects.
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
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
import "../math"
other_math :: import "../math"
heap :: import "@std/mem/heap"
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
26. import from project "root" (implemented)
- imports beginning with `@` resolve from the compiler process cwd / project root
- `heap :: import "@std/mem/heap"` works from any package depth without `../../../` path math
- imports beginning with `@` resolve from the project root
- `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)
- `$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
program package it names. The config is read from the checked HIR — build.bro
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
anonymous global whose address decays to a slice, so list fields like
`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))
}
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(
llvm_path, output_path: string,
link_arguments: []linker.Argument,
@@ -29,6 +55,7 @@ build_command :: proc(
append_owned(&command, target.name(selected), allocator)
append_owned(&command, "-Wno-override-module", allocator)
append_owned(&command, "-Wno-unused-command-line-argument", allocator)
append_macos_sdk_paths(&command, selected, allocator)
append_owned(&command, llvm_path, allocator)
for path in c_options.include_paths {
append(&command, fmt.aprintf("-I%s", path, allocator=allocator))
+93 -4
View File
@@ -11,6 +11,8 @@ 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"
@@ -46,6 +48,21 @@ destroy_build_config :: proc(cfg: ^BuildConfig) {
// 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)
@@ -61,10 +78,10 @@ run_build :: proc(root: string) -> int {
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)
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:", root)
fmt.eprintln("failed to load build root:", project_root)
return 2
}
// 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)
program := filepath.join({root, cfg.source_dir})
program := filepath.join({project_root, cfg.source_dir})
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
+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) {
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(
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,
selected := target.DEFAULT,
c_options := cimport.Options{},
project_root := "",
) -> int {
sources := source.init_store()
defer source.destroy_store(&sources)
@@ -75,6 +76,7 @@ compile_package :: proc(
vmem.arena_allocator(&parser_arena),
c_options,
selected,
project_root if len(project_root) > 0 else input_path,
)
if !loaded {
source.print_all(&diagnostics)
+4 -2
View File
@@ -1416,11 +1416,13 @@ load :: proc(
allocator := context.allocator,
c_options := cimport.Options{},
selected := target.DEFAULT,
project_root_path := "",
) -> (ast.Module, bool) {
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 {
project_root = strings.clone(".", allocator)
project_root = strings.clone(project_root_source, allocator)
}
state := State{
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",
"--c-define",
"FEATURE=1",
"--root",
".",
"--target",
"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_value(t, options.input_path, "app")
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, options.link_arguments[0].kind, linker.Kind.Input)
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"})
_, incomplete_valid := parse_cli_args([]string{"brolang", "app", "-o"})
_, 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"})
_, 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"})
@@ -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, !incomplete_valid)
testing.expect(t, !duplicate_output_valid)
testing.expect(t, !duplicate_root_valid)
testing.expect(t, !duplicate_empty_output_valid)
testing.expect(t, !invalid_target)
testing.expect(t, !legacy_link)
@@ -2560,22 +2565,194 @@ build_command_is_recognized :: proc(t: ^testing.T) {
@(test)
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")
testing.expect_value(t, status, 0)
state := run_executable("./hello")
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
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")
testing.expect_value(t, status, 0)
state := run_executable("./manual_build")
state := run_executable(output)
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)
c_printf_accepts_a_string_literal :: proc(t: ^testing.T) {
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) {
output := "/tmp/brolang-test-heap"
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)
state := run_executable(output)
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)
symbols := symbol.init_table()
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)
testing.expect(t, loaded)
@@ -2818,6 +2995,35 @@ yield_compiles_and_runs :: proc(t: ^testing.T) {
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)
native_union_compiles_and_runs :: proc(t: ^testing.T) {
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)
defer backend.destroy_command(command)
expected := []string{
expected_prefix := []string{
"/usr/bin/env",
"zig",
"cc",
@@ -4314,6 +4520,24 @@ backend_translates_link_arguments_without_reordering_them :: proc(t: ^testing.T)
"aarch64-macos",
"-Wno-override-module",
"-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",
"-Ivendor/include",
"-DFEATURE=1",
@@ -4324,9 +4548,9 @@ backend_translates_link_arguments_without_reordering_them :: proc(t: ^testing.T)
"-o",
"program",
}
testing.expect_value(t, len(command), len(expected))
for value, index in expected {
testing.expect_value(t, command[index], value)
testing.expect_value(t, len(command), tail_index + len(expected_tail))
for value, index in expected_tail {
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
# installed and the game sources under ./game).
# installed).
#
# Generate the raylib bindings once with:
# 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
# the data that action needs (or `void` when it needs none).
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
clear void
idle void
@@ -55,11 +55,11 @@ SpawnError :: union(enum) {
}
# 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 {
.circle: Color{ r = 235, g = 90, b = 90, a = 255 }
.square: Color{ r = 90, g = 205, b = 130, a = 255 }
.triangle: Color{ r = 105, g = 160, b = 245, a = 255 }
.circle: rl.Color{ r = 235, g = 90, b = 90, a = 255 }
.square: rl.Color{ r = 90, g = 205, b = 130, a = 255 }
.triangle: rl.Color{ r = 105, g = 160, b = 245, a = 255 }
}
return col
}
@@ -76,19 +76,19 @@ next_kind func(k Kind) Kind {
# Read this frame's input into a single Command (contextual union construction:
# `.clear`, `.spawn{...}`, `.push{...}` are built against the return type).
read_command func() Command {
if rl.IsMouseButtonPressed(MOUSE_BUTTON_LEFT) return .spawn{ GetMousePosition() }
if rl.IsKeyPressed(KEY_SPACE) return .clear
if rl.IsMouseButtonPressed(rl.MOUSE_BUTTON_LEFT) return .spawn{ rl.GetMousePosition() }
if rl.IsKeyPressed(rl.KEY_SPACE) return .clear
fx f32 = 0.0
fy f32 = 0.0
if rl.IsKeyDown(KEY_A) fx -= FORCE
if rl.IsKeyDown(KEY_D) fx += FORCE
if rl.IsKeyDown(KEY_W) fy -= FORCE
if rl.IsKeyDown(KEY_S) fy += FORCE
if rl.IsKeyDown(KEY_LEFT) fx -= FORCE
if rl.IsKeyDown(KEY_RIGHT) fx += FORCE
if rl.IsKeyDown(KEY_UP) fy -= FORCE
if rl.IsKeyDown(KEY_DOWN) fy += FORCE
if rl.IsKeyDown(rl.KEY_A) fx -= FORCE
if rl.IsKeyDown(rl.KEY_D) fx += FORCE
if rl.IsKeyDown(rl.KEY_W) fy -= FORCE
if rl.IsKeyDown(rl.KEY_S) fy += FORCE
if rl.IsKeyDown(rl.KEY_LEFT) fx -= FORCE
if rl.IsKeyDown(rl.KEY_RIGHT) fx += FORCE
if rl.IsKeyDown(rl.KEY_UP) fy -= FORCE
if rl.IsKeyDown(rl.KEY_DOWN) fy += FORCE
moved :: fx != 0.0 or fy != 0.0
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 {
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 {
.circle: rl.DrawCircleV(center, b.radius, 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 {
rl.SetConfigFlags(FLAG_MSAA_4X_HINT)
rl.SetConfigFlags(rl.FLAG_MSAA_4X_HINT)
rl.InitWindow(W, H, "brolang — bouncing shapes")
defer rl.CloseWindow() # runs on every exit path out of main
rl.SetTargetFPS(60)
@@ -210,9 +210,9 @@ main func() i32 {
# --- which shape is under the cursor? (optional via a value-loop) ---
mouse :: rl.GetMousePosition()
sel :: for 0..(count) |i| blk: {
c Vector2 = Vector2{ x = balls[i].x, y = balls[i].y }
if rl.CheckCollisionPointCircle(mouse, c, balls[i].radius) yield :blk i
sel :: for 0..(count) |i| hover: {
c rl.Vector2 = rl.Vector2{ x = balls[i].x, y = balls[i].y }
if rl.CheckCollisionPointCircle(mouse, c, balls[i].radius) yield :hover i
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"
#printf c_func(fmt *c_char, ...) c_int
main func() i32 {
memory ?*mut u8 = heap.alloc(4)
defer heap.free(memory)
@@ -11,11 +9,6 @@ main func() i32 {
bytes[1] = 20
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) {
return 2
}
+265 -4
View File
@@ -14,6 +14,7 @@ import "core:strings"
Cli_Options :: struct {
input_path: string,
output_path: string,
project_root: string,
link_arguments: []linker.Argument,
c_options: cimport.Options,
target: target.Target,
@@ -71,6 +72,11 @@ parse_cli_args :: proc(args: []string, allocator := context.allocator) -> (Cli_O
return {}, false
}
options.target = selected
case "--root":
if len(options.project_root) > 0 {
return {}, false
}
options.project_root = value
case:
return {}, false
}
@@ -87,13 +93,19 @@ parse_cli_args :: proc(args: []string, allocator := context.allocator) -> (Cli_O
print_usage :: proc() {
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(
" brolang translate-c|--translate-c <header.h> [--target aarch64-macos] [--c-include-path <dir> | --c-define <name[=value]>]...",
)
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"
}
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) {
rest := text
prefix := ".lib_dir = \""
@@ -241,9 +477,27 @@ run_translate_c :: proc(args: []string) -> int {
main :: proc() {
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))
}
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]) {
os2.exit(run_translate_c(os2.args))
}
@@ -255,7 +509,14 @@ main :: proc() {
defer delete(options.link_arguments)
defer delete(options.c_options.include_paths)
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 {
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
# 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
# address-of an array literal (`&["raylib"]`); empty lists are written `&[]`.
BuildConfig :: struct {
name []u8 # output executable name
name []u8 # output executable name under root/build
source []u8 # program package directory, relative to build.bro
libraries [][]u8 # library names to link (-l)
lib_paths [][]u8 # library search directories (-L)