Compare commits

...

10 Commits

Author SHA1 Message Date
hl-valdemar 7ce3917c15 toolchain version command 2026-07-06 19:36:55 +02:00
hl-valdemar e7f69ffb5a add new and init to toolchain 2026-07-06 19:36:55 +02:00
hl-valdemar cdde49e68b build system (first pass) 2026-07-05 00:09:39 +02:00
hl-valdemar 4ebe9c90e9 comptime storage and function values 2026-07-03 23:35:00 +02:00
hl-valdemar ee41a54e41 runtime mutable global state 2026-07-03 22:40:21 +02:00
hl-valdemar adf142736c more comptime eval 2026-07-03 18:18:45 +02:00
hl-valdemar e00a4e929a comptime eval 2026-07-03 18:18:45 +02:00
hl-valdemar b94687c30a comptime type params 2026-07-02 22:24:57 +02:00
hl-valdemar a98b26446d array size inference from value 2026-07-02 20:20:31 +02:00
hl-valdemar 7cda126924 comptime value-params 2026-07-02 20:10:02 +02:00
37 changed files with 7797 additions and 361 deletions
+10 -5
View File
@@ -8,8 +8,8 @@ roadmap and milestone history.
### source, declarations, and packages
- newline-terminated statements and `#` comments
- immutable `::` bindings, mutable function-local `=` bindings, and `_` sinks
- immutable package globals, function-local mutable locals, and mutable local declarations initialized with `undefined`
- immutable `::` bindings, typed mutable `=` locals/globals, and `_` sinks
- immutable package globals, mutable runtime globals, function-local mutable locals, and mutable local declarations initialized with `undefined`
- package-level functions, globals, native type declarations, and `Name :: alias T`
- directory packages with merged declarations
- file-local relative imports, import aliases, and qualified member access
@@ -22,7 +22,7 @@ roadmap and milestone history.
- target-dependent C scalar primitives from `c_char` through `c_longdouble`, kept semantically distinct from native scalars
- contextual integer/float/character literals, backward type-demand inference through names and arithmetic, and compile-time folding for numeric constant expressions
- strict numeric conversion by default, widening where valid, C scalar coercions at C boundaries, and explicit scalar keyword casts such as `i32(x)` / `c_float(x)`
- arrays `[N]T`, sentinel arrays `[N;S]T`, compile-time expression array counts, slices `[]T` / `[;S]T`, single-item pointers `@T`, many-item pointers `*T`, and sentinel many-item pointers `[*;S]T`
- arrays `[N]T`, inferred-count arrays `[_]T`, sentinel arrays `[N;S]T`, compile-time expression array counts, slices `[]T` / `[;S]T`, single-item pointers `@T`, many-item pointers `*T`, and sentinel many-item pointers `[*;S]T`
- pointer mutability via `mut`, optional pointers as nullable pointers, pointer arithmetic for many-item pointers, postfix dereference `^`, and trapping optional unwrap `?`
- pointer-to-array `.len`, indexing, slicing, `.ptr` on slices and pointers-to-arrays, implicit address-taking for array-variable slices, and pointer/slice sentinel weakening
- UTF-8 string literals as immutable pointers to static zero-terminated byte arrays, plus raw backtick multiline strings
@@ -52,8 +52,13 @@ roadmap and milestone history.
### functions, C interop, and linking
- demand-monomorphized Brolang and C-ABI functions
- integer comptime value parameters such as `make_array func($N usize) [N]u8`, specialized by value and omitted from the runtime ABI
- explicit comptime type parameters such as `max func($T type, a, b T) T`, specialized by type and omitted from the runtime ABI
- forced typed comptime expressions such as `$sum(1, 2)`, `$Point { x = 1, y = 2 }`, and comptime value blocks such as `${ yield 4 }`
- comptime execution for bodyful Brolang functions with mutable locals, loops, `defer`, `match`, `try`/`catch`, pointer/slice storage mutation, pointer captures, and calls through comptime-known function values
- bodyful `c_func` definitions and bodyless `c_func` declarations with exact external symbol names
- concrete-only C signatures, C variadic declarations/calls, and C default argument promotions
- native function pointer values and types with `*func(...) R`, fallible `*func(...) R ! E`, optional `?*func(...) R`, and non-variadic native indirect calls
- Apple Silicon C ABI lowering for scalars, pointers, fixed-signature plain records/unions, small aggregates, homogeneous float aggregates, and indirect aggregate returns
- imported C typedefs, scalar constants, enum constants, fixed arrays, complete plain structs/unions, and pointers to opaque records
- imported external C object variables, including mutable variables and immutable object globals
@@ -72,14 +77,14 @@ roadmap and milestone history.
- error-tolerant compilation with diagnostics and runtime traps where recovery is possible
- lazy semantic checking of demanded function specializations
- static, eager runtime, and deferred problematic globals with cycle diagnostics
- static, eager runtime, mutable runtime, and deferred problematic globals with cycle diagnostics
- demand-driven LLVM declarations for referenced foreign functions
- replaceable dynamically loaded libclang C-import backend
- C-header import caching by canonical path, target, include paths, and defines
## PLANNED / DEFERRED
- comptime polymorphism
- aggregate comptime parameters and stable aggregate specialization keys
- tuples and native Brolang variadic functions
- exporting Brolang functions to C and broader target-specific C ABI lowering
- non-plain C record layouts such as bitfields, packed records, flexible arrays, qualified fields, and C variadic record arguments
+46 -3
View File
@@ -30,6 +30,35 @@ invocation in command-line order:
`-l<name>`. `--c-include-path <dir>` and `--c-define <name[=value]>` configure
C preprocessing.
Instead of passing these on the command line, a project can describe its build
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"
config :: b.BuildConfig{
name = "manual",
source = "src",
libraries = &[],
lib_paths = &[],
includes = &[],
defines = &[],
links = &["examples/build/manual/native.c"],
}
```
`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:
```bro
@@ -74,6 +103,15 @@ call_mapper func(mapper native.Imported_Mapper) c_int {
}
```
Native Brolang function pointer values use `*func(...) R`, with fallible
channels written on the result:
```bro
call func(callback *func(value i32) i32, value i32) i32 {
return callback(value)
}
```
Bodyless manual and imported C functions may be variadic:
```bro
@@ -111,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)
```
@@ -124,7 +163,7 @@ Current prototype features:
- Newline-terminated, multiline statements; `}` may terminate a block's final statement
- `#` comments
- Immutable `::` bindings, mutable function-local `=` bindings, and `_` sinks
- Immutable `::` bindings, typed mutable `=` locals/globals, and `_` sinks
- Exact-width signed/unsigned integers, `f32`, `f64`, `isize`, `usize`, and loose integer-constrained `int`
- Target-dependent atomic `c_*` primitive types, `c_func`, and defined or opaque `c_struct`
- Arrays, sentinel arrays, single-item pointers, many-item pointers, sentinel many-item pointers, slices, sentinel slices, strings, character literals, optionals, and native structs
@@ -136,11 +175,15 @@ Current prototype features:
- Plain imported C structs/unions, fixed arrays, and C function pointer typedefs, including keyed literals, field access, callbacks, and Apple Silicon by-value ABI lowering
- Qualified imported globals and functions with package-aware symbol mangling
- Demand-monomorphized Brolang and C-ABI functions
- Integer and type comptime parameters (`func($N usize) [N]u8`, `func($T type, value T) T`) specialized by comptime argument
- Forced typed comptime expressions (`$sum(1, 2)`, `$Point { x = 1, y = 2 }`) and comptime value blocks (`${ yield 4 }`)
- Comptime execution for bodyful Brolang functions with mutable locals, loops, `defer`, `match`, `try`/`catch`, pointer/slice storage mutation, pointer captures, and calls through comptime-known function values
- Native function pointer values and types (`*func(...) R`, `*func(...) R ! E`, `?*func(...) R`)
- Bodyless concrete C function declarations with exact external symbol names
- Bodyless manual and imported C variadic declarations with default argument promotions
- Ordered linking of additional C sources, objects, archives, and libraries
- Checked signed addition and unary negation
- Static, eager runtime, and deferred problematic globals
- Static, eager runtime, mutable runtime, and deferred problematic globals
- Runtime diagnostics followed by `llvm.trap`
See [LANGUAGE.md](LANGUAGE.md) for the concise implemented and planned language
+108 -8
View File
@@ -91,7 +91,7 @@
- operators: `and`, `or`, `!`
- lazy evaluation / short-circuit evaluation
- if statements (implemented). example: `if condition { ... } else if { ... } else { ... }`
- conditions must be `bool`; block-scoped locals with shadowing across blocks
- conditions must be `bool`; block-scoped locals do not escape their blocks
- lowered through new `Label` / `Br` / `Cond_Br` IR opcodes (alloca-backed locals, no phi nodes)
- conditional unwrapping for optionals (`?T`) (implemented): `if val |v| { ... } else { ... }` - unwrap `val` into `v` if it is not `none`
- single immutable binding scoped to the then-block; `v` not visible in `else` or after the `if`
@@ -403,8 +403,7 @@
statements first (a throwaway probe), so a first concrete `yield :blk` that references a
block local still resolves the result to `?T`
- deferred (`// ponytail:`): the same `none`-before-concrete typing in an untyped block (or
loop) whose concrete yield references a local declared *past* the first yield (annotate);
same-label loop/block shadowing resolves innermost-wins
loop) whose concrete yield references a local declared *past* the first yield (annotate)
21. unions and tagged unions (implemented; first pass — native untagged unions only; see below)
- inspired by zig
@@ -627,14 +626,103 @@
- 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 polymorphism (zig inspired)
27. comptime integer value parameters (implemented; v1)
- `$N` marks an integer comptime parameter in a normal `func` signature:
`make_array func($N usize) [N]u8`
- callers pass a compile-time integer expression; the value specializes the function
and is omitted from the runtime ABI
- inside the specialization, `N` is visible as an immutable compile-time integer in
array counts, types, and body expressions
- v1 intentionally supports integer values only; no comptime branch pruning or
user-function execution
28. comptime execution
27.5 comptime type parameters (implemented; v1)
- `$T type` marks an explicit comptime type parameter in a normal `func` signature:
`max func($T type, a, b T) T`
- callers pass the type explicitly as an ordinary comptime argument (`max(i32, a, b)`);
the type argument specializes the function and is omitted from the runtime ABI
- inside the specialization, `T` is visible in parameter, result, local, array, pointer,
slice, and fallible type syntax
- v1 intentionally keeps `type` contextual to comptime parameter declarations; no
inferred type parameters, first-class type values, or comptime execution
29. brolang build system (requires comptime execution)
27.6 comptime-evaluable constants/functions (implemented)
- `$expr` forces comptime evaluation of an expression:
`x :: $32`, `n :: $sum(1, 2)`, and `res :: ${ ... }`
- constant contexts such as array counts and comptime value arguments implicitly
require comptime evaluation; ordinary immutable bindings remain ordinary bindings
- ordinary `func` calls are comptime-evaluable when reached from a comptime context;
do not add a separate `$sum func(...)` declaration form
- v1 evaluator supported integer literals/arithmetic, boolean conditions, immutable
locals, `return`, `if`/`else`, comptime blocks, and direct calls to other evaluable
brolang functions
- broader typed execution is milestone 27.7
27.7 broader Zig-style comptime execution (implemented; v1)
- `compiler/checker/comptime.odin` owns checker-local evaluator state, typed
comptime values, execution, and HIR materialization; `checker.odin` keeps type
checking, inference, specialization, and build orchestration
- typed `$` values cover bools, integers, floats, strings, arrays, structs, tagged
unions, enums, optionals, and fallibles
- supports mutable comptime locals/assignment, `if`, `while`, `for`,
`break`/`continue`, `defer`, `match`, value blocks/`yield`, direct calls to
bodyful Brolang functions, and `try`/`catch`
- successful `$` results materialize back into ordinary HIR expressions so lowering
and LLVM stay unchanged
- evaluation uses a fixed `100_000` step quota
- immutable locals/globals with comptime-known initializers may feed comptime
evaluation; runtime-dependent values remain invalid in comptime contexts
- runtime-only behavior is rejected in comptime: external/bodyless `c_func`,
writable globals, and materializing comptime storage pointers/slices as runtime memory
- v1 keeps integer-only `$N` specialization keys; aggregate comptime parameters
and stable aggregate serialization are deferred
27.8 source-defined mutable runtime globals (implemented)
- allow mutable global declarations in Brolang source for process-global runtime
state, matching the writable-global support already needed for imported C globals
- require source type syntax and an initializer; constraints (`int`/`float`/`range`)
and inferred array counts may resolve through the existing inference fixpoint, but
the final type must be concrete runtime storage
- emit source-defined mutable globals as writable globals, not constants
- allow assignment, address-taking, field/index mutation, and pointer passing under
the same mutability rules as other writable locations
- keep mutable globals invalid in comptime evaluation; `$global_var` and writes from
comptime execution must remain runtime-dependent errors
- define initialization order and cycle behavior by reusing the existing global
initializer dependency/cycle system where possible
- reject user-visible name shadowing across imports, named types, globals, functions,
params, locals, comptime params, captures, and labels; `_` remains reusable
27.9 comptime storage and function values (implemented; practical v1)
- comptime locals, params, and immutable globals can own evaluator storage cells
addressable through places instead of compiler-owned memory
- comptime supports address/deref, mutable pointer and slice mutation, field/index
places, slicing, `.len`, `.ptr`, pointer captures, and pointer-param aliasing
- comptime storage pointers/slices cannot materialize as runtime memory; escaped
dead storage is rejected
- bare concrete non-comptime function names are values; native function pointer
types use `*func(...) R` and fallible `*func(...) R ! E`
- comptime-known native/bodyful `c_func` values can be called; bodyless/imported
callbacks remain runtime-only
- native function pointers are non-variadic v1; C variadic function pointers stay
under `*c_func(...) R`
28. brolang build system — v0 shipped
- `brolang build [root]` reads a declarative `config` constant from
`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 `&[]`
- deferred: build graph / steps / caching, multiple artifacts, computed paths
(needs string building), struct field defaults to drop `&[]` on empty lists
## A word on multi-unwrap
@@ -1221,6 +1309,18 @@ data :: read_file(path) catch |e| match e {
| `yield :label value` | Provide value from labeled block |
| `return` / `return value` | Exit the current function; in fallible functions, return value dispatches by type |
## A word on comptime
```
make_array func($N usize) [N]u8 { ... } # implemented: integer comptime value params
max func($T type, a, b T) T { ... } # implemented: comptime type params
x :: $32 # implemented: force comptime expression evaluation
n :: $sum(1, 2) # implemented: ordinary functions can run at comptime
res :: ${ yield 4 } # implemented: comptime value block
p :: $Point { x = 1, y = 2 } # implemented: typed aggregate comptime values
total :: $sum_loop(4) # implemented: mutable locals/loops/defer/match/try/catch
```
## A word on memory allocation
(NOTE THAT SYNTAX MAY NOT MATCH BROLANG EXACTLY AND SHOULD BE TAKEN WITH A GRAIN OF SALT - INSPIRATION ONLY)
+3
View File
@@ -72,6 +72,7 @@ Expr_Kind :: enum u8 {
Array,
None,
Undefined,
Type,
Name,
Enum_Literal,
Address,
@@ -84,6 +85,7 @@ Expr_Kind :: enum u8 {
Struct_Literal,
Keyed,
Cast,
Comptime,
Negate,
Not,
Add,
@@ -123,6 +125,7 @@ Param :: struct {
name: symbol.Id,
span: source.Span,
type: Type_Syntax,
comptime_value: bool,
}
Stmt_Kind :: enum u8 {
+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))
+347
View File
@@ -0,0 +1,347 @@
package compiler
import "./checker"
import "./cimport"
import "./hir"
import "./linker"
import "./loader"
import "./source"
import "./symbol"
import "./target"
import "./types"
import "core:fmt"
import vmem "core:mem/virtual"
import "core:os"
import "core:os/os2"
import "core:path/filepath"
import "core:strings"
// BuildConfig is the native, extracted form of std/build's BuildConfig: all
// strings are cloned into context.allocator so they outlive the build module's
// arena (freed at the end of run_build). Free with destroy_build_config.
BuildConfig :: struct {
output_name: string,
source_dir: string,
link_arguments: []linker.Argument,
c_options: cimport.Options,
}
destroy_build_config :: proc(cfg: ^BuildConfig) {
delete(cfg.output_name)
delete(cfg.source_dir)
for arg in cfg.link_arguments {
delete(arg.value)
}
delete(cfg.link_arguments)
for path in cfg.c_options.include_paths {
delete(path)
}
delete(cfg.c_options.include_paths)
for define in cfg.c_options.defines {
delete(define)
}
delete(cfg.c_options.defines)
}
// run_build implements `brolang build [root]`: it loads and type-checks
// `root/build.bro`, reads its `config` constant, and compiles the program
// package the config names. build.bro is only checked (never lowered/emitted),
// so the config is read straight from the HIR.
run_build :: proc(root: string) -> int {
project_root := root
owns_project_root := false
if len(project_root) == 0 {
found_root, found := find_build_root()
if !found {
fmt.eprintln("brolang build: could not find build.bro in the current directory or any parent")
return 2
}
project_root = found_root
owns_project_root = true
}
defer if owns_project_root {
delete(project_root)
}
sources := source.init_store()
defer source.destroy_store(&sources)
diagnostics := source.init_store_diagnostics(&sources)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
arena: vmem.Arena
if err := vmem.arena_init_growing(&arena); err != nil {
fmt.eprintln("failed to initialize build arena:", err)
return 2
}
defer vmem.arena_destroy(&arena)
a := vmem.arena_allocator(&arena)
ast_module, loaded := loader.load(project_root, &sources, &diagnostics, &symbols, a, a, cimport.Options{}, target.DEFAULT, project_root)
if !loaded {
source.print_all(&diagnostics)
fmt.eprintln("failed to load build root:", project_root)
return 2
}
// check needs no `main`: it synthesizes a trap main and emits one benign
// "missing main" diagnostic, which is expected for build.bro. Suppress that
// one but surface any real errors in build.bro (and fail on them).
hir_module := checker.check(&ast_module, &diagnostics, &symbols, target.DEFAULT, a)
if build_bro_has_errors(&diagnostics) {
source.print_all(&diagnostics)
return 2
}
cfg, ok := extract_build_config(&hir_module, &symbols)
if !ok {
return 2
}
defer destroy_build_config(&cfg)
program := filepath.join({project_root, cfg.source_dir})
defer delete(program)
output, output_ok := build_output_path(project_root, cfg.output_name)
if !output_ok {
return 2
}
defer delete(output)
return compile_package(program, output, cfg.link_arguments, target.DEFAULT, cfg.c_options, project_root)
}
valid_output_name :: proc(name: string) -> bool {
return len(name) > 0 && name != "." && name != ".." &&
!strings.contains(name, "/") && !strings.contains(name, "\\")
}
build_output_path :: proc(project_root, name: string, allocator := context.allocator) -> (string, bool) {
if !valid_output_name(name) {
fmt.eprintfln("build.bro: config 'name' must be a plain executable name, got '%s'", name)
return "", false
}
build_dir, dir_error := filepath.join({project_root, "build"}, allocator)
if dir_error != nil {
return "", false
}
if os.exists(build_dir) {
if !os.is_dir(build_dir) {
fmt.eprintfln("build path exists and is not a directory: %s", build_dir)
delete(build_dir, allocator)
return "", false
}
} else if err := os2.make_directory_all(build_dir); err != nil {
fmt.eprintfln("failed to create build directory '%s': %v", build_dir, err)
delete(build_dir, allocator)
return "", false
}
output, output_error := filepath.join({build_dir, name}, allocator)
delete(build_dir, allocator)
if output_error != nil {
return "", false
}
return output, true
}
find_build_root :: proc(allocator := context.allocator) -> (string, bool) {
current := os.get_current_directory(allocator)
if len(current) == 0 {
return "", false
}
defer delete(current, allocator)
return find_build_root_from(current, allocator)
}
find_build_root_from :: proc(start: string, allocator := context.allocator) -> (string, bool) {
current, current_ok := filepath.abs(start, allocator)
if !current_ok {
current = strings.clone(start, allocator)
}
for {
build_path, build_error := filepath.join({current, "build.bro"}, allocator)
if build_error != nil {
delete(current, allocator)
return "", false
}
found := os.exists(build_path)
delete(build_path, allocator)
if found {
return current, true
}
if current == "/" {
delete(current, allocator)
return "", false
}
parent := filepath.dir(current, allocator)
if parent == current {
delete(parent, allocator)
delete(current, allocator)
return "", false
}
delete(current, allocator)
current = parent
}
}
// build_bro_has_errors reports whether checking build.bro produced any diagnostic
// other than the benign "missing or unusable main function" (build.bro has no
// main by design; that one is emitted with an empty span).
build_bro_has_errors :: proc(diagnostics: ^source.Diagnostics) -> bool {
for item in diagnostics.items {
if item.span == (source.Span{}) && item.message == "missing or unusable main function" {
continue
}
return true
}
return false
}
// extract_build_config finds the top-level `config` constant and reads its
// BuildConfig{...} fields out of the HIR. All returned strings are cloned into
// context.allocator.
extract_build_config :: proc(m: ^hir.Module, symbols: ^symbol.Table) -> (BuildConfig, bool) {
config_id := symbol.intern(symbols, "config")
config_expr := hir.INVALID_EXPR
found := false
for g in m.globals {
if g.name == config_id {
config_expr = g.expr
found = true
break
}
}
if !found {
fmt.eprintln("build.bro: missing top-level 'config' constant")
return {}, false
}
root := unwrap_coercions(m, config_expr)
if root == hir.INVALID_EXPR || m.exprs[root].kind != .Struct {
fmt.eprintln("build.bro: 'config' must be a BuildConfig{...} literal")
return {}, false
}
args := m.exprs[root].args
fields := types.fields_for(&m.types, m.exprs[root].type)
cfg: BuildConfig
links: [dynamic]linker.Argument
includes: [dynamic]string
defines: [dynamic]string
for field, i in fields {
if i >= len(args) {
break
}
switch symbol.resolve(symbols, symbol.Id(field.name)) {
case "name":
if s, sok := read_string(m, args[i]); sok {
cfg.output_name = strings.clone(s)
}
case "source":
if s, sok := read_string(m, args[i]); sok {
cfg.source_dir = strings.clone(s)
}
case "libraries":
list := read_string_list(m, args[i])
for v in list {
append(&links, linker.Argument{kind = .Library, value = strings.clone(v)})
}
delete(list)
case "lib_paths":
list := read_string_list(m, args[i])
for v in list {
append(&links, linker.Argument{kind = .Library_Path, value = strings.clone(v)})
}
delete(list)
case "links":
list := read_string_list(m, args[i])
for v in list {
append(&links, linker.Argument{kind = .Input, value = strings.clone(v)})
}
delete(list)
case "includes":
list := read_string_list(m, args[i])
for v in list {
append(&includes, strings.clone(v))
}
delete(list)
case "defines":
list := read_string_list(m, args[i])
for v in list {
append(&defines, strings.clone(v))
}
delete(list)
}
}
cfg.link_arguments = links[:]
cfg.c_options.include_paths = includes[:]
cfg.c_options.defines = defines[:]
if len(cfg.output_name) == 0 || len(cfg.source_dir) == 0 {
fmt.eprintln("build.bro: config requires non-empty 'name' and 'source'")
destroy_build_config(&cfg)
return {}, false
}
return cfg, true
}
// unwrap_coercions strips implicit coercion wrappers (each stores its inner
// expr in `.left`) to reach the underlying value expression.
unwrap_coercions :: proc(m: ^hir.Module, id: hir.Expr_Id) -> hir.Expr_Id {
cur := id
for cur != hir.INVALID_EXPR && int(cur) < len(m.exprs) {
#partial switch m.exprs[cur].kind {
case .Retype, .Weaken_Slice, .Weaken_Pointer, .Decay_Array_Pointer, .Slice_Ptr,
.Widen, .Sum_Widen, .Optional_Some, .C_Coerce, .Scalar_Cast:
cur = m.exprs[cur].left
case:
return cur
}
}
return cur
}
read_string :: proc(m: ^hir.Module, id: hir.Expr_Id) -> (string, bool) {
e := unwrap_coercions(m, id)
if e == hir.INVALID_EXPR || m.exprs[e].kind != .String {
return "", false
}
sid := m.exprs[e].integer
if sid < 0 || int(sid) >= len(m.strings) {
return "", false
}
return m.strings[int(sid)], true
}
// read_string_list reads a `&[...]` list field: the value is an address of an
// anonymous global array (see checker `&<array literal>` promotion), whose
// elements are strings. Returned strings alias m.strings; callers clone them.
// The returned slice is owned by the caller (delete it).
read_string_list :: proc(m: ^hir.Module, id: hir.Expr_Id) -> []string {
addr := unwrap_coercions(m, id)
if addr == hir.INVALID_EXPR || m.exprs[addr].kind != .Address {
return nil
}
g := unwrap_coercions(m, m.exprs[addr].left)
if g == hir.INVALID_EXPR || m.exprs[g].kind != .Global {
return nil
}
gid := hir.as_global(m.exprs[g].target)
if gid == hir.INVALID_GLOBAL || int(gid) >= len(m.globals) {
return nil
}
arr := m.globals[gid].expr
if arr == hir.INVALID_EXPR || m.exprs[arr].kind != .Array {
return nil
}
elems := m.exprs[arr].args
out := make([]string, len(elems))
for a, i in elems {
s, ok := read_string(m, a)
if !ok {
delete(out)
return nil
}
out[i] = s
}
return out
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+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)
+3
View File
@@ -201,6 +201,9 @@ lex :: proc(
case '@':
append_token(&stream, source_file, .At, cursor, cursor+1)
cursor += 1
case '$':
append_token(&stream, source_file, .Dollar, cursor, cursor+1)
cursor += 1
case '*':
start := cursor
cursor += 1
+9 -2
View File
@@ -1234,6 +1234,11 @@ declaration_conflicts :: proc(module: ^ast.Module, pkg: ast.Package_Id, name: sy
return true
}
}
type_id := types.find_named(&module.type_store, u32(pkg), u32(name))
type_item, type_ok := types.node(&module.type_store, type_id)
if type_ok && type_item.declared {
return true
}
return false
}
@@ -1411,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,
+54 -7
View File
@@ -113,7 +113,7 @@ is_type_token :: proc(kind: token.Kind) -> bool {
.Keyword_C_Short, .Keyword_C_Ushort, .Keyword_C_Int, .Keyword_C_Uint,
.Keyword_C_Long, .Keyword_C_Ulong, .Keyword_C_Longlong, .Keyword_C_Ulonglong,
.Keyword_C_Float, .Keyword_C_Double, .Keyword_C_Longdouble,
.Keyword_Void, .Keyword_Bool, .Keyword_C_Func, .Identifier, .Question, .At, .Star, .Left_Bracket:
.Keyword_Void, .Keyword_Bool, .Keyword_Func, .Keyword_C_Func, .Identifier, .Question, .At, .Star, .Left_Bracket:
return true
}
return false
@@ -329,10 +329,11 @@ parse_type_atom :: proc(parser: ^Parser) -> ast.Type_Syntax {
case .Keyword_Bool:
advance(parser)
return types.BOOL
case .Keyword_C_Func:
case .Keyword_Func, .Keyword_C_Func:
c_abi := tok.kind == .Keyword_C_Func
advance(parser)
if _, ok := allow(parser, .Left_Paren); !ok {
source.add(parser.diagnostics, current(parser).span, "expected '(' after c_func type")
source.add(parser.diagnostics, current(parser).span, "expected '(' after function type")
return types.INVALID
}
params, variadic := parse_params(parser)
@@ -340,11 +341,19 @@ parse_type_atom :: proc(parser: ^Parser) -> ast.Type_Syntax {
source.add(parser.diagnostics, current(parser).span, "expected ')' after function type parameters")
}
result := parse_type(parser)
if _, ok := allow(parser, .Bang); ok {
error_type := parse_error_type(parser)
if c_abi {
source.add(parser.diagnostics, current(parser).span, "c_func pointer types cannot be fallible")
} else {
result = types.fallible(&parser.module.type_store, result, error_type)
}
}
param_types := make([]types.Type, len(params), parser.module.allocator)
for param, index in params {
param_types[index] = param.type
}
function_type := types.function(&parser.module.type_store, param_types, result, true, variadic)
function_type := types.function(&parser.module.type_store, param_types, result, c_abi, variadic)
delete(param_types, parser.module.allocator)
delete(params, parser.module.allocator)
return function_type
@@ -591,6 +600,17 @@ parse_integer_magnitude :: proc(text: string) -> (u64, bool) {
parse_primary :: proc(parser: ^Parser, nesting: int) -> ast.Expr_Id {
tok := current(parser)
#partial switch tok.kind {
case .Keyword_Int, .Keyword_Float, .Keyword_Range, .Keyword_Void, .Keyword_Bool:
start := tok
target := parse_type_atom(parser)
return add_expr(parser, ast.Expr{
kind=.Type,
span=start.span,
type=target,
left=ast.INVALID_EXPR,
right=ast.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
})
case .Keyword_I8, .Keyword_I16, .Keyword_I32, .Keyword_I64,
.Keyword_U8, .Keyword_U16, .Keyword_U32, .Keyword_U64,
.Keyword_Isize, .Keyword_Usize, .Keyword_F32, .Keyword_F64,
@@ -601,7 +621,14 @@ parse_primary :: proc(parser: ^Parser, nesting: int) -> ast.Expr_Id {
start := tok
target := parse_type_atom(parser)
if _, ok := allow(parser, .Left_Paren); !ok {
return invalid_expr(parser, current(parser).span, "expected '(' after scalar cast type")
return add_expr(parser, ast.Expr{
kind=.Type,
span=start.span,
type=target,
left=ast.INVALID_EXPR,
right=ast.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
})
}
parser.delimiter_depth += 1
skip_newlines(parser)
@@ -896,7 +923,7 @@ is_simple_range_bound :: proc(expr: ast.Expr) -> bool {
prefix_binding_power :: proc(kind: token.Kind) -> (right: int, ok: bool) {
#partial switch kind {
case .Minus, .Ampersand, .Bang, .Keyword_Try:
case .Minus, .Ampersand, .Bang, .Dollar, .Keyword_Try:
return 20, true
}
return 0, false
@@ -913,6 +940,18 @@ parse_expression_bp :: proc(parser: ^Parser, minimum_binding_power, nesting: int
left := ast.INVALID_EXPR
if right_power, ok := prefix_binding_power(current(parser).kind); ok {
operator := advance(parser)
if operator.kind == .Dollar && current(parser).kind == .Left_Brace {
body := parse_block(parser)
end := previous(parser)
left = add_expr(parser, ast.Expr{
kind = .Comptime,
span = span_from(operator.span, end.span),
body = body,
left = ast.INVALID_EXPR,
right = ast.INVALID_EXPR,
diagnostic = source.INVALID_DIAGNOSTIC,
})
} else {
if parser.delimiter_depth > 0 {
skip_newlines(parser)
}
@@ -922,6 +961,7 @@ parse_expression_bp :: proc(parser: ^Parser, minimum_binding_power, nesting: int
#partial switch operator.kind {
case .Ampersand: prefix_kind = .Address
case .Bang: prefix_kind = .Not
case .Dollar: prefix_kind = .Comptime
case .Keyword_Try: prefix_kind = .Try
}
left = add_expr(parser, ast.Expr{
@@ -931,6 +971,7 @@ parse_expression_bp :: proc(parser: ^Parser, minimum_binding_power, nesting: int
right = ast.INVALID_EXPR,
diagnostic = source.INVALID_DIAGNOSTIC,
})
}
} else {
left = parse_primary(parser, nesting)
}
@@ -1577,6 +1618,7 @@ parse_params :: proc(parser: ^Parser) -> ([]ast.Param, bool) {
}
continue
}
_, comptime_value := allow(parser, .Dollar)
names: [dynamic]token.Token
names.allocator = parser.module.allocator
for {
@@ -1596,7 +1638,12 @@ parse_params :: proc(parser: ^Parser) -> ([]ast.Param, bool) {
}
type_syntax := parse_type(parser)
for name in names {
append(&params, ast.Param{name=name.symbol, span=name.span, type=type_syntax})
append(&params, ast.Param{
name=name.symbol,
span=name.span,
type=type_syntax,
comptime_value=comptime_value,
})
}
delete(names)
skip_newlines(parser)
+1
View File
@@ -36,6 +36,7 @@ Kind :: enum u8 {
Range_Inclusive,
Ellipsis,
At,
Dollar,
Star,
Ampersand,
Caret,
+986 -18
View File
File diff suppressed because it is too large Load Diff
+11
View File
@@ -0,0 +1,11 @@
b :: import "@std/build"
config :: b.BuildConfig{
name = "hello",
source = "src",
libraries = &[],
lib_paths = &[],
includes = &[],
defines = &[],
links = &[],
}
+3
View File
@@ -0,0 +1,3 @@
main func() i32 {
return 0
}
+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
}
+11
View File
@@ -0,0 +1,11 @@
b :: import "@std/build"
config :: b.BuildConfig{
name = "manual_build",
source = "src",
libraries = &[],
lib_paths = &[],
includes = &[],
defines = &[],
links = &["examples/build/manual/native.c"],
}
+3
View File
@@ -0,0 +1,3 @@
int foreign_add(int a, int b) {
return a + b;
}
+5
View File
@@ -0,0 +1,5 @@
foreign_add c_func(a, b i32) i32
main func() i32 {
return foreign_add(20, 22)
}
+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
}
+18
View File
@@ -0,0 +1,18 @@
# Illustrative build.bro for a raylib program (not run in CI: needs raylib
# installed).
#
# Generate the raylib bindings once with:
# brolang translate-c /opt/homebrew/Cellar/raylib/*/include/raylib.h > game/raylib.bro
# then:
# brolang build examples/build/raylib
b :: import "@std/build"
config :: b.BuildConfig{
name = "game",
source = "source",
libraries = &["raylib"],
lib_paths = &["/opt/homebrew/lib"],
includes = &["/opt/homebrew/include"],
defines = &[],
links = &["-framework", "Cocoa", "-framework", "IOKit", "-framework", "CoreVideo", "-framework", "OpenGL"],
}
+250
View File
@@ -0,0 +1,250 @@
# Bouncing-shapes sandbox — a tour of brolang on top of raylib.
#
# Click to spawn a shape under the cursor, WASD/arrows to blow them around,
# SPACE to clear. The shape nearest the cursor is highlighted with its stats.
#
# Feature tour: native structs, enums, tagged unions + match (value, statement,
# payload capture, void variants, contextual construction), optionals + unwrap,
# value-loops (`yield :blk`), fallible functions with try/catch, defer, ranged
# and pointer-capturing for-loops, while, break/continue, compound assignment,
# scalar C interop, and multi-line strings.
rl :: import "@vendor/raylib"
# --- screen and physics constants -------------------------------------------
W :: 900
H :: 540
CAP usize :: 64
GRAV :: 0.18 # downward pull per frame
DAMP :: 0.82 # energy kept on a wall bounce
FORCE :: 0.9 # wind impulse from a key press
SPINMAX :: 3.0 # spin magnitude cap
RING_PAD :: 6.0 # highlight ring spacing
# --- shapes -----------------------------------------------------------------
Kind :: enum {
circle
square
triangle
}
Ball :: struct {
x f32
y f32
dx f32
dy f32
radius f32
kind Kind
}
# 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 rl.Vector2 # spawn a shape at this point
push struct { dx f32, dy f32 } # blow every shape this way
clear void
idle void
}
# Spawning can fail when the backing array is full; the error carries the cap so
# the caller can report it.
SpawnError :: union(enum) {
full struct { cap usize }
}
# value-match used as an expression source: each arm yields a Color.
color_for func(k Kind) rl.Color {
col :: match k {
.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
}
# value-match dispatching to a contextual return, used to cycle spawn kind.
next_kind func(k Kind) Kind {
return match k {
.circle: .square
.square: .triangle
.triangle: .circle
}
}
# 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(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(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 }
return .idle
}
# Fallible capacity check: returns the slot index to fill, or fails `.full`.
reserve func(used usize) usize ! SpawnError {
if (used >= CAP) return .full{ cap = CAP }
return used
}
# Advance one ball: gravity, integrate, bounce off the four walls with damping.
# `b` is a pointer into the array, so the writes land in place.
step func(b @mut Ball) void {
b.dy += GRAV
b.x += b.dx
b.y += b.dy
if (b.x < b.radius) {
b.x = b.radius
b.dx = -b.dx * DAMP
}
right :: f32(W) - b.radius
if (b.x > right) {
b.x = right
b.dx = -b.dx * DAMP
}
if (b.y < b.radius) {
b.y = b.radius
b.dy = -b.dy * DAMP
}
floor :: f32(H) - b.radius
if (b.y > floor) {
b.y = floor
b.dy = -b.dy * DAMP
}
}
draw_ball func(b @Ball, highlight bool) void {
col :: color_for(b.kind)
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)
.triangle: rl.DrawPoly(center, 3, b.radius, 0.0, col)
}
if highlight {
ring rl.Color = rl.Color{ r = 250, g = 245, b = 200, a = 255 }
rl.DrawPoly(center, 24, b.radius + RING_PAD, 0.0, ring)
}
}
main func() i32 {
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)
help ::
`[click] spawn a shape [WASD/arrows] blow wind
`[space] clear
balls [CAP]mut Ball = undefined
count usize = 0 # number of live balls, in slots 0..count
kc Kind = .circle # next kind to spawn
spin f32 = 1.0 # rotates spawn velocity for variety
at_cap bool = false # show the "at capacity" banner
bg :: rl.Color{ r = 24, g = 26, b = 34, a = 255 }
text :: rl.Color{ r = 225, g = 225, b = 230, a = 255 }
warn :: rl.Color{ r = 245, g = 180, b = 90, a = 255 }
while !rl.WindowShouldClose() {
# --- input -> command -----------------------------------------------
cmd :: read_command()
match cmd {
.spawn |at|: {
slot :: reserve(count) catch |e| {
match e {
.full |info|: at_cap = true
}
yield CAP # sentinel: >= CAP means "didn't fit"
}
if (slot < CAP) {
balls[slot] = Ball{
x = f32(at.x), y = f32(at.y),
dx = FORCE * 6.0 * spin,
dy = -FORCE * 5.0,
radius = 18.0,
kind = kc,
}
count += 1
kc = next_kind(kc)
spin = -spin * 1.2
if (spin > SPINMAX or spin < -SPINMAX) spin = 1.0
at_cap = false
}
}
.push |f|: {
for (&balls) |@b, i| {
if (i >= count) break
b.dx += f.dx
b.dy += f.dy
}
}
.clear: {
count = 0
at_cap = false
}
.idle: {}
}
# --- physics --------------------------------------------------------
for (&balls) |@b, i| {
if (i >= count) break
step(b)
}
# --- which shape is under the cursor? (optional via a value-loop) ---
mouse :: rl.GetMousePosition()
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
}
# --- draw -----------------------------------------------------------
rl.BeginDrawing()
rl.ClearBackground(bg)
for (&balls) |@b, i| {
if (i >= count) break
hot bool = false
if sel |s| {
if (s == i) hot = true # true only for the hovered ball
}
draw_ball(b, hot)
}
rl.DrawText(help, 16, 16, 20, text)
if sel |s| {
label :: match balls[s].kind {
.circle: "circle"
.square: "square"
.triangle: "triangle"
}
rl.DrawText(label, 16, H - 36, 20, text)
}
if (at_cap) rl.DrawText("at capacity", W - 170, 16, 20, warn)
rl.DrawFPS(W - 90, H - 28)
rl.EndDrawing()
}
return 0
}
+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
}
File diff suppressed because it is too large Load Diff
+49
View File
@@ -0,0 +1,49 @@
sum func(a, b int) int {
return a + b
}
max func(a, b int) int {
if a > b {
return a
}
return b
}
nested func(value int) int {
two :: 2
return sum(value, two)
}
make_array func($N usize) [N]u8 {
data [N]u8 = undefined
return data
}
forced :: $sum(1, 2)
main func() i32 {
value i32 :: $sum(20, 22)
choice i32 :: $max(9, 3)
blocked i32 :: ${
local :: 5
yield sum(local, 6)
}
bytes [_]u8 :: make_array($nested(2))
if forced != 3 {
return 1
}
if value != 42 {
return 2
}
if choice != 9 {
return 3
}
if blocked != 11 {
return 4
}
if bytes.len != 4 {
return 5
}
return 0
}
@@ -0,0 +1,46 @@
Point :: struct {
x i32
}
max func($T type, a, b T) T {
if a > b {
return a
}
return b
}
id func($T type, value T) T {
return value
}
buffer func($T type, $N usize, value T) [N]T {
data [N]T = undefined
_ = value
return data
}
main func() i32 {
a i32 :: 42
b i32 :: 27
if max(i32, a, b) != 42 {
return 1
}
small_a u8 :: 3
small_b u8 :: 9
if max(u8, small_a, small_b) != 9 {
return 2
}
p Point :: Point { x = 11 }
q Point :: id(Point, p)
if q.x != 11 {
return 3
}
bytes [_]u8 :: buffer(u8, 4, small_a)
if bytes.len != 4 {
return 4
}
return 0
}
+209
View File
@@ -0,0 +1,209 @@
State :: enum {
idle
ready
failed
}
Point :: struct {
x i32
y i32
}
Box :: union(enum) {
point Point
empty void
}
Error :: enum {
bad
}
make_point func() Point {
return Point { x = 3, y = 4 }
}
sum_loop func(limit i32) i32 {
total i32 = 0
i i32 = 0
while i < limit {
i += 1
if i == 2 {
continue
}
total += i
}
return total
}
sum_for func() i32 {
total i32 = 0
values [_]i32 = [1, 2, 3]
for values |value, index| {
total += value + index
}
return total
}
defer_value func() i32 {
value i32 = 1
{
defer value += 10
value += 1
}
return value
}
describe func(box Box) i32 {
return match box {
.point |p|: p.x + p.y
.empty: 0
}
}
maybe func(flag bool) ?i32 {
if flag {
return 9
}
return none
}
may_fail func(flag bool) i32 ! Error {
if flag {
return .bad
}
return 7
}
increment func(value i32) i32 {
return value + 1
}
call_native func(callback *func(value i32) i32, value i32) i32 {
return callback(value)
}
call_fallible func(callback *func(flag bool) i32 ! Error, flag bool) i32 ! Error {
return try callback(flag)
}
use_try func() i32 ! Error {
value :: try may_fail(false)
return value + 1
}
recover func() i32 {
return may_fail(true) catch |e| {
match e {
.bad: yield 5
}
}
}
bump_ptr func(value @mut i32) void {
value^ += 1
}
alias_add func(left @mut i32, right @mut i32) void {
left^ += 2
right^ += 3
}
storage_mutation func() i32 {
values [3]mut i32 = [1, 2, 3]
values[0] += 1
bump_ptr(&values[1])
view []mut i32 = values[..]
for view |@item| {
item^ += 1
}
pointer *mut i32 = view.ptr
pointer[2] += 1
alias_add(&values[0], &view[0])
box Box = Box { point = Point { x = 2, y = 3 } }
match box {
.point |@p|: p.x += values[1]
.empty: values[0] = values[0]
}
if view.len != 3 {
return 0
}
return values[0] + view[1] + pointer[2] + box.point.x
}
GLOBAL :: $sum_loop(4)
main func() i32 {
point Point :: $make_point()
numbers [_]i32 :: $[4, 5, 6]
box Box :: $Box { point = Point { x = 8, y = 1 } }
state State :: $State.ready
name :: $"bro"
value i32 :: $sum_for()
deferred i32 :: $defer_value()
optional i32 :: $maybe(true)?
tried i32 :: $use_try() catch 0
recovered i32 :: $recover()
storage i32 :: $storage_mutation()
called i32 :: $call_native(increment, 11)
fallible_ok i32 :: $call_fallible(may_fail, false) catch 0
fallible_err i32 :: $call_fallible(may_fail, true) catch |e| {
result i32 :: match e {
.bad: 13
}
yield result
}
if point.x + point.y != 7 {
return 1
}
if numbers.len != 3 or numbers[2] != 6 {
return 2
}
if describe(box) != 9 {
return 3
}
if state != State.ready {
return 4
}
if name.len != 3 {
return 5
}
if value != 9 {
return 6
}
if deferred != 12 {
return 7
}
if optional != 9 {
return 8
}
if tried != 8 {
return 9
}
if recovered != 5 {
return 10
}
if GLOBAL != 8 {
return 11
}
if storage != 23 {
return 12
}
if called != 12 {
return 13
}
if call_native(increment, 20) != 21 {
return 14
}
if fallible_ok != 7 {
return 15
}
if fallible_err != 13 {
return 16
}
return 0
}
@@ -0,0 +1,32 @@
make_array func($N usize) [N]u8 {
data [N]u8 = undefined
return data
}
value func($N usize) usize {
return N
}
main func() i32 {
four [_]u8 :: make_array(4)
if four.len != 4 {
return 1
}
if value(4) != 4 {
return 2
}
eight [_]u8 :: make_array(8)
if eight.len != 8 {
return 3
}
if value(8) != 8 {
return 4
}
again [_]u8 :: [1, 2, 3, 4]
if again.len != 4 {
return 5
}
return 0
}
+3 -3
View File
@@ -44,11 +44,11 @@ main func() i32 {
total = total + 5 # 35
}
# block scoping: inner x shadows outer x, outer is unchanged after the block
# block scoping: inner bindings do not escape the block
x i32 = 1
if x == 1 {
x i32 = 100
if x == 100 {
inner_x i32 = 100
if inner_x == 100 {
total = total + 5 # 40
}
}
@@ -1,4 +1,4 @@
bad int = 1
bad i32 :: undefined
read_bad func() int {
return bad
@@ -1,4 +1,4 @@
bad int = 1
bad i32 :: undefined
read_bad func() int {
return bad
-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
}
+30
View File
@@ -0,0 +1,30 @@
Point :: struct {
x i32
}
counter int = 0
ratio float = 1
span range = 0..2
point Point = Point { x = 1 }
values [_]mut i32 = [10, 20]
bump func(value @mut i32) void {
value^ += 1
}
main func() i32 {
counter = 10
counter += 5
bump(&counter)
point.x += counter
values[1] = point.x
total i32 = counter + point.x + values[1]
for span |i| {
total += i
}
if ratio == 1.0 {
total += 1
}
return total
}
+3 -3
View File
@@ -31,12 +31,12 @@ main func() i32 {
}
}
# The body-local k shadows only inside the body. The update still targets
# Body-local storage stays scoped to the body. The update still targets
# the mutable k declared before the loop.
k u32 = 0
while k < 4 : k = k + 1 {
k u32 = 100
if k == 100 {
body_k u32 = 100
if body_k == 100 {
total = total + 2
}
}
+291 -2
View File
@@ -11,9 +11,12 @@ import "core:os/os2"
import "core:path/filepath"
import "core:strings"
BROLANG_VERSION :: "0.0.0-dev"
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 +74,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,17 +95,261 @@ 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; writes root/build/name)",
)
fmt.eprintln(
" brolang new <project-path>",
)
fmt.eprintln(
" brolang init",
)
fmt.eprintln(
" brolang version",
)
}
is_translate_c_command :: proc(arg: string) -> bool {
return arg == "translate-c" || arg == "--translate-c"
}
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"
}
is_version_command :: proc(arg: string) -> bool {
return arg == "version"
}
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 = \""
@@ -233,6 +485,36 @@ run_translate_c :: proc(args: []string) -> int {
}
main :: proc() {
if len(os2.args) >= 2 && is_version_command(os2.args[1]) {
if len(os2.args) != 2 {
print_usage()
os2.exit(2)
}
fmt.printfln("brolang %s", BROLANG_VERSION)
os2.exit(0)
}
if len(os2.args) >= 2 && is_build_command(os2.args[1]) {
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))
}
@@ -244,7 +526,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)
}
+17
View File
@@ -0,0 +1,17 @@
# 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.
#
# 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 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)
includes [][]u8 # C include directories (-I)
defines [][]u8 # C preprocessor defines (name or name=value)
links [][]u8 # extra linker inputs (object/source files, -framework pairs)
}