diff --git a/README.md b/README.md index fb46f7a..172c795 100644 --- a/README.md +++ b/README.md @@ -48,9 +48,10 @@ Instead of passing these on the command line, a project can describe its build 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/`. +from exactly one of `root/build.bro` or `root/build.hon` and compiles the +program package it names. Without `root`, it searches the current directory +and parents for the nearest build file. Build outputs are written to +`root/build/`. ```bro b :: import "@std/build" @@ -67,15 +68,16 @@ config :: b.BuildConfig{ ``` `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` → +to the build file. 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. Projects can declare tests directly and run them with `brolang test [root]`. -The command reads the same `build.bro`, writes `build/-test`, and reuses -its C link inputs, libraries, include paths, and defines. +The command reads the same build file—exactly one of `build.bro` or +`build.hon`—writes `build/-test`, and reuses its C link inputs, +libraries, include paths, and defines. ```bro math :: import "../math" diff --git a/compiler/build.odin b/compiler/build.odin index 8977061..8c03152 100644 --- a/compiler/build.odin +++ b/compiler/build.odin @@ -66,10 +66,10 @@ load_build_config :: proc(project_root: string) -> (BuildConfig, bool) { return {}, false } // check needs no `main`: it synthesizes a trap main and emits one benign - // "missing main" diagnostic, which is expected for build.bro. Suppress that - // one but surface any real errors in build.bro (and fail on them). + // "missing main" diagnostic, which is expected for a build config. Suppress + // that one but surface any real errors in the build config (and fail on them). hir_module := checker.check(&ast_module, &diagnostics, &symbols, target.DEFAULT, a) - if build_bro_has_errors(&diagnostics) { + if build_config_has_errors(&diagnostics) { source.print_all(&diagnostics) return {}, false } @@ -82,14 +82,14 @@ project_root_for_command :: proc(root, command: string) -> (string, bool, bool) } project_root, found := find_build_root() if !found { - fmt.eprintfln("brolang %s: could not find build.bro in the current directory or any parent", command) + fmt.eprintfln("brolang %s: could not find build.bro or build.hon in the current directory or any parent", command) return "", false, false } return project_root, true, true } -// run_build implements `brolang build [root]`: it reads build.bro and compiles -// the configured program package. +// run_build implements `brolang build [root]`: it reads the build config and +// compiles the configured program package. run_build :: proc(root: string) -> int { project_root, owned, found := project_root_for_command(root, "build") if !found { @@ -161,7 +161,7 @@ valid_output_name :: proc(name: string) -> bool { 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) + fmt.eprintfln("build config: config 'name' must be a plain executable name, got '%s'", name) return "", false } build_dir, dir_error := filepath.join({project_root, "build"}, allocator) @@ -202,15 +202,17 @@ find_build_root_from :: proc(start: string, allocator := context.allocator) -> ( 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 + for build_file in ([?]string{"build.bro", "build.hon"}) { + build_path, build_error := filepath.join({current, build_file}, 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) @@ -227,10 +229,10 @@ find_build_root_from :: proc(start: string, allocator := context.allocator) -> ( } } -// build_bro_has_errors reports whether checking build.bro produced any diagnostic -// other than the benign "missing or unusable main function" (build.bro has no -// main by design; that one is emitted with an empty span). -build_bro_has_errors :: proc(diagnostics: ^source.Diagnostics) -> bool { +// build_config_has_errors reports whether checking a build config produced any +// diagnostic other than the benign "missing or unusable main function" (build +// configs have no main by design; that one is emitted with an empty span). +build_config_has_errors :: proc(diagnostics: ^source.Diagnostics) -> bool { for item in diagnostics.items { if item.span == (source.Span{}) && item.message == "missing or unusable main function" { continue @@ -255,12 +257,12 @@ extract_build_config :: proc(m: ^hir.Module, symbols: ^symbol.Table) -> (BuildCo } } if !found { - fmt.eprintln("build.bro: missing top-level 'config' constant") + fmt.eprintln("build config: missing top-level 'config' constant") return {}, false } root := unwrap_coercions(m, config_expr) if root == hir.INVALID_EXPR || m.exprs[root].kind != .Struct { - fmt.eprintln("build.bro: 'config' must be a BuildConfig{...} literal") + fmt.eprintln("build config: 'config' must be a BuildConfig{...} literal") return {}, false } args := m.exprs[root].args @@ -322,7 +324,7 @@ extract_build_config :: proc(m: ^hir.Module, symbols: ^symbol.Table) -> (BuildCo cfg.c_options.defines = defines[:] if len(cfg.output_name) == 0 || len(cfg.source_dir) == 0 { - fmt.eprintln("build.bro: config requires non-empty 'name' and 'source'") + fmt.eprintln("build config: config requires non-empty 'name' and 'source'") destroy_build_config(&cfg) return {}, false } diff --git a/compiler/types/types.odin b/compiler/types/types.odin index 5ac7f18..81ac2b3 100644 --- a/compiler/types/types.odin +++ b/compiler/types/types.odin @@ -1677,7 +1677,7 @@ is_opaque_struct :: proc(value: Type, store: ^Store) -> bool { size :: proc(value: Type, store: ^Store, selected := target.DEFAULT) -> u64 { #partial switch kind(value, store) { case .Scalar: - return u64(bits(value, selected)/8) + return u64((bits(value, selected)+7)/8) case .Pointer: return u64(target.pointer_bits(selected)/8) case .Slice: diff --git a/compiler_tests.odin b/compiler_tests.odin index f9aaeae..9684e0f 100644 --- a/compiler_tests.odin +++ b/compiler_tests.odin @@ -1738,6 +1738,16 @@ layout_builtins_compile_and_run :: proc(t: ^testing.T) { y u8 } +Bool_First :: struct { + flag bool + byte u8 +} + +Bool_Last :: struct { + byte u8 + flag bool +} + Opaque :: opaque Color :: enum { red @@ -1755,6 +1765,20 @@ buffer func($T type) [sizeof!(T)]u8 { return data } +make_bool_first func() Bool_First { + value Bool_First = undefined + value.flag = true + value.byte = 41 + return value +} + +make_bool_last func() Bool_Last { + value Bool_Last = undefined + value.byte = 42 + value.flag = true + return value +} + main func() i32 { bytes [_]u8 :: buffer(i32) if (needs_usize(SIZE_GLOBAL) != 4) return 1 @@ -1771,6 +1795,15 @@ main func() i32 { if (alignof!(Point) != 4) return 12 if (sizeof!(UserID) != 4) return 13 if (alignof!(UserID) != 4) return 14 + if (sizeof!(bool) != 1) return 15 + if (sizeof!(Bool_First) != 2) return 16 + if (sizeof!(Bool_Last) != 2) return 17 + first :: make_bool_first() + if (!first.flag) return 18 + if (first.byte != 41) return 19 + last :: make_bool_last() + if (!last.flag) return 20 + if (last.byte != 42) return 21 return 0 } ` @@ -5560,19 +5593,27 @@ init_project_creates_missing_layout_and_preserves_existing_files :: proc(t: ^tes } @(test) -build_root_search_finds_nearest_parent_build_file :: proc(t: ^testing.T) { +build_hon_is_discovered_from_nested_directory_and_builds :: proc(t: ^testing.T) { root := "/tmp/brolang-test-build-search" nested := "/tmp/brolang-test-build-search/source/nested" + build_bro := "/tmp/brolang-test-build-search/build.bro" + build_hon := "/tmp/brolang-test-build-search/build.hon" + output := "/tmp/brolang-test-build-search/build/brolang-test-build-search" _ = os2.remove_all(root) defer _ = os2.remove_all(root) + testing.expect(t, init_project(root)) + testing.expect(t, os.rename(build_bro, build_hon)) 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")) + + status := compiler_core.run_build(found) + testing.expect_value(t, status, 0) + state := run_executable(output) + testing.expect_value(t, state.exit_code, 0) } @(test) @@ -10460,6 +10501,62 @@ cross_package_recursive_specialization_reaches_fixed_point :: proc(t: ^testing.T testing.expect(t, os.exists(output)) } +@(test) +qualified_concrete_call_in_runtime_for_retains_specialization :: proc(t: ^testing.T) { + directory := "/tmp/brolang-test-qualified-concrete-call" + ast_directory := "/tmp/brolang-test-qualified-concrete-call/ast" + main_path := "/tmp/brolang-test-qualified-concrete-call/main.hon" + ast_path := "/tmp/brolang-test-qualified-concrete-call/ast/ast.hon" + output := "/tmp/brolang-test-qualified-concrete-call-output" + ast_text := `Node :: struct { token usize } +Token :: struct { start usize } + +render_literal func(node @Node, tokens []Token, program []u8) ?[]u8 { + if (node.token >= tokens.len) return null + token :: tokens[node.token] + if (token.start >= program.len) return null + return program[token.start..token.start+1] +} +` + main_text := `ast :: import "./ast" + +main func() i32 { + nodes [1]mut ast.Node = undefined + nodes[0].token = 0 + token_storage [1]mut ast.Token = undefined + token_storage[0].start = 0 + mutable_tokens []mut ast.Token = token_storage[..] + program :: "x" + for nodes |node| { + _ = ast.render_literal(&node, mutable_tokens, program) + } + return 0 +} +` + _ = os2.remove_all(directory) + defer _ = os2.remove_all(directory) + defer _ = os.remove(output) + testing.expect(t, os.make_directory(directory) == nil) + testing.expect(t, os.make_directory(ast_directory) == nil) + testing.expect(t, os.write_entire_file(main_path, transmute([]byte)main_text)) + testing.expect(t, os.write_entire_file(ast_path, transmute([]byte)ast_text)) + + 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) + module, loaded := loader.load(directory, &sources, &diagnostics, &symbols) + defer ast.destroy_module(&module) + testing.expect(t, loaded) + testing.expect_value(t, len(diagnostics.items), 0) + + testing.expect_value(t, compiler_core.compile_package(directory, output), 0) + state := run_executable(output) + testing.expect_value(t, state.exit_code, 0) +} + @(test) imported_main_does_not_satisfy_root_main_requirement :: proc(t: ^testing.T) { output := "/tmp/brolang-test-package-missing-root-main" diff --git a/main.odin b/main.odin index e8284fb..7d05669 100644 --- a/main.odin +++ b/main.odin @@ -101,10 +101,10 @@ print_usage :: proc() { " brolang translate-c|--translate-c ... [--output-dir ] [--target aarch64-macos] [--c-include-path | --c-define ]...", ) fmt.eprintln( - " brolang build [root] (reads root/build.bro; writes root/build/name)", + " brolang build [root] (reads exactly one of root/build.bro or root/build.hon; writes root/build/name)", ) fmt.eprintln( - " brolang test [root] (reads root/build.bro; writes and runs root/build/name-test)", + " brolang test [root] (reads exactly one of root/build.bro or root/build.hon; writes and runs root/build/name-test)", ) fmt.eprintln( " brolang new ", diff --git a/std/build/build.hon b/std/build/build.hon index 8e48122..2d6f269 100644 --- a/std/build/build.hon +++ b/std/build/build.hon @@ -1,14 +1,15 @@ # Build configuration surface for `brolang build` (v0). # -# 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 writes root/build/name. +# A project's `build.bro` or `build.hon` imports this module and declares a +# top-level constant named `config` of type `BuildConfig`. `brolang build +# [root]` accepts exactly one of those files, 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"]`) and default to empty. BuildConfig :: struct { name []u8 # output executable name under root/build - source []u8 # program package directory, relative to build.bro + source []u8 # program package directory, relative to the build file libraries [][]u8 = &[] # library names to link (-l) lib_paths [][]u8 = &[] # library search directories (-L) includes [][]u8 = &[] # C include directories (-I) diff --git a/zig-union-layout.o b/zig-union-layout.o new file mode 100644 index 0000000..e69de29