diff --git a/README.md b/README.md index 781adba..cf44e87 100644 --- a/README.md +++ b/README.md @@ -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 ` 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/`. ```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) ``` diff --git a/TODO.md b/TODO.md index a3220ad..1fedf89 100644 --- a/TODO.md +++ b/TODO.md @@ -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 -o ` defaults the project root to the package dir; `--root ` 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 ` and `brolang init` create the default project layout + with local `std`, `ffi`, `vendor`, `source`, and `build.bro` - enabled `&` (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 `&[]` diff --git a/compiler/backend/backend.odin b/compiler/backend/backend.odin index c1cb66a..344f2ac 100644 --- a/compiler/backend/backend.odin +++ b/compiler/backend/backend.odin @@ -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)) diff --git a/compiler/build.odin b/compiler/build.odin index 86a2079..b1803c0 100644 --- a/compiler/build.odin +++ b/compiler/build.odin @@ -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 diff --git a/compiler/checker/checker.odin b/compiler/checker/checker.odin index 2735ee4..078eab4 100644 --- a/compiler/checker/checker.odin +++ b/compiler/checker/checker.odin @@ -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, ) } diff --git a/compiler/compiler.odin b/compiler/compiler.odin index b9bee4e..73425bb 100644 --- a/compiler/compiler.odin +++ b/compiler/compiler.odin @@ -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) diff --git a/compiler/loader/loader.odin b/compiler/loader/loader.odin index 995d054..e7b67eb 100644 --- a/compiler/loader/loader.odin +++ b/compiler/loader/loader.odin @@ -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, diff --git a/compiler_tests.odin b/compiler_tests.odin index 8a6ca5f..00c3b20 100644 --- a/compiler_tests.odin +++ b/compiler_tests.odin @@ -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) } } diff --git a/examples/build/hello/std/build/build.bro b/examples/build/hello/std/build/build.bro new file mode 100644 index 0000000..e929d30 --- /dev/null +++ b/examples/build/hello/std/build/build.bro @@ -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 +} diff --git a/examples/build/manual/std/build/build.bro b/examples/build/manual/std/build/build.bro new file mode 100644 index 0000000..e929d30 --- /dev/null +++ b/examples/build/manual/std/build/build.bro @@ -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 +} diff --git a/examples/build/raylib/build.bro b/examples/build/raylib/build.bro index e575ee7..89e1b73 100644 --- a/examples/build/raylib/build.bro +++ b/examples/build/raylib/build.bro @@ -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 diff --git a/examples/build/raylib/source/main.bro b/examples/build/raylib/source/main.bro index cec6833..f47ba35 100644 --- a/examples/build/raylib/source/main.bro +++ b/examples/build/raylib/source/main.bro @@ -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 } diff --git a/examples/build/raylib/std/build/build.bro b/examples/build/raylib/std/build/build.bro new file mode 100644 index 0000000..e929d30 --- /dev/null +++ b/examples/build/raylib/std/build/build.bro @@ -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 +} diff --git a/examples/build/raylib/vendor/raylib.bro b/examples/build/raylib/vendor/raylib/raylib.bro similarity index 100% rename from examples/build/raylib/vendor/raylib.bro rename to examples/build/raylib/vendor/raylib/raylib.bro diff --git a/examples/programs/heap/main.bro b/examples/programs/heap/main.bro index eb48014..f3c12e4 100644 --- a/examples/programs/heap/main.bro +++ b/examples/programs/heap/main.bro @@ -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 } diff --git a/main.odin b/main.odin index ba05e67..c3357d8 100644 --- a/main.odin +++ b/main.odin @@ -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 -o [--target aarch64-macos] [--c-link | --c-library-path | --c-library | --c-include-path | --c-define ]...", + "usage: brolang -o [--root ] [--target aarch64-macos] [--c-link | --c-library-path | --c-library | --c-include-path | --c-define ]...", ) fmt.eprintln( " brolang translate-c|--translate-c [--target aarch64-macos] [--c-include-path | --c-define ]...", ) 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 ", + ) + 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) } diff --git a/std/build/build.bro b/std/build/build.bro index 5c39b55..fad3c9e 100644 --- a/std/build/build.bro +++ b/std/build/build.bro @@ -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)