Compare commits

..

2 Commits

Author SHA1 Message Date
hl-valdemar ef91f13e7b unsigned integer constraint (uint) 2026-07-18 00:17:13 +02:00
hl-valdemar b09787029d native test framework 2026-07-18 00:12:12 +02:00
25 changed files with 142928 additions and 139650 deletions
+11 -4
View File
@@ -18,11 +18,13 @@ roadmap and milestone history.
- `hide` makes any named top-level declaration file-local; declarations are public by default,
leading underscores are ordinary identifier characters, and imports are always file-local
- relative `.h` imports as synthetic C header package namespaces
- native `name test { ... }` declarations with implicit fallible-void results, plus anonymous
transitive `test import "..."` discovery used only by test builds
- root `main` validation with trap executable recovery for missing or unusable entry points
### scalar, aggregate, and pointer types
- exact-width integers, `isize`, `usize`, `f32`, `f64`, `bool`, `void`, `anyopaque`, and contextual `int`, `float`, and `range` constraints
- exact-width integers, concrete pointer-sized `isize` / `usize`, `f32`, `f64`, `bool`, `void`, and `anyopaque`; contextual `int` accepts the whole integer family, while `uint` accepts only unsigned native and target-classified C integers
- 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)`
@@ -42,16 +44,17 @@ roadmap and milestone history.
#### native record constraint fields
A direct `int`, `float`, or `range` field in a named native struct or union is a
A direct `int`, `uint`, `float`, or `range` field in a named native struct or union is a
program-wide constraint, not per-value polymorphism. Before record layout, all reachable keyed
constructors, field assignments, and concrete uses of field reads contribute demands and the field
resolves once to one concrete runtime type. Compatible scalar demands widen normally. Integer
literals remain provisional until inference settles, so a later `usize` use can resolve an `int`
field to `usize`; otherwise literal-only `int` fields use the widest smallest-signed type required,
and literal-only `float` fields use `f64`.
literal-only `uint` fields use the widest smallest-unsigned type required, and literal-only `float`
fields use `f64`.
An undemanded field or incompatible demands are errors. This inference applies only to direct
fields of named native records. `c_struct` fields, nested constraints such as `[]int`, and fields in
fields of named native records. `c_struct` fields, nested constraints such as `[]int` / `[]uint`, and fields in
anonymous generated records still require concrete runtime types.
#### keyword member names
@@ -180,6 +183,8 @@ as `math.divfloor(a, b)` resolve to ordinary functions.
- `std/io` explicit `Io` capabilities, provider-bound `Reader`/`Writer` handles, existing-file open/close operations, allocation-free `write_all`, and comptime-expanded writer-first `print`; formatting supports natural `{}`, byte `{s}`, decimal `{d}`, integer `{b}` / `{o}` / `{x}` / `{X}`, byte-character `{c}`, scientific float `{e}`, and `{{` / `}}`, with malformed formats and incompatible tuple fields rejected at comptime
- entry points are either `main func() ...` or `main func(init process.Init) ...`; `std/process.Init` carries startup capabilities, currently only `io`, while the system provider remains hidden inside `std/io`
- `std/debug.print` is an allocation-free, failure-ignoring stderr escape hatch independent of `process.Init`
- `std/testing` supplies fallible `expect` and expected-first `expect_equal`; direct calls through
an alias of exactly `@std/testing` receive compiler-injected source locations
### compiler behavior
@@ -188,6 +193,8 @@ as `math.divfloor(a, b)` resolve to ordinary functions.
- static, eager runtime, mutable runtime, and deferred problematic globals with cycle diagnostics
- demand-driven LLVM declarations for referenced foreign functions
- root `main` may be parameterless or accept canonical `@std/process Init`; the generated C entry point obtains the hidden system I/O provider and constructs the init value
- `brolang test [root]` reuses `build.bro`, discovers only explicit test-import edges, skips the
application entry point, and runs tests sequentially while continuing after assertion failures
- replaceable dynamically loaded libclang C-import backend
- C-header import caching by canonical path, target, include paths, and defines
+21
View File
@@ -73,6 +73,27 @@ 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/<name>-test`, and reuses
its C link inputs, libraries, include paths, and defines.
```bro
math :: import "../math"
testing :: import "@std/testing"
test import "../math"
addition test {
try testing.expect(math.add(20, 22) == 42)
try testing.expect_equal(42, math.add(20, 22))
}
```
`test import` discovers tests transitively without creating a namespace;
calling package code still requires an ordinary import. Ordinary imports do
not discover dependency tests. Assertions report their source location, a
failure ends only the current test, and the runner continues with the suite.
Relative `.h` imports create synthetic package namespaces backed by libclang:
```bro
+16
View File
@@ -897,6 +897,22 @@
- `tag!` reads or folds a tagged union discriminant, while `tagname!` turns a comptime-known enum
value into its immutable field-name string
41. native test framework (implemented; v1)
- `name test { ... }` declares an implicit `void ! testing.Error` test omitted from executable builds
- anonymous `test import` edges discover dependency tests transitively; ordinary imports remain
separate namespaces and never discover tests
- direct `@std/testing` `expect` and expected-first `expect_equal` calls inject their source locations
- `brolang test` reuses `build.bro`, runs tests sequentially, continues after assertion failures,
prints a summary, and returns failure when a test fails or traps
- filtering, skipping, fixtures, snapshots, parallelism, isolation, allocators, and more assertion
families remain deferred
42. unsigned integer constraint (implemented)
- `uint` is the unsigned subset constraint while `int` remains the whole integer family
- literal-only values choose the smallest fitting `u8`, `u16`, `u32`, or `u64`
- native unsigned scalars and target-classified unsigned C scalars satisfy the constraint
- `isize` and `usize` remain concrete pointer-sized types
## A word on unchecked casts
For casts that bypass safety checks, Honey provides builtin functions:
+8
View File
@@ -227,6 +227,7 @@ Function :: struct {
c_abi: bool,
imported: bool,
generated: bool,
test: bool,
file_hidden: bool,
has_body: bool,
variadic: bool,
@@ -276,6 +277,7 @@ Import :: struct {
target: Package_Id,
valid: bool,
used: bool,
test_only: bool,
diagnostic: source.Diagnostic_Id,
}
@@ -310,9 +312,15 @@ Package :: struct {
path: string,
name: symbol.Id,
available: bool,
test: bool,
kind: Package_Kind,
}
Compile_Mode :: enum u8 {
Executable,
Test,
}
Package_Kind :: enum u8 {
Native,
C_Header,
+69 -24
View File
@@ -43,26 +43,7 @@ destroy_build_config :: proc(cfg: ^BuildConfig) {
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)
}
load_build_config :: proc(project_root: string) -> (BuildConfig, bool) {
sources := source.init_store()
defer source.destroy_store(&sources)
diagnostics := source.init_store_diagnostics(&sources)
@@ -73,7 +54,7 @@ run_build :: proc(root: string) -> int {
arena: vmem.Arena
if err := vmem.arena_init_growing(&arena); err != nil {
fmt.eprintln("failed to initialize build arena:", err)
return 2
return {}, false
}
defer vmem.arena_destroy(&arena)
a := vmem.arena_allocator(&arena)
@@ -82,7 +63,7 @@ run_build :: proc(root: string) -> int {
if !loaded {
source.print_all(&diagnostics)
fmt.eprintln("failed to load build root:", project_root)
return 2
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
@@ -90,10 +71,32 @@ run_build :: proc(root: string) -> int {
hir_module := checker.check(&ast_module, &diagnostics, &symbols, target.DEFAULT, a)
if build_bro_has_errors(&diagnostics) {
source.print_all(&diagnostics)
return {}, false
}
return extract_build_config(&hir_module, &symbols)
}
project_root_for_command :: proc(root, command: string) -> (string, bool, bool) {
if len(root) > 0 {
return root, false, true
}
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)
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 :: proc(root: string) -> int {
project_root, owned, found := project_root_for_command(root, "build")
if !found {
return 2
}
cfg, ok := extract_build_config(&hir_module, &symbols)
defer if owned {delete(project_root)}
cfg, ok := load_build_config(project_root)
if !ok {
return 2
}
@@ -109,6 +112,48 @@ run_build :: proc(root: string) -> int {
return compile_package(program, output, cfg.link_arguments, target.DEFAULT, cfg.c_options, project_root)
}
run_tests :: proc(root: string) -> int {
project_root, owned, found := project_root_for_command(root, "test")
if !found {
return 2
}
defer if owned {delete(project_root)}
cfg, ok := load_build_config(project_root)
if !ok {
return 2
}
defer destroy_build_config(&cfg)
program := filepath.join({project_root, cfg.source_dir})
defer delete(program)
test_name := fmt.aprintf("%s-test", cfg.output_name)
defer delete(test_name)
output, output_ok := build_output_path(project_root, test_name)
if !output_ok {
return 2
}
defer delete(output)
status := compile_package(
program, output, cfg.link_arguments, target.DEFAULT, cfg.c_options, project_root, .Test,
)
if status != 0 {
return status
}
state, stdout, stderr, err := os2.process_exec(
os2.Process_Desc{command=[]string{output}},
context.allocator,
)
defer delete(stdout)
defer delete(stderr)
if len(stdout) > 0 {fmt.print(string(stdout))}
if len(stderr) > 0 {fmt.eprint(string(stderr))}
if err != nil {
fmt.eprintln("failed to run test executable:", err)
return 2
}
return 0 if state.exit_code == 0 else 1
}
valid_output_name :: proc(name: string) -> bool {
return len(name) > 0 && name != "." && name != ".." &&
!strings.contains(name, "/") && !strings.contains(name, "\\")
+113 -38
View File
@@ -501,7 +501,7 @@ write_type_label :: proc(checker: ^Checker, builder: ^strings.Builder, value: ty
strings.write_string(builder, "enum")
case .Alias, .Distinct, .Named:
write_type_label(checker, builder, item.child)
case .Invalid, .Void, .Anyopaque, .Int_Constraint, .Float_Constraint, .Range_Constraint, .Scalar:
case .Invalid, .Void, .Anyopaque, .Int_Constraint, .Uint_Constraint, .Float_Constraint, .Range_Constraint, .Scalar:
strings.write_string(builder, types.name(value))
}
}
@@ -1052,6 +1052,26 @@ fits_i64 :: proc(value: i128) -> bool {
return fits_signed_type(value, types.I64)
}
fits_u64 :: proc(value: i128) -> bool {
return value >= 0 && value <= i128(0xffff_ffff_ffff_ffff)
}
constraint_integer_literal_type :: proc(constraint: types.Type, value: i128) -> types.Type {
if constraint == types.UINT {
return types.smallest_unsigned_for_literal(u64(value)) if fits_u64(value) else types.INVALID
}
return types.smallest_signed_for_literal(i64(value)) if fits_i64(value) else types.INVALID
}
constraint_recovery_type :: proc(checker: ^Checker, constraint: types.Type) -> types.Type {
switch constraint {
case types.UINT: return types.U64
case types.FLOAT: return types.F64
case types.RANGE: return types.range(&checker.module.types, types.I64)
case: return types.I64
}
}
type_from_syntax :: proc(
checker: ^Checker,
value: ast.Type_Syntax,
@@ -1386,9 +1406,20 @@ build_symbol_indexes :: proc(checker: ^Checker) {
}
slice.sort_by(checker.global_index, global_index_less)
checker.import_index = make([]Import_Index_Entry, len(checker.ast_module.imports), checker.allocator)
import_count := 0
for import_item in checker.ast_module.imports {
if !import_item.test_only {
import_count += 1
}
}
checker.import_index = make([]Import_Index_Entry, import_count, checker.allocator)
import_index := 0
for import_item, id in checker.ast_module.imports {
checker.import_index[id] = Import_Index_Entry{scope=import_item.file, name=import_item.alias, id=ast.import_id(id)}
if import_item.test_only {
continue
}
checker.import_index[import_index] = Import_Index_Entry{scope=import_item.file, name=import_item.alias, id=ast.import_id(id)}
import_index += 1
}
slice.sort_by(checker.import_index, import_index_less)
}
@@ -2636,7 +2667,7 @@ call_arg_expected :: proc(checker: ^Checker, function: ast.Function, index: int)
declared := type_from_syntax(checker, function.params[index].type, function.pkg, function.file)
// A `float` param defaults to f64 so an integer-literal argument builds as a
// float constant (e.g. `f(3)` -> 3.0), mirroring `pi float = 3` for locals.
// `int`/`range` constraints have no single default and keep building naturally.
// Integer/range constraints keep building naturally from their literals.
if declared == types.FLOAT {
return types.F64
}
@@ -3613,13 +3644,21 @@ record_field_expr_candidate :: proc(
}
expr := checker.ast_module.exprs[expr_id]
constraint := checker.record_field_constraints[slot]
if constant := eval_integer_constant_in_context(checker, expr_id, pkg, file);
constant.kind == .Value && fits_i64(constant.value) {
candidate := types.smallest_signed_for_literal(i64(constant.value))
if constraint == types.FLOAT {
candidate = types.F64
if !numeric_operand_is_open(checker, expr_id, locals, pkg, file) {
concrete := types.constraint_target(constraint, inferred, &checker.module.types)
if is_runtime_type(checker, concrete) {
return merge_record_field_demand(checker, slot, concrete, expr.span)
}
}
if constant := eval_integer_constant_in_context(checker, expr_id, pkg, file);
constant.kind == .Value {
candidate := constraint_integer_literal_type(constraint, constant.value)
if constraint == types.FLOAT {
candidate = types.F64 if fits_i64(constant.value) else types.INVALID
}
if types.is_valid(candidate) {
return merge_record_field_default(checker, slot, candidate, expr.span)
}
return merge_record_field_default(checker, slot, candidate, expr.span)
}
if is_float_constant_expr(checker, expr_id) {
return merge_record_field_default(checker, slot, types.F64, expr.span)
@@ -3682,14 +3721,7 @@ finalize_record_field_inference :: proc(checker: ^Checker) {
)
}
if types.is_constraint(current) {
switch constraint {
case types.INT:
checker.module.types.fields[slot].type = types.I64
case types.FLOAT:
checker.module.types.fields[slot].type = types.F64
case types.RANGE:
checker.module.types.fields[slot].type = types.range(&checker.module.types, types.I64)
}
checker.module.types.fields[slot].type = constraint_recovery_type(checker, constraint)
}
}
}
@@ -3988,7 +4020,7 @@ find_spec :: proc(
}
// specialized_param_type maps a parameter's declared type to its monomorphized
// type for a given actual argument. A constraint param (`int`/`float`/`range`)
// type for a given actual argument. A constraint param (`int`/`uint`/`float`/`range`)
// resolves to the actual's family member (INVALID if out of family), so a call
// passing an out-of-family argument fails to specialize and is rejected.
specialized_param_type :: proc(
@@ -4415,14 +4447,16 @@ infer_expr :: proc(
if expr.kind != .Name || symbol.is_valid(expr.qualifier) || !static_name {
constant = eval_constant(checker, frame.expr)
}
if constant.kind == .Overflow || constant.kind == .Div_By_Zero ||
(constant.kind == .Value && !fits_i64(constant.value)) {
if constant.kind == .Overflow || constant.kind == .Div_By_Zero {
last = types.I64
_ = pop(&stack)
continue
}
if constant.kind == .Value {
last = types.smallest_signed_for_literal(i64(constant.value))
last = constraint_integer_literal_type(frame.expected, constant.value)
if !types.is_valid(last) {
last = types.I64
}
_ = pop(&stack)
continue
}
@@ -5055,7 +5089,7 @@ infer_statements :: proc(
declared_local := resolve_inferred_array(checker, type_from_syntax(checker, statement.type, pkg, file), statement.expr)
value_type := types.INVALID
if !is_undefined_expr(checker, statement.expr) {
expected := declared_local if is_runtime_type(checker, declared_local) else types.INVALID
expected := declared_local if is_runtime_type(checker, declared_local) || declared_local == types.UINT else types.INVALID
value_type = infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded, local_types, expected)
declared_local = resolve_inferred_array_from_type(checker, declared_local, value_type)
}
@@ -5071,7 +5105,8 @@ infer_statements :: proc(
const_val := i128(0)
if !is_runtime_type(checker, declared_local) && !is_undefined_expr(checker, statement.expr) {
constant := eval_integer_constant_in_context(checker, statement.expr, pkg, file)
if constant.kind == .Value && fits_i64(constant.value) {
if constant.kind == .Value &&
(fits_i64(constant.value) || declared_local == types.UINT && fits_u64(constant.value)) {
open = true
const_val = constant.value
} else if is_float_constant_expr(checker, statement.expr) {
@@ -5112,11 +5147,25 @@ infer_statements :: proc(
expected_assignment := types.INVALID
if statement.target != ast.INVALID_EXPR {
expected_assignment = infer_expr(checker, statement.target, locals^[:], pkg, file, demanded, local_types)
target_expr := checker.ast_module.exprs[statement.target]
if target_expr.kind == .Name && !symbol.is_valid(target_expr.qualifier) {
if local_index, ok := find_infer_local_index(locals^[:], target_expr.name); ok &&
locals^[local_index].declared == types.UINT {
expected_assignment = types.UINT
} else if global := find_global(checker, target_expr.name, pkg, file); global != ast.INVALID_GLOBAL {
ast_global := checker.ast_module.globals[global]
if type_from_syntax(checker, ast_global.type, ast_global.pkg, ast_global.file) == types.UINT {
expected_assignment = types.UINT
}
}
}
} else if statement.name != checker.sink_symbol {
if local_index, ok := find_infer_local_index(locals^[:], statement.name); ok {
expected_assignment = locals^[local_index].type
expected_assignment = types.UINT if locals^[local_index].declared == types.UINT else locals^[local_index].type
} else if global := find_global(checker, statement.name, pkg, file); global != ast.INVALID_GLOBAL {
expected_assignment = checker.global_types[global]
ast_global := checker.ast_module.globals[global]
declared_global := type_from_syntax(checker, ast_global.type, ast_global.pkg, ast_global.file)
expected_assignment = types.UINT if declared_global == types.UINT else checker.global_types[global]
}
}
value_type := infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded, local_types, expected_assignment)
@@ -5419,7 +5468,7 @@ infer_spec_locals_and_result :: proc(
if function.pkg == 0 && function.name == checker.main_symbol && function.result == types.INT {
declared = types.I32
}
result_hint := declared if is_runtime_type(checker, declared) else types.INVALID
result_hint := declared if is_runtime_type(checker, declared) || declared == types.UINT else types.INVALID
locals: [dynamic]Infer_Local
locals.allocator = checker.allocator
@@ -5604,7 +5653,7 @@ open_const_default_type :: proc(
if index, ok := find_infer_local_index(locals, expr.name); ok {
local := locals[index]
if local.open_const {
return types.smallest_signed_for_literal(i64(local.const_value))
return constraint_integer_literal_type(local.declared, local.const_value)
}
if local.open_float {
return types.F64
@@ -5622,7 +5671,9 @@ open_const_default_type :: proc(
return types.INVALID
}
if checker.global_open_const[index] {
return types.smallest_signed_for_literal(i64(checker.global_const_value[index]))
ast_global := checker.ast_module.globals[global]
declared := type_from_syntax(checker, ast_global.type, ast_global.pkg, ast_global.file)
return constraint_integer_literal_type(declared, checker.global_const_value[index])
}
if checker.global_open_float[index] {
return types.F64
@@ -5888,7 +5939,8 @@ infer_all :: proc(checker: ^Checker) {
}
}
constant := eval_integer_constant_in_context(checker, global.expr, global.pkg, global.file)
if constant.kind == .Value && fits_i64(constant.value) {
if constant.kind == .Value &&
(fits_i64(constant.value) || declared == types.UINT && fits_u64(constant.value)) {
checker.global_open_const[index] = true
checker.global_const_value[index] = constant.value
} else if is_float_constant_expr(checker, global.expr) {
@@ -5936,7 +5988,7 @@ infer_all :: proc(checker: ^Checker) {
type_from_syntax(checker, global.type, global.pkg, global.file),
global.expr,
)
expected := declared if is_runtime_type(checker, declared) else types.INVALID
expected := declared if is_runtime_type(checker, declared) || declared == types.UINT else types.INVALID
inferred := infer_expr(checker, global.expr, nil, global.pkg, global.file, expected=expected)
if resolved := resolve_inferred_array_from_type(checker, declared, inferred);
resolved != declared {
@@ -6009,7 +6061,8 @@ infer_all :: proc(checker: ^Checker) {
continue
}
if checker.global_open_const[index] {
checker.global_types[index] = types.smallest_signed_for_literal(i64(checker.global_const_value[index]))
declared := type_from_syntax(checker, global.type, global.pkg, global.file)
checker.global_types[index] = constraint_integer_literal_type(declared, checker.global_const_value[index])
defaulted = true
} else if checker.global_open_float[index] {
checker.global_types[index] = types.F64
@@ -6383,7 +6436,7 @@ build_constant_expr :: proc(
return invalid_hir_expr(checker, expr.span, id, recovery_type)
}
if constant.kind == .Overflow ||
(!types.is_concrete_integer(expected) && !fits_i64(constant.value)) {
(!types.is_concrete_integer(expected) && expected != types.UINT && !fits_i64(constant.value)) {
id := source.add(
checker.diagnostics,
expr.span,
@@ -6396,7 +6449,16 @@ build_constant_expr :: proc(
if constant.value >= 0 && constant.value <= i128(0xffff_ffff_ffff_ffff) {
value = transmute(i64)u64(constant.value)
}
result_type := types.smallest_signed_for_literal(value)
result_type := constraint_integer_literal_type(expected, constant.value)
if expected == types.UINT && !types.is_valid(result_type) {
id := source.addf(
checker.diagnostics,
expr.span,
"integer constant %d does not satisfy uint",
constant.value,
)
return invalid_hir_expr(checker, expr.span, id, types.U64)
}
if types.is_concrete_integer(expected) {
if !fits_integer_type(constant.value, expected, checker.target) {
id := source.addf(
@@ -12364,11 +12426,12 @@ build_globals :: proc(checker: ^Checker) {
if is_runtime_type(checker, declared) {
expected = declared
} else if constant := eval_integer_constant_in_context(checker, global.expr, global.pkg, global.file);
constant.kind == .Value && fits_i64(constant.value) &&
constant.kind == .Value &&
(fits_i64(constant.value) || declared == types.UINT && fits_u64(constant.value)) &&
is_runtime_type(checker, checker.global_types[global_index]) {
// Open integer constant: build against its demanded/defaulted type. Gated
// to the infer-side open-constant condition so out-of-range constants keep
// their original "exceeds signed i64 range" diagnostic.
// their original range diagnostic.
expected = checker.global_types[global_index]
} else if (is_float_constant_expr(checker, global.expr) ||
is_numeric_arithmetic_expr(checker, global.expr)) &&
@@ -12390,6 +12453,18 @@ build_globals :: proc(checker: ^Checker) {
global_type = types.I64
}
}
if diagnostic == source.INVALID_DIAGNOSTIC && types.is_constraint(declared) &&
!types.constraint_accepts(declared, global_type, &checker.module.types) {
diagnostic = source.addf(
checker.diagnostics,
global.span,
"could not resolve the '%s' constraint for global '%s'",
types.name(declared),
symbol_text(checker, global.name),
)
global_type = constraint_recovery_type(checker, declared)
expr = invalid_hir_expr(checker, global.span, diagnostic, global_type)
}
if !global.immutable && is_undefined_expr(checker, global.expr) {
diagnostic = source.add(
checker.diagnostics,
@@ -12874,7 +12949,7 @@ check :: proc(
main_template := find_template(&checker, checker.main_symbol, 0)
main_declarations := 0
for function in ast_module.functions {
if function.pkg == 0 && function.name == checker.main_symbol {
if !function.generated && function.pkg == 0 && function.name == checker.main_symbol {
main_declarations += 1
}
}
@@ -12917,7 +12992,7 @@ check :: proc(
delete(states, allocator)
propagate_problems(&checker)
for import_item in ast_module.imports {
if import_item.valid && !import_item.used {
if !import_item.test_only && import_item.valid && !import_item.used {
source.addf_warning(diagnostics, import_item.span, "unused import '%s'", symbol_text(&checker, import_item.alias))
}
}
+8
View File
@@ -1,5 +1,6 @@
package compiler
import "./ast"
import "./backend"
import "./cimport"
import "./checker"
@@ -34,6 +35,7 @@ compile_package :: proc(
selected := target.DEFAULT,
c_options := cimport.Options{},
project_root := "",
mode := ast.Compile_Mode.Executable,
) -> int {
sources := source.init_store()
defer source.destroy_store(&sources)
@@ -77,12 +79,18 @@ compile_package :: proc(
c_options,
selected,
project_root if len(project_root) > 0 else input_path,
mode,
)
if !loaded {
source.print_all(&diagnostics)
fmt.eprintln("failed to load root package directory:", input_path)
return 2
}
effective_root := project_root if len(project_root) > 0 else input_path
if !prepare_tests(&ast_module, &sources, &diagnostics, &symbols, mode, effective_root) {
source.print_all(&diagnostics)
return 1
}
// Generated C trampolines (for `static inline` imports) must be compiled and
// linked with the program. Write them out and add the source as a link input
+2
View File
@@ -14,6 +14,7 @@ is_identifier_continue :: proc(value: byte) -> bool {
keyword_kind :: proc(text: string) -> token.Kind {
switch text {
case "test": return .Keyword_Test
case "func": return .Keyword_Func
case "c_func": return .Keyword_C_Func
case "struct": return .Keyword_Struct
@@ -51,6 +52,7 @@ keyword_kind :: proc(text: string) -> token.Kind {
case "anyopaque": return .Keyword_Anyopaque
case "bool": return .Keyword_Bool
case "int": return .Keyword_Int
case "uint": return .Keyword_Uint
case "float": return .Keyword_Float
case "range": return .Keyword_Range
case "i8": return .Keyword_I8
+56 -22
View File
@@ -26,6 +26,7 @@ State :: struct {
c_options: cimport.Options,
selected: target.Target,
project_root: string,
mode: ast.Compile_Mode,
record_identities: [dynamic]string,
record_types: [dynamic]types.Type,
root_failed: bool,
@@ -1130,7 +1131,42 @@ load_header :: proc(state: ^State, path: string, import_span: source.Span) -> as
return pkg_id
}
load_package :: proc(state: ^State, path: string, import_span: source.Span, is_root := false) -> ast.Package_Id {
resolve_package_imports :: proc(state: ^State, pkg_id: ast.Package_Id) {
if int(pkg_id) >= len(state.module.packages) {
return
}
canonical := state.module.packages[pkg_id].path
import_count := len(state.module.imports)
for import_id in 0..<import_count {
import_item := state.module.imports[import_id]
if import_item.pkg != pkg_id || import_item.target != ast.INVALID_PACKAGE ||
import_item.test_only && (state.mode != .Test || !state.module.packages[pkg_id].test) {
continue
}
if filepath.is_abs(import_item.path) {
state.module.imports[import_id].diagnostic = source.add(state.diagnostics, import_item.span, "absolute import paths are invalid")
state.module.imports[import_id].valid = false
state.module.imports[import_id].target = add_placeholder(state, import_item.path)
continue
}
target_path, target_ok := resolve_import_path(state, canonical, import_item.path)
target := load_header(state, target_path, import_item.span) if filepath.ext(import_item.path) == ".h" else
load_package(state, target_path, import_item.span, include_tests=import_item.test_only)
state.module.imports[import_id].target = target
if !target_ok || target == ast.INVALID_PACKAGE || !state.module.packages[target].available {
state.module.imports[import_id].valid = false
}
delete(target_path, state.allocator)
}
}
load_package :: proc(
state: ^State,
path: string,
import_span: source.Span,
is_root := false,
include_tests := false,
) -> ast.Package_Id {
canonical, ok := filepath.abs(path, state.allocator)
if !ok || !os.is_dir(path) {
if is_root {
@@ -1152,6 +1188,10 @@ load_package :: proc(state: ^State, path: string, import_span: source.Span, is_r
return id
}
if existing := find_package(state, canonical); existing != ast.INVALID_PACKAGE {
if include_tests && !state.module.packages[existing].test {
state.module.packages[existing].test = true
resolve_package_imports(state, existing)
}
delete(canonical, state.allocator)
return existing
}
@@ -1161,6 +1201,7 @@ load_package :: proc(state: ^State, path: string, import_span: source.Span, is_r
path=canonical,
name=symbol.intern(state.symbols, filepath.base(canonical)),
available=true,
test=state.mode == .Test && is_root || include_tests,
})
files, files_ok := read_package_files(state, canonical)
if !files_ok {
@@ -1204,26 +1245,7 @@ load_package :: proc(state: ^State, path: string, import_span: source.Span, is_r
}
os.file_info_slice_delete(files, state.allocator)
import_count := len(state.module.imports)
for import_id in 0..<import_count {
import_item := state.module.imports[import_id]
if import_item.pkg != pkg_id || import_item.target != ast.INVALID_PACKAGE {
continue
}
if filepath.is_abs(import_item.path) {
state.module.imports[import_id].diagnostic = source.add(state.diagnostics, import_item.span, "absolute import paths are invalid")
state.module.imports[import_id].valid = false
state.module.imports[import_id].target = add_placeholder(state, import_item.path)
continue
}
target_path, target_ok := resolve_import_path(state, canonical, import_item.path)
target := load_header(state, target_path, import_item.span) if filepath.ext(import_item.path) == ".h" else load_package(state, target_path, import_item.span)
state.module.imports[import_id].target = target
if !target_ok || target == ast.INVALID_PACKAGE || !state.module.packages[target].available {
state.module.imports[import_id].valid = false
}
delete(target_path, state.allocator)
}
resolve_package_imports(state, pkg_id)
return pkg_id
}
@@ -1248,6 +1270,9 @@ declaration_conflicts :: proc(module: ^ast.Module, pkg: ast.Package_Id, file: as
validate_imports :: proc(state: ^State) {
for import_item, import_id in state.module.imports {
if import_item.test_only {
continue
}
if !symbol.is_valid(import_item.alias) && import_item.target != ast.INVALID_PACKAGE {
state.module.imports[import_id].alias = state.module.packages[import_item.target].name
}
@@ -1287,7 +1312,7 @@ validate_imports :: proc(state: ^State) {
find_type_import :: proc(module: ^ast.Module, file: ast.File_Id, alias: symbol.Id) -> ast.Import_Id {
for import_item, index in module.imports {
if import_item.file == file && import_item.alias == alias {
if !import_item.test_only && import_item.file == file && import_item.alias == alias {
return ast.import_id(index)
}
}
@@ -1879,6 +1904,7 @@ load :: proc(
c_options := cimport.Options{},
selected := target.DEFAULT,
project_root_path := "",
mode := ast.Compile_Mode.Executable,
) -> (ast.Module, bool) {
module := ast.init_module(allocator)
project_root_source := project_root_path if len(project_root_path) > 0 else root_path
@@ -1896,6 +1922,7 @@ load :: proc(
c_options=c_options,
selected=selected,
project_root=project_root,
mode=mode,
}
state.record_identities.allocator = allocator
state.record_types.allocator = allocator
@@ -1911,6 +1938,13 @@ load :: proc(
if root != ast.Package_Id(0) && root != ast.INVALID_PACKAGE {
state.root_failed = true
}
if mode == .Test {
testing_path, testing_error := filepath.join({project_root, "std", "testing"}, allocator)
if testing_error == nil {
_ = load_package(&state, testing_path, source.Span{})
delete(testing_path, allocator)
}
}
validate_imports(&state)
validate_declaration_aliases(&state)
resolve_enum_values(&state)
+46 -3
View File
@@ -146,7 +146,7 @@ invalid_expr :: proc(parser: ^Parser, span: source.Span, message: string) -> ast
is_type_token :: proc(kind: token.Kind) -> bool {
#partial switch kind {
case .Keyword_Int, .Keyword_Float, .Keyword_Range, .Keyword_I8, .Keyword_I16, .Keyword_I32, .Keyword_I64,
case .Keyword_Int, .Keyword_Uint, .Keyword_Float, .Keyword_Range, .Keyword_I8, .Keyword_I16, .Keyword_I32, .Keyword_I64,
.Keyword_U8, .Keyword_U16, .Keyword_U32, .Keyword_U64,
.Keyword_Isize, .Keyword_Usize, .Keyword_F32, .Keyword_F64,
.Keyword_C_Char, .Keyword_C_Schar, .Keyword_C_Uchar,
@@ -280,6 +280,9 @@ parse_type_atom :: proc(parser: ^Parser) -> ast.Type_Syntax {
case .Keyword_Int:
advance(parser)
return types.INT
case .Keyword_Uint:
advance(parser)
return types.UINT
case .Keyword_Float:
advance(parser)
return types.FLOAT
@@ -774,7 +777,7 @@ 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_Anyopaque, .Keyword_Bool:
case .Keyword_Int, .Keyword_Uint, .Keyword_Float, .Keyword_Range, .Keyword_Void, .Keyword_Anyopaque, .Keyword_Bool:
start := tok
target := parse_type_atom(parser)
return add_expr(parser, ast.Expr{
@@ -3002,7 +3005,7 @@ decode_multiline_string :: proc(parser: ^Parser, tok: token.Token) -> string {
return fmt.aprintf("%s", strings.to_string(builder), allocator=parser.module.allocator)
}
parse_import :: proc(parser: ^Parser, alias: token.Token, start: token.Token) {
parse_import :: proc(parser: ^Parser, alias: token.Token, start: token.Token, test_only := false) {
skip_newlines(parser)
path_token := current(parser)
if path_token.kind != .String {
@@ -3018,6 +3021,7 @@ parse_import :: proc(parser: ^Parser, alias: token.Token, start: token.Token) {
file=parser.file,
target=ast.INVALID_PACKAGE,
valid=false,
test_only=test_only,
diagnostic=source.INVALID_DIAGNOSTIC,
})
_ = finish_statement(parser)
@@ -3033,13 +3037,45 @@ parse_import :: proc(parser: ^Parser, alias: token.Token, start: token.Token) {
file=parser.file,
target=ast.INVALID_PACKAGE,
valid=true,
test_only=test_only,
diagnostic=source.INVALID_DIAGNOSTIC,
})
_ = finish_statement(parser)
}
parse_test :: proc(parser: ^Parser, name: token.Token) {
advance(parser) // consume 'test'
skip_newlines(parser)
if current(parser).kind != .Left_Brace {
source.add(parser.diagnostics, current(parser).span, "expected '{' after test name")
_ = finish_statement(parser)
return
}
body := parse_block(parser)
append(&parser.module.functions, ast.Function{
span=span_from(name.span, previous(parser).span),
name=name.symbol,
pkg=parser.pkg,
file=parser.file,
test=true,
has_body=true,
result=types.VOID,
body=body,
diagnostic=source.INVALID_DIAGNOSTIC,
})
}
parse_top_level :: proc(parser: ^Parser) {
hide_token, file_hidden := allow(parser, .Keyword_Hide)
if current(parser).kind == .Keyword_Test && peek(parser).kind == .Keyword_Import {
if file_hidden {
source.add(parser.diagnostics, hide_token.span, "test imports cannot use 'hide'")
}
start := advance(parser)
advance(parser) // consume 'import'
parse_import(parser, token.Token{}, start, test_only=true)
return
}
if current(parser).kind == .Keyword_Import {
if file_hidden {
source.add(parser.diagnostics, hide_token.span, "imports are already file-local and cannot use 'hide'")
@@ -3058,6 +3094,13 @@ parse_top_level :: proc(parser: ^Parser) {
return
}
name := advance(parser)
if current(parser).kind == .Keyword_Test {
if file_hidden {
source.add(parser.diagnostics, hide_token.span, "test declarations cannot use 'hide'")
}
parse_test(parser, name)
return
}
if current(parser).kind == .Colon_Colon {
saved := parser.cursor
advance(parser)
+281
View File
@@ -0,0 +1,281 @@
package compiler
import "./ast"
import "./lexer"
import "./parser"
import "./source"
import "./symbol"
import "./types"
import "core:fmt"
import "core:path/filepath"
import "core:slice"
import "core:strings"
Test_Entry :: struct {
function: ast.Function_Id,
package_path: string,
source_path: string,
offset: source.Offset,
}
test_entry_less :: proc(a, b: Test_Entry) -> bool {
if a.package_path != b.package_path {
return a.package_path < b.package_path
}
if a.source_path != b.source_path {
return a.source_path < b.source_path
}
return a.offset < b.offset
}
testing_package :: proc(module: ^ast.Module, project_root: string) -> ast.Package_Id {
path, err := filepath.join({project_root, "std", "testing"}, module.allocator)
if err != nil {
return ast.INVALID_PACKAGE
}
defer delete(path, module.allocator)
canonical, ok := filepath.abs(path, module.allocator)
if !ok {
return ast.INVALID_PACKAGE
}
defer delete(canonical, module.allocator)
for pkg, index in module.packages {
if pkg.path == canonical {
return ast.package_id(index)
}
}
return ast.INVALID_PACKAGE
}
source_file_id :: proc(module: ^ast.Module, id: source.Source_Id) -> ast.File_Id {
for file, index in module.files {
if file.source == id {
return ast.file_id(index)
}
}
return ast.INVALID_FILE
}
append_ast_expr :: proc(module: ^ast.Module, expr: ast.Expr) -> ast.Expr_Id {
id := ast.expr_id(len(module.exprs))
append(&module.exprs, expr)
return id
}
location_expr :: proc(
module: ^ast.Module,
sources: ^source.Store,
symbols: ^symbol.Table,
span: source.Span,
qualifier: symbol.Id,
) -> ast.Expr_Id {
line, column := 1, 1
path := "<unknown>"
if int(span.file) < len(sources.items) {
file := &sources.items[span.file]
line, column = source.line_and_column(file, span.start)
path = file.path
}
string_index := u64(len(module.strings))
append(&module.strings, strings.clone(path, module.allocator))
file_expr := append_ast_expr(module, ast.Expr{
kind=.String, span=span, integer=string_index,
left=ast.INVALID_EXPR, right=ast.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
})
line_expr := append_ast_expr(module, ast.Expr{
kind=.Integer, span=span, integer=u64(line),
left=ast.INVALID_EXPR, right=ast.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
})
column_expr := append_ast_expr(module, ast.Expr{
kind=.Integer, span=span, integer=u64(column),
left=ast.INVALID_EXPR, right=ast.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
})
values := []ast.Expr_Id{file_expr, line_expr, column_expr}
names := []string{"file", "line", "column"}
fields := make([]ast.Expr_Id, 3, module.allocator)
for index in 0..<3 {
fields[index] = append_ast_expr(module, ast.Expr{
kind=.Keyed, span=span,
name=symbol.intern(symbols, names[index]),
left=values[index], right=ast.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
})
}
return append_ast_expr(module, ast.Expr{
kind=.Struct_Literal, span=span,
qualifier=qualifier,
name=symbol.intern(symbols, "SourceLocation"),
args=fields,
left=ast.INVALID_EXPR, right=ast.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
})
}
inject_assertion_locations :: proc(
module: ^ast.Module,
sources: ^source.Store,
symbols: ^symbol.Table,
testing_pkg: ast.Package_Id,
) {
expect := symbol.intern(symbols, "expect")
expect_equal := symbol.intern(symbols, "expect_equal")
original_count := len(module.exprs)
for index in 0..<original_count {
expr := &module.exprs[index]
if expr.kind != .Call || expr.intrinsic || !symbol.is_valid(expr.qualifier) ||
(expr.name != expect && expr.name != expect_equal) {
continue
}
file := source_file_id(module, expr.span.file)
matched := false
for import_item in module.imports {
if !import_item.test_only && import_item.file == file &&
import_item.alias == expr.qualifier && import_item.target == testing_pkg {
matched = true
break
}
}
if !matched {
continue
}
location := location_expr(module, sources, symbols, expr.span, expr.qualifier)
args := make([]ast.Expr_Id, len(expr.args)+1, module.allocator)
copy(args, expr.args)
args[len(expr.args)] = location
delete(expr.args, module.allocator)
expr.args = args
}
}
write_brolang_string :: proc(builder: ^strings.Builder, value: string) {
for byte_value in transmute([]byte)value {
if byte_value == '\\' || byte_value == '"' {
strings.write_byte(builder, '\\')
}
strings.write_byte(builder, byte_value)
}
}
append_runner :: proc(
module: ^ast.Module,
sources: ^source.Store,
diagnostics: ^source.Diagnostics,
symbols: ^symbol.Table,
testing_pkg: ast.Package_Id,
tests: []Test_Entry,
) {
builder := strings.builder_make(module.allocator)
defer strings.builder_destroy(&builder)
strings.write_string(&builder, "main func() i32 {\n\tfailed i32 = 0\n")
for entry, index in tests {
test_id := entry.function
test := module.functions[test_id]
pkg := module.packages[test.pkg]
alias := fmt.tprintf("__brolang_test_%d", index)
name := fmt.tprintf("%s.%s", filepath.base(pkg.path), symbol.resolve(symbols, test.name))
strings.write_string(&builder, "\tif (!__brolang_testing.run(\"")
write_brolang_string(&builder, name)
fmt.sbprintf(&builder, "\", %s.%s)) ", alias, symbol.resolve(symbols, test.name))
strings.write_string(&builder, "{\n\t\tfailed += 1\n\t}\n")
}
fmt.sbprintf(&builder, "\t__brolang_testing.summary(%d - failed, failed)\n", len(tests))
strings.write_string(&builder, "\tif (failed != 0) return 1\n\treturn 0\n}\n")
runner_text := strings.to_string(builder)
source_id := source.add_source(sources, "<brolang-test-runner>", runner_text)
file_id := ast.file_id(len(module.files))
append(&module.files, ast.File{source=source_id, pkg=0})
stream := lexer.lex(&sources.items[source_id], diagnostics, symbols, module.allocator)
defer delete(stream.items)
parser.parse_into(&stream, &sources.items[source_id], diagnostics, module, 0, file_id)
append(&module.imports, ast.Import{
alias=symbol.intern(symbols, "__brolang_testing"),
path=strings.clone("@std/testing", module.allocator),
pkg=0, file=file_id, target=testing_pkg,
valid=true, used=true,
diagnostic=source.INVALID_DIAGNOSTIC,
})
for entry, index in tests {
test_id := entry.function
test := module.functions[test_id]
append(&module.imports, ast.Import{
alias=symbol.intern(symbols, fmt.tprintf("__brolang_test_%d", index)),
path=strings.clone(module.packages[test.pkg].path, module.allocator),
pkg=0, file=file_id, target=test.pkg,
valid=true, used=true,
diagnostic=source.INVALID_DIAGNOSTIC,
})
}
}
prepare_tests :: proc(
module: ^ast.Module,
sources: ^source.Store,
diagnostics: ^source.Diagnostics,
symbols: ^symbol.Table,
mode: ast.Compile_Mode,
project_root: string,
) -> bool {
if mode == .Executable {
for &function in module.functions {
if !function.test {
continue
}
function.generated = true
for &import_item in module.imports {
if !import_item.test_only && import_item.file == function.file {
import_item.used = true
}
}
}
return true
}
testing_pkg := testing_package(module, project_root)
if testing_pkg == ast.INVALID_PACKAGE {
source.add(diagnostics, source.Span{}, "test builds require @std/testing")
return false
}
error_type := types.find_named(
&module.type_store,
u32(testing_pkg),
u32(symbol.intern(symbols, "Error")),
)
if !types.is_enum(error_type, &module.type_store) {
source.add(diagnostics, source.Span{}, "@std/testing must declare Error as an enum")
return false
}
tests: [dynamic]Test_Entry
tests.allocator = module.allocator
defer delete(tests)
main_name := symbol.intern(symbols, "main")
for &function, index in module.functions {
if function.test {
if int(function.pkg) < len(module.packages) && module.packages[function.pkg].test {
function.result = types.VOID
function.error = error_type
file := module.files[function.file]
append(&tests, Test_Entry{
function=ast.function_id(index),
package_path=module.packages[function.pkg].path,
source_path=sources.items[file.source].path,
offset=function.span.start,
})
} else {
function.generated = true
}
} else if function.pkg == 0 && function.name == main_name {
function.generated = true
}
}
slice.sort_by(tests[:], test_entry_less)
inject_assertion_locations(module, sources, symbols, testing_pkg)
append_runner(module, sources, diagnostics, symbols, testing_pkg, tests[:])
return true
}
+3 -1
View File
@@ -50,6 +50,7 @@ Kind :: enum u8 {
Right_Brace,
Comma,
Pipe,
Keyword_Test,
Keyword_Func,
Keyword_C_Func,
Keyword_Struct,
@@ -87,6 +88,7 @@ Kind :: enum u8 {
Keyword_Anyopaque,
Keyword_Bool,
Keyword_Int,
Keyword_Uint,
Keyword_Float,
Keyword_Range,
Keyword_I8,
@@ -118,7 +120,7 @@ Kind :: enum u8 {
}
is_keyword :: proc(kind: Kind) -> bool {
return kind >= .Keyword_Func && kind <= .Keyword_C_Longdouble
return kind >= .Keyword_Test && kind <= .Keyword_C_Longdouble
}
Token :: struct {
+23 -1
View File
@@ -44,6 +44,7 @@ BOOL :: Type(29)
FLOAT :: Type(30)
RANGE :: Type(31)
ANYOPAQUE :: Type(32)
UINT :: Type(33)
DYNAMIC_START :: Type(64)
@@ -59,6 +60,7 @@ Kind :: enum u8 {
Void,
Anyopaque,
Int_Constraint,
Uint_Constraint,
Float_Constraint,
Range_Constraint,
Scalar,
@@ -595,6 +597,8 @@ kind :: proc(value: Type, store: ^Store = nil) -> Kind {
return .Anyopaque
case INT:
return .Int_Constraint
case UINT:
return .Uint_Constraint
case FLOAT:
return .Float_Constraint
case RANGE:
@@ -642,7 +646,7 @@ is_bool :: proc(value: Type) -> bool {
}
is_constraint :: proc(value: Type) -> bool {
return value == INT || value == FLOAT || value == RANGE
return value == INT || value == UINT || value == FLOAT || value == RANGE
}
// constraint_target reports the concrete type a constraint binding (local, or a
@@ -656,6 +660,8 @@ constraint_target :: proc(constraint, inferred: Type, store: ^Store = nil) -> Ty
switch constraint {
case INT:
return inferred if is_concrete_integer(inferred) else INVALID
case UINT:
return inferred if is_unsigned(inferred) else INVALID
case FLOAT:
if is_float(inferred) {
return inferred
@@ -673,6 +679,8 @@ constraint_accepts :: proc(constraint, concrete: Type, store: ^Store = nil) -> b
switch constraint {
case INT:
return is_concrete_integer(concrete)
case UINT:
return is_unsigned(concrete)
case FLOAT:
return is_float(concrete)
case RANGE:
@@ -1686,6 +1694,19 @@ smallest_signed_for_literal :: proc(value: i64) -> Type {
return I64
}
smallest_unsigned_for_literal :: proc(value: u64) -> Type {
if value <= 255 {
return U8
}
if value <= 65535 {
return U16
}
if value <= 4294967295 {
return U32
}
return U64
}
name :: proc(value: Type) -> string {
switch value {
case INVALID: return "<invalid>"
@@ -1693,6 +1714,7 @@ name :: proc(value: Type) -> string {
case ANYOPAQUE: return "anyopaque"
case BOOL: return "bool"
case INT: return "int"
case UINT: return "uint"
case FLOAT: return "float"
case RANGE: return "range"
case I8: return "i8"
+286 -1
View File
@@ -1485,6 +1485,7 @@ main func() void {
_ = maxval!(u8, u16)
_ = minval!(1)
_ = maxval!(int)
_ = maxval!(uint)
_ = maxval!(f32)
_ = maxval!(bool)
_ = maxval!(Named)
@@ -1513,7 +1514,7 @@ main func() void {
}
testing.expect_value(t, bad_arity, 2)
testing.expect(t, bad_type)
testing.expect_value(t, bad_target, 5)
testing.expect_value(t, bad_target, 6)
}
@(test)
@@ -12778,3 +12779,287 @@ main func() void {
testing.expect(t, types.is_slice(nested_field, &hir_module.types))
testing.expect_value(t, types.child_type(nested_field, &hir_module.types), types.INT)
}
@(test)
parser_records_native_tests_and_test_imports :: proc(t: ^testing.T) {
text := `test import "../math"
addition test {
return
}
`
source_file := source.Source{path="tests.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)
module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&module)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, len(module.imports), 1)
testing.expect(t, module.imports[0].test_only)
testing.expect_value(t, module.imports[0].alias, symbol.INVALID)
testing.expect_value(t, len(module.functions), 1)
testing.expect(t, module.functions[0].test)
testing.expect_value(t, symbol.resolve(&symbols, module.functions[0].name), "addition")
}
@(test)
native_test_framework_discovers_reports_and_preserves_main :: proc(t: ^testing.T) {
directory := "/tmp/brolang-test-native-framework"
root := "/tmp/brolang-test-native-framework/root"
dependency := "/tmp/brolang-test-native-framework/dependency"
root_path := "/tmp/brolang-test-native-framework/root/main.bro"
dependency_path := "/tmp/brolang-test-native-framework/dependency/math.bro"
test_output := "/tmp/brolang-test-native-framework-tests"
app_output := "/tmp/brolang-test-native-framework-app"
_ = os2.remove_all(directory)
defer _ = os2.remove_all(directory)
defer _ = os.remove(test_output)
defer _ = os.remove(app_output)
testing.expect(t, os2.make_directory_all(root) == nil)
testing.expect(t, os2.make_directory_all(dependency) == nil)
root_text := `testing :: import "@std/testing"
test import "../dependency"
main func() i32 { return 77 }
root_passes test {
try testing.expect(true)
}
root_fails test {
try testing.expect_equal(42, 41)
}
root_continues test {
try testing.expect(true)
}
`
dependency_text := `testing :: import "@std/testing"
dependency_passes test {
try testing.expect(true)
}
`
testing.expect(t, os.write_entire_file(root_path, transmute([]byte)root_text))
testing.expect(t, os.write_entire_file(dependency_path, transmute([]byte)dependency_text))
app_status := compiler_core.compile_package(
root, app_output, nil, target.DEFAULT, cimport.Options{}, ".",
)
testing.expect_value(t, app_status, 0)
app_state := run_executable(app_output)
testing.expect_value(t, app_state.exit_code, 77)
test_status := compiler_core.compile_package(
root, test_output, nil, target.DEFAULT, cimport.Options{}, ".", .Test,
)
testing.expect_value(t, test_status, 0)
state, stdout, stderr, _ := os2.process_exec(
os2.Process_Desc{command=[]string{test_output}},
context.allocator,
)
defer delete(stdout)
defer delete(stderr)
output := string(stderr)
testing.expect_value(t, state.exit_code, 1)
testing.expect(t, strings.contains(output, "PASS root.root_passes"))
testing.expect(t, strings.contains(output, "FAIL root.root_fails"))
testing.expect(t, strings.contains(output, "expected 42, found 41"))
testing.expect(t, strings.contains(output, "PASS root.root_continues"))
testing.expect(t, strings.contains(output, "PASS dependency.dependency_passes"))
testing.expect(t, strings.contains(output, root_path))
testing.expect(t, strings.contains(output, "3 passed, 1 failed"))
}
@(test)
uint_constraint_parses_as_type_and_comptime_value :: proc(t: ^testing.T) {
text := `value uint :: 1
Constraint :: uint
`
source_file := source.Source{path="uint.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)
module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&module)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, len(module.globals), 2)
testing.expect_value(t, module.globals[0].type, types.UINT)
testing.expect_value(t, module.exprs[module.globals[1].expr].kind, ast.Expr_Kind.Type)
testing.expect_value(t, module.exprs[module.globals[1].expr].type, types.UINT)
}
@(test)
uint_constraint_uses_smallest_unsigned_types_and_widens :: proc(t: ^testing.T) {
text := `Zero uint :: 0
Byte uint :: 255
Word uint :: 256
Dword uint :: 65536
Maximum uint :: 18446744073709551615
Counter uint = 1
widen func() uint {
value uint = 1
value = 1000
return value
}
widen_global func() void { Counter = 1000 }
main func() void {
_ = Zero
_ = Byte
_ = Word
_ = Dword
_ = Maximum
_ = widen()
widen_global()
}
`
source_file := source.Source{path="uint_types.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)
expected := []types.Type{types.U8, types.U8, types.U16, types.U32, types.U64, types.U16}
for value_type, index in expected {
testing.expect_value(t, hir_module.globals[index].type, value_type)
}
widen_symbol := symbol.intern(&symbols, "widen")
found_widen := false
for function in hir_module.functions {
if function.name != widen_symbol {
continue
}
found_widen = true
testing.expect_value(t, function.result, types.U16)
testing.expect_value(t, function.locals[0].type, types.U16)
}
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, found_widen)
}
@(test)
uint_constraint_infers_boundaries_calls_and_records :: proc(t: ^testing.T) {
directory := "/tmp/brolang-test-uint-constraint"
main_path := "/tmp/brolang-test-uint-constraint/main.bro"
output := "/tmp/brolang-test-uint-constraint-output"
text := `Box :: struct { value uint }
identity func(value uint) uint { return value }
whole func(value int) int { return value }
max_value func() uint { return 18446744073709551615 }
make_box func(value usize) Box { return Box{value = value} }
main func() i32 {
zero uint :: 0
byte uint :: 255
word uint :: 256
word_max uint :: 65535
dword uint :: 65536
maximum uint :: 18446744073709551615
platform usize = 7
c_value c_uint = c_uint(9)
box Box = make_box(7)
signed isize = -1
if (identity(zero) != 0) return 1
if (identity(byte) != 255) return 2
if (identity(word) != 256) return 3
if (identity(word_max) != 65535) return 4
if (identity(dword) != 65536) return 5
if (identity(maximum) != 18446744073709551615) return 6
if (box.value != 7) return 7
if (signed != -1) return 8
if (identity(platform) != 7) return 9
if (identity(c_value) != c_value) return 10
if (whole(word) != 256) return 11
if (max_value() != 18446744073709551615) return 12
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.write_entire_file(main_path, transmute([]byte)text))
testing.expect_value(t, compiler_core.compile_package(directory, output), 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@(test)
uint_constraint_rejects_other_families_and_recovers_records_as_u64 :: proc(t: ^testing.T) {
text := `Unresolved :: struct { value uint }
Conflict :: struct { value uint }
take func(value uint) uint { return value }
bad_result func() uint { return -1 }
bad func(signed i32, decimal f64) void {
_ = take(signed)
_ = take(decimal)
negative uint :: -1
a Conflict = Conflict{value = signed}
_ = negative
_ = a
}
Overflow uint :: 18446744073709551616
NegativeGlobal uint :: -1
main func() void {
bad(1, 1.0)
_ = bad_result()
}
`
source_file := source.Source{path="bad_uint.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)
signed_error := false
float_error := false
negative_error := false
field_error := false
overflow_error := false
global_error := false
result_error := false
for diagnostic in diagnostics.items {
signed_error = signed_error || strings.contains(diagnostic.message, "cannot pass i32 to 'uint' parameter 'value'")
float_error = float_error || strings.contains(diagnostic.message, "cannot pass f64 to 'uint' parameter 'value'")
negative_error = negative_error || strings.contains(diagnostic.message, "could not resolve the 'uint' constraint for local 'negative'")
field_error = field_error || strings.contains(diagnostic.message, "type i32 does not satisfy the 'uint' constraint for field 'Conflict.value'")
overflow_error = overflow_error || strings.contains(diagnostic.message, "magnitude does not fit in u64")
global_error = global_error || strings.contains(diagnostic.message, "could not resolve the 'uint' constraint for global 'NegativeGlobal'")
result_error = result_error || strings.contains(diagnostic.message, "could not resolve a concrete result type for 'bad_result'")
}
unresolved, unresolved_ok := named_record_field_type(&hir_module, &symbols, "Unresolved", "value")
testing.expect(t, signed_error && float_error && negative_error && field_error && overflow_error && global_error && result_error)
testing.expect(t, unresolved_ok)
testing.expect_value(t, unresolved, types.U64)
}
+1 -1
View File
@@ -9,5 +9,5 @@ languages = ["languages/brolang"]
[grammars.brolang]
repository = "file:///Users/valdemar/Developer/Personal/Languages/brolang"
rev = "03f5445509575fb72226e8b4bbf230d7905b7b8e"
rev = "71357b2866d2a558f80b6d5cc65761fb853e74d3"
path = "tree-sitter-brolang"
+2
View File
@@ -35,6 +35,7 @@
(type_declaration name: (identifier) @type)
(function_declaration name: (identifier) @function)
(test_declaration name: (identifier) @function)
(parameter name: (identifier) @variable.parameter)
(intrinsic_call_expression function: (identifier) @function.builtin)
@@ -53,6 +54,7 @@
[
"func"
"test"
"c_func"
"struct"
"c_struct"
+15
View File
@@ -103,6 +103,9 @@ print_usage :: proc() {
fmt.eprintln(
" brolang build [root] (reads root/build.bro; writes root/build/name)",
)
fmt.eprintln(
" brolang test [root] (reads root/build.bro; writes and runs root/build/name-test)",
)
fmt.eprintln(
" brolang new <project-path>",
)
@@ -122,6 +125,10 @@ is_build_command :: proc(arg: string) -> bool {
return arg == "build"
}
is_test_command :: proc(arg: string) -> bool {
return arg == "test"
}
is_new_command :: proc(arg: string) -> bool {
return arg == "new"
}
@@ -609,6 +616,14 @@ main :: proc() {
root := os2.args[2] if len(os2.args) == 3 else ""
os2.exit(compiler.run_build(root))
}
if len(os2.args) >= 2 && is_test_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_tests(root))
}
if len(os2.args) >= 2 && is_new_command(os2.args[1]) {
if len(os2.args) != 3 {
print_usage()
+39
View File
@@ -0,0 +1,39 @@
debug :: import "@std/debug"
Error :: enum {
expectation_failed
}
SourceLocation :: struct {
file []u8
line usize
column usize
}
expect func(condition bool, location SourceLocation) void ! Error {
if (!condition) {
debug.print("{s}:{d}:{d}: expectation failed\n", {location.file, location.line, location.column})
return .expectation_failed
}
}
expect_equal func($T type, expected, actual T, location SourceLocation) void ! Error {
if (expected != actual) {
debug.print("{s}:{d}:{d}: expected {}, found {}\n", {location.file, location.line, location.column, expected, actual})
return .expectation_failed
}
}
run func(name []u8, callback *func() void ! Error) bool {
debug.print("RUN {s}\n", {name,})
callback() catch |_| {
debug.print("FAIL {s}\n", {name,})
return false
}
debug.print("PASS {s}\n", {name,})
return true
}
summary func(passed, failed i32) void {
debug.print("{d} passed, {d} failed\n", {passed, failed})
}
+17 -1
View File
@@ -40,7 +40,9 @@ module.exports = grammar({
_top_level_declaration: $ => choice(
$.import_declaration,
$.test_import_declaration,
$.function_declaration,
$.test_declaration,
$.type_declaration,
$.global_constant_declaration,
$.global_variable_declaration,
@@ -53,6 +55,20 @@ module.exports = grammar({
field('path', $.string),
),
test_import_declaration: $ => seq(
'test',
'import',
repeat($._newline),
field('path', $.string),
),
test_declaration: $ => seq(
field('name', $.identifier),
'test',
repeat($._newline),
field('body', $.block),
),
function_declaration: $ => seq(
optional('hide'),
field('name', $.identifier),
@@ -212,7 +228,7 @@ module.exports = grammar({
_type_constant: $ => seq(optional('-'), choice($.integer, $.character)),
builtin_type: _ => choice(
'void', 'anyopaque', 'bool', 'int', 'float', 'range',
'void', 'anyopaque', 'bool', 'int', 'uint', 'float', 'range',
'i8', 'i16', 'i32', 'i64', 'u8', 'u16', 'u32', 'u64',
'isize', 'usize', 'f32', 'f64',
'c_char', 'c_schar', 'c_uchar', 'c_short', 'c_ushort',
@@ -35,6 +35,7 @@
(type_declaration name: (identifier) @type)
(function_declaration name: (identifier) @function)
(test_declaration name: (identifier) @function)
(parameter name: (identifier) @variable.parameter)
(intrinsic_call_expression function: (identifier) @function.builtin)
@@ -53,6 +54,7 @@
[
"func"
"test"
"c_func"
"struct"
"c_struct"
+72
View File
@@ -26,10 +26,18 @@
"type": "SYMBOL",
"name": "import_declaration"
},
{
"type": "SYMBOL",
"name": "test_import_declaration"
},
{
"type": "SYMBOL",
"name": "function_declaration"
},
{
"type": "SYMBOL",
"name": "test_declaration"
},
{
"type": "SYMBOL",
"name": "type_declaration"
@@ -100,6 +108,66 @@
}
]
},
"test_import_declaration": {
"type": "SEQ",
"members": [
{
"type": "STRING",
"value": "test"
},
{
"type": "STRING",
"value": "import"
},
{
"type": "REPEAT",
"content": {
"type": "SYMBOL",
"name": "_newline"
}
},
{
"type": "FIELD",
"name": "path",
"content": {
"type": "SYMBOL",
"name": "string"
}
}
]
},
"test_declaration": {
"type": "SEQ",
"members": [
{
"type": "FIELD",
"name": "name",
"content": {
"type": "SYMBOL",
"name": "identifier"
}
},
{
"type": "STRING",
"value": "test"
},
{
"type": "REPEAT",
"content": {
"type": "SYMBOL",
"name": "_newline"
}
},
{
"type": "FIELD",
"name": "body",
"content": {
"type": "SYMBOL",
"name": "block"
}
}
]
},
"function_declaration": {
"type": "SEQ",
"members": [
@@ -1408,6 +1476,10 @@
"type": "STRING",
"value": "int"
},
{
"type": "STRING",
"value": "uint"
},
{
"type": "STRING",
"value": "float"
+58
View File
@@ -1852,6 +1852,14 @@
"type": "import_declaration",
"named": true
},
{
"type": "test_declaration",
"named": true
},
{
"type": "test_import_declaration",
"named": true
},
{
"type": "type_declaration",
"named": true
@@ -2000,6 +2008,48 @@
]
}
},
{
"type": "test_declaration",
"named": true,
"fields": {
"body": {
"multiple": false,
"required": true,
"types": [
{
"type": "block",
"named": true
}
]
},
"name": {
"multiple": false,
"required": true,
"types": [
{
"type": "identifier",
"named": true
}
]
}
}
},
{
"type": "test_import_declaration",
"named": true,
"fields": {
"path": {
"multiple": false,
"required": true,
"types": [
{
"type": "string",
"named": true
}
]
}
}
},
{
"type": "tuple_literal",
"named": true,
@@ -2737,6 +2787,10 @@
"type": "struct",
"named": false
},
{
"type": "test",
"named": false
},
{
"type": "true",
"named": false
@@ -2761,6 +2815,10 @@
"type": "u8",
"named": false
},
{
"type": "uint",
"named": false
},
{
"type": "undefined",
"named": true
+141735 -139552
View File
File diff suppressed because it is too large Load Diff
+41 -2
View File
@@ -364,7 +364,30 @@ main func() void {
(expression
(sink))
(expression
(identifier))))))))))
(identifier))))))))))
==================
Native tests
==================
math :: import "../math"
test import "../math"
addition test {}
---
(source_file
(import_declaration
(identifier)
(string
(string_content)))
(test_import_declaration
(string
(string_content)))
(test_declaration
(identifier)
(block)))
==================
Expanded match arms
@@ -373,7 +396,7 @@ Expanded match arms
Kind :: enum { one, two }
Value :: union(enum) { number i32, empty void }
test func(kind Kind, value Value) void {
visit func(kind Kind, value Value) void {
match kind {
.one: {}
expand |tag|: _ = tag
@@ -470,3 +493,19 @@ test func(kind Kind, value Value) void {
(sink))
(expression
(identifier))))))))))))
==================
Unsigned constraint
==================
value uint :: 255
---
(source_file
(global_constant_declaration
(identifier)
(type
(builtin_type))
(expression
(integer))))
@@ -1,6 +1,9 @@
hide helper func() void {}
# <- keyword
value uint :: 1
# ^^^^ type.builtin
main func() void {
_ = sizeof!(i32)
# ^^^^^^ function.builtin