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, - `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 leading underscores are ordinary identifier characters, and imports are always file-local
- relative `.h` imports as synthetic C header package namespaces - 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 - root `main` validation with trap executable recovery for missing or unusable entry points
### scalar, aggregate, and pointer types ### 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 - 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 - 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)` - 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 #### 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 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 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 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` 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, 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 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. anonymous generated records still require concrete runtime types.
#### keyword member names #### 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 - `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` - 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/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 ### 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 - static, eager runtime, mutable runtime, and deferred problematic globals with cycle diagnostics
- demand-driven LLVM declarations for referenced foreign functions - 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 - 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 - replaceable dynamically loaded libclang C-import backend
- C-header import caching by canonical path, target, include paths, and defines - 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 directory. Lists take the address of an array literal; empty lists are written
`&[]`. See `examples/build/` for runnable projects. `&[]`. 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: Relative `.h` imports create synthetic package namespaces backed by libclang:
```bro ```bro
+16
View File
@@ -897,6 +897,22 @@
- `tag!` reads or folds a tagged union discriminant, while `tagname!` turns a comptime-known enum - `tag!` reads or folds a tagged union discriminant, while `tagname!` turns a comptime-known enum
value into its immutable field-name string 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 ## A word on unchecked casts
For casts that bypass safety checks, Honey provides builtin functions: For casts that bypass safety checks, Honey provides builtin functions:
+8
View File
@@ -227,6 +227,7 @@ Function :: struct {
c_abi: bool, c_abi: bool,
imported: bool, imported: bool,
generated: bool, generated: bool,
test: bool,
file_hidden: bool, file_hidden: bool,
has_body: bool, has_body: bool,
variadic: bool, variadic: bool,
@@ -276,6 +277,7 @@ Import :: struct {
target: Package_Id, target: Package_Id,
valid: bool, valid: bool,
used: bool, used: bool,
test_only: bool,
diagnostic: source.Diagnostic_Id, diagnostic: source.Diagnostic_Id,
} }
@@ -310,9 +312,15 @@ Package :: struct {
path: string, path: string,
name: symbol.Id, name: symbol.Id,
available: bool, available: bool,
test: bool,
kind: Package_Kind, kind: Package_Kind,
} }
Compile_Mode :: enum u8 {
Executable,
Test,
}
Package_Kind :: enum u8 { Package_Kind :: enum u8 {
Native, Native,
C_Header, C_Header,
+69 -24
View File
@@ -43,26 +43,7 @@ destroy_build_config :: proc(cfg: ^BuildConfig) {
delete(cfg.c_options.defines) delete(cfg.c_options.defines)
} }
// run_build implements `brolang build [root]`: it loads and type-checks load_build_config :: proc(project_root: string) -> (BuildConfig, bool) {
// `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() sources := source.init_store()
defer source.destroy_store(&sources) defer source.destroy_store(&sources)
diagnostics := source.init_store_diagnostics(&sources) diagnostics := source.init_store_diagnostics(&sources)
@@ -73,7 +54,7 @@ run_build :: proc(root: string) -> int {
arena: vmem.Arena arena: vmem.Arena
if err := vmem.arena_init_growing(&arena); err != nil { if err := vmem.arena_init_growing(&arena); err != nil {
fmt.eprintln("failed to initialize build arena:", err) fmt.eprintln("failed to initialize build arena:", err)
return 2 return {}, false
} }
defer vmem.arena_destroy(&arena) defer vmem.arena_destroy(&arena)
a := vmem.arena_allocator(&arena) a := vmem.arena_allocator(&arena)
@@ -82,7 +63,7 @@ run_build :: proc(root: string) -> int {
if !loaded { if !loaded {
source.print_all(&diagnostics) source.print_all(&diagnostics)
fmt.eprintln("failed to load build root:", project_root) 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 // check needs no `main`: it synthesizes a trap main and emits one benign
// "missing main" diagnostic, which is expected for build.bro. Suppress that // "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) hir_module := checker.check(&ast_module, &diagnostics, &symbols, target.DEFAULT, a)
if build_bro_has_errors(&diagnostics) { if build_bro_has_errors(&diagnostics) {
source.print_all(&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 return 2
} }
defer if owned {delete(project_root)}
cfg, ok := extract_build_config(&hir_module, &symbols) cfg, ok := load_build_config(project_root)
if !ok { if !ok {
return 2 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) 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 { valid_output_name :: proc(name: string) -> bool {
return len(name) > 0 && name != "." && name != ".." && return len(name) > 0 && name != "." && name != ".." &&
!strings.contains(name, "/") && !strings.contains(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") strings.write_string(builder, "enum")
case .Alias, .Distinct, .Named: case .Alias, .Distinct, .Named:
write_type_label(checker, builder, item.child) 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)) strings.write_string(builder, types.name(value))
} }
} }
@@ -1052,6 +1052,26 @@ fits_i64 :: proc(value: i128) -> bool {
return fits_signed_type(value, types.I64) 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( type_from_syntax :: proc(
checker: ^Checker, checker: ^Checker,
value: ast.Type_Syntax, value: ast.Type_Syntax,
@@ -1386,9 +1406,20 @@ build_symbol_indexes :: proc(checker: ^Checker) {
} }
slice.sort_by(checker.global_index, global_index_less) 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 { 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) 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) 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 // 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. // 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 { if declared == types.FLOAT {
return types.F64 return types.F64
} }
@@ -3613,13 +3644,21 @@ record_field_expr_candidate :: proc(
} }
expr := checker.ast_module.exprs[expr_id] expr := checker.ast_module.exprs[expr_id]
constraint := checker.record_field_constraints[slot] constraint := checker.record_field_constraints[slot]
if constant := eval_integer_constant_in_context(checker, expr_id, pkg, file); if !numeric_operand_is_open(checker, expr_id, locals, pkg, file) {
constant.kind == .Value && fits_i64(constant.value) { concrete := types.constraint_target(constraint, inferred, &checker.module.types)
candidate := types.smallest_signed_for_literal(i64(constant.value)) if is_runtime_type(checker, concrete) {
if constraint == types.FLOAT { return merge_record_field_demand(checker, slot, concrete, expr.span)
candidate = types.F64 }
}
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) { if is_float_constant_expr(checker, expr_id) {
return merge_record_field_default(checker, slot, types.F64, expr.span) 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) { if types.is_constraint(current) {
switch constraint { checker.module.types.fields[slot].type = constraint_recovery_type(checker, 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)
}
} }
} }
} }
@@ -3988,7 +4020,7 @@ find_spec :: proc(
} }
// specialized_param_type maps a parameter's declared type to its monomorphized // 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 // 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. // passing an out-of-family argument fails to specialize and is rejected.
specialized_param_type :: proc( specialized_param_type :: proc(
@@ -4415,14 +4447,16 @@ infer_expr :: proc(
if expr.kind != .Name || symbol.is_valid(expr.qualifier) || !static_name { if expr.kind != .Name || symbol.is_valid(expr.qualifier) || !static_name {
constant = eval_constant(checker, frame.expr) constant = eval_constant(checker, frame.expr)
} }
if constant.kind == .Overflow || constant.kind == .Div_By_Zero || if constant.kind == .Overflow || constant.kind == .Div_By_Zero {
(constant.kind == .Value && !fits_i64(constant.value)) {
last = types.I64 last = types.I64
_ = pop(&stack) _ = pop(&stack)
continue continue
} }
if constant.kind == .Value { 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) _ = pop(&stack)
continue continue
} }
@@ -5055,7 +5089,7 @@ infer_statements :: proc(
declared_local := resolve_inferred_array(checker, type_from_syntax(checker, statement.type, pkg, file), statement.expr) declared_local := resolve_inferred_array(checker, type_from_syntax(checker, statement.type, pkg, file), statement.expr)
value_type := types.INVALID value_type := types.INVALID
if !is_undefined_expr(checker, statement.expr) { 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) 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) declared_local = resolve_inferred_array_from_type(checker, declared_local, value_type)
} }
@@ -5071,7 +5105,8 @@ infer_statements :: proc(
const_val := i128(0) const_val := i128(0)
if !is_runtime_type(checker, declared_local) && !is_undefined_expr(checker, statement.expr) { if !is_runtime_type(checker, declared_local) && !is_undefined_expr(checker, statement.expr) {
constant := eval_integer_constant_in_context(checker, statement.expr, pkg, file) 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 open = true
const_val = constant.value const_val = constant.value
} else if is_float_constant_expr(checker, statement.expr) { } else if is_float_constant_expr(checker, statement.expr) {
@@ -5112,11 +5147,25 @@ infer_statements :: proc(
expected_assignment := types.INVALID expected_assignment := types.INVALID
if statement.target != ast.INVALID_EXPR { if statement.target != ast.INVALID_EXPR {
expected_assignment = infer_expr(checker, statement.target, locals^[:], pkg, file, demanded, local_types) 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 { } else if statement.name != checker.sink_symbol {
if local_index, ok := find_infer_local_index(locals^[:], statement.name); ok { 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 { } 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) 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 { if function.pkg == 0 && function.name == checker.main_symbol && function.result == types.INT {
declared = types.I32 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: [dynamic]Infer_Local
locals.allocator = checker.allocator locals.allocator = checker.allocator
@@ -5604,7 +5653,7 @@ open_const_default_type :: proc(
if index, ok := find_infer_local_index(locals, expr.name); ok { if index, ok := find_infer_local_index(locals, expr.name); ok {
local := locals[index] local := locals[index]
if local.open_const { 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 { if local.open_float {
return types.F64 return types.F64
@@ -5622,7 +5671,9 @@ open_const_default_type :: proc(
return types.INVALID return types.INVALID
} }
if checker.global_open_const[index] { 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] { if checker.global_open_float[index] {
return types.F64 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) 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_open_const[index] = true
checker.global_const_value[index] = constant.value checker.global_const_value[index] = constant.value
} else if is_float_constant_expr(checker, global.expr) { } 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), type_from_syntax(checker, global.type, global.pkg, global.file),
global.expr, 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) inferred := infer_expr(checker, global.expr, nil, global.pkg, global.file, expected=expected)
if resolved := resolve_inferred_array_from_type(checker, declared, inferred); if resolved := resolve_inferred_array_from_type(checker, declared, inferred);
resolved != declared { resolved != declared {
@@ -6009,7 +6061,8 @@ infer_all :: proc(checker: ^Checker) {
continue continue
} }
if checker.global_open_const[index] { 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 defaulted = true
} else if checker.global_open_float[index] { } else if checker.global_open_float[index] {
checker.global_types[index] = types.F64 checker.global_types[index] = types.F64
@@ -6383,7 +6436,7 @@ build_constant_expr :: proc(
return invalid_hir_expr(checker, expr.span, id, recovery_type) return invalid_hir_expr(checker, expr.span, id, recovery_type)
} }
if constant.kind == .Overflow || 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( id := source.add(
checker.diagnostics, checker.diagnostics,
expr.span, expr.span,
@@ -6396,7 +6449,16 @@ build_constant_expr :: proc(
if constant.value >= 0 && constant.value <= i128(0xffff_ffff_ffff_ffff) { if constant.value >= 0 && constant.value <= i128(0xffff_ffff_ffff_ffff) {
value = transmute(i64)u64(constant.value) 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 types.is_concrete_integer(expected) {
if !fits_integer_type(constant.value, expected, checker.target) { if !fits_integer_type(constant.value, expected, checker.target) {
id := source.addf( id := source.addf(
@@ -12364,11 +12426,12 @@ build_globals :: proc(checker: ^Checker) {
if is_runtime_type(checker, declared) { if is_runtime_type(checker, declared) {
expected = declared expected = declared
} else if constant := eval_integer_constant_in_context(checker, global.expr, global.pkg, global.file); } 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]) { is_runtime_type(checker, checker.global_types[global_index]) {
// Open integer constant: build against its demanded/defaulted type. Gated // Open integer constant: build against its demanded/defaulted type. Gated
// to the infer-side open-constant condition so out-of-range constants keep // 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] expected = checker.global_types[global_index]
} else if (is_float_constant_expr(checker, global.expr) || } else if (is_float_constant_expr(checker, global.expr) ||
is_numeric_arithmetic_expr(checker, global.expr)) && is_numeric_arithmetic_expr(checker, global.expr)) &&
@@ -12390,6 +12453,18 @@ build_globals :: proc(checker: ^Checker) {
global_type = types.I64 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) { if !global.immutable && is_undefined_expr(checker, global.expr) {
diagnostic = source.add( diagnostic = source.add(
checker.diagnostics, checker.diagnostics,
@@ -12874,7 +12949,7 @@ check :: proc(
main_template := find_template(&checker, checker.main_symbol, 0) main_template := find_template(&checker, checker.main_symbol, 0)
main_declarations := 0 main_declarations := 0
for function in ast_module.functions { 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 main_declarations += 1
} }
} }
@@ -12917,7 +12992,7 @@ check :: proc(
delete(states, allocator) delete(states, allocator)
propagate_problems(&checker) propagate_problems(&checker)
for import_item in ast_module.imports { 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)) 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 package compiler
import "./ast"
import "./backend" import "./backend"
import "./cimport" import "./cimport"
import "./checker" import "./checker"
@@ -34,6 +35,7 @@ compile_package :: proc(
selected := target.DEFAULT, selected := target.DEFAULT,
c_options := cimport.Options{}, c_options := cimport.Options{},
project_root := "", project_root := "",
mode := ast.Compile_Mode.Executable,
) -> int { ) -> int {
sources := source.init_store() sources := source.init_store()
defer source.destroy_store(&sources) defer source.destroy_store(&sources)
@@ -77,12 +79,18 @@ compile_package :: proc(
c_options, c_options,
selected, selected,
project_root if len(project_root) > 0 else input_path, project_root if len(project_root) > 0 else input_path,
mode,
) )
if !loaded { if !loaded {
source.print_all(&diagnostics) source.print_all(&diagnostics)
fmt.eprintln("failed to load root package directory:", input_path) fmt.eprintln("failed to load root package directory:", input_path)
return 2 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 // 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 // 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 { keyword_kind :: proc(text: string) -> token.Kind {
switch text { switch text {
case "test": return .Keyword_Test
case "func": return .Keyword_Func case "func": return .Keyword_Func
case "c_func": return .Keyword_C_Func case "c_func": return .Keyword_C_Func
case "struct": return .Keyword_Struct case "struct": return .Keyword_Struct
@@ -51,6 +52,7 @@ keyword_kind :: proc(text: string) -> token.Kind {
case "anyopaque": return .Keyword_Anyopaque case "anyopaque": return .Keyword_Anyopaque
case "bool": return .Keyword_Bool case "bool": return .Keyword_Bool
case "int": return .Keyword_Int case "int": return .Keyword_Int
case "uint": return .Keyword_Uint
case "float": return .Keyword_Float case "float": return .Keyword_Float
case "range": return .Keyword_Range case "range": return .Keyword_Range
case "i8": return .Keyword_I8 case "i8": return .Keyword_I8
+56 -22
View File
@@ -26,6 +26,7 @@ State :: struct {
c_options: cimport.Options, c_options: cimport.Options,
selected: target.Target, selected: target.Target,
project_root: string, project_root: string,
mode: ast.Compile_Mode,
record_identities: [dynamic]string, record_identities: [dynamic]string,
record_types: [dynamic]types.Type, record_types: [dynamic]types.Type,
root_failed: bool, root_failed: bool,
@@ -1130,7 +1131,42 @@ load_header :: proc(state: ^State, path: string, import_span: source.Span) -> as
return pkg_id 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) canonical, ok := filepath.abs(path, state.allocator)
if !ok || !os.is_dir(path) { if !ok || !os.is_dir(path) {
if is_root { if is_root {
@@ -1152,6 +1188,10 @@ load_package :: proc(state: ^State, path: string, import_span: source.Span, is_r
return id return id
} }
if existing := find_package(state, canonical); existing != ast.INVALID_PACKAGE { 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) delete(canonical, state.allocator)
return existing return existing
} }
@@ -1161,6 +1201,7 @@ load_package :: proc(state: ^State, path: string, import_span: source.Span, is_r
path=canonical, path=canonical,
name=symbol.intern(state.symbols, filepath.base(canonical)), name=symbol.intern(state.symbols, filepath.base(canonical)),
available=true, available=true,
test=state.mode == .Test && is_root || include_tests,
}) })
files, files_ok := read_package_files(state, canonical) files, files_ok := read_package_files(state, canonical)
if !files_ok { 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) os.file_info_slice_delete(files, state.allocator)
import_count := len(state.module.imports) resolve_package_imports(state, pkg_id)
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)
}
return 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) { validate_imports :: proc(state: ^State) {
for import_item, import_id in state.module.imports { 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 { 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 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 { find_type_import :: proc(module: ^ast.Module, file: ast.File_Id, alias: symbol.Id) -> ast.Import_Id {
for import_item, index in module.imports { 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) return ast.import_id(index)
} }
} }
@@ -1879,6 +1904,7 @@ load :: proc(
c_options := cimport.Options{}, c_options := cimport.Options{},
selected := target.DEFAULT, selected := target.DEFAULT,
project_root_path := "", project_root_path := "",
mode := ast.Compile_Mode.Executable,
) -> (ast.Module, bool) { ) -> (ast.Module, bool) {
module := ast.init_module(allocator) module := ast.init_module(allocator)
project_root_source := project_root_path if len(project_root_path) > 0 else root_path 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, c_options=c_options,
selected=selected, selected=selected,
project_root=project_root, project_root=project_root,
mode=mode,
} }
state.record_identities.allocator = allocator state.record_identities.allocator = allocator
state.record_types.allocator = allocator state.record_types.allocator = allocator
@@ -1911,6 +1938,13 @@ load :: proc(
if root != ast.Package_Id(0) && root != ast.INVALID_PACKAGE { if root != ast.Package_Id(0) && root != ast.INVALID_PACKAGE {
state.root_failed = true 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_imports(&state)
validate_declaration_aliases(&state) validate_declaration_aliases(&state)
resolve_enum_values(&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 { is_type_token :: proc(kind: token.Kind) -> bool {
#partial switch kind { #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_U8, .Keyword_U16, .Keyword_U32, .Keyword_U64,
.Keyword_Isize, .Keyword_Usize, .Keyword_F32, .Keyword_F64, .Keyword_Isize, .Keyword_Usize, .Keyword_F32, .Keyword_F64,
.Keyword_C_Char, .Keyword_C_Schar, .Keyword_C_Uchar, .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: case .Keyword_Int:
advance(parser) advance(parser)
return types.INT return types.INT
case .Keyword_Uint:
advance(parser)
return types.UINT
case .Keyword_Float: case .Keyword_Float:
advance(parser) advance(parser)
return types.FLOAT 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 { parse_primary :: proc(parser: ^Parser, nesting: int) -> ast.Expr_Id {
tok := current(parser) tok := current(parser)
#partial switch tok.kind { #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 start := tok
target := parse_type_atom(parser) target := parse_type_atom(parser)
return add_expr(parser, ast.Expr{ 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) 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) skip_newlines(parser)
path_token := current(parser) path_token := current(parser)
if path_token.kind != .String { if path_token.kind != .String {
@@ -3018,6 +3021,7 @@ parse_import :: proc(parser: ^Parser, alias: token.Token, start: token.Token) {
file=parser.file, file=parser.file,
target=ast.INVALID_PACKAGE, target=ast.INVALID_PACKAGE,
valid=false, valid=false,
test_only=test_only,
diagnostic=source.INVALID_DIAGNOSTIC, diagnostic=source.INVALID_DIAGNOSTIC,
}) })
_ = finish_statement(parser) _ = finish_statement(parser)
@@ -3033,13 +3037,45 @@ parse_import :: proc(parser: ^Parser, alias: token.Token, start: token.Token) {
file=parser.file, file=parser.file,
target=ast.INVALID_PACKAGE, target=ast.INVALID_PACKAGE,
valid=true, valid=true,
test_only=test_only,
diagnostic=source.INVALID_DIAGNOSTIC, diagnostic=source.INVALID_DIAGNOSTIC,
}) })
_ = finish_statement(parser) _ = 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) { parse_top_level :: proc(parser: ^Parser) {
hide_token, file_hidden := allow(parser, .Keyword_Hide) 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 current(parser).kind == .Keyword_Import {
if file_hidden { if file_hidden {
source.add(parser.diagnostics, hide_token.span, "imports are already file-local and cannot use 'hide'") 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 return
} }
name := advance(parser) 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 { if current(parser).kind == .Colon_Colon {
saved := parser.cursor saved := parser.cursor
advance(parser) 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, Right_Brace,
Comma, Comma,
Pipe, Pipe,
Keyword_Test,
Keyword_Func, Keyword_Func,
Keyword_C_Func, Keyword_C_Func,
Keyword_Struct, Keyword_Struct,
@@ -87,6 +88,7 @@ Kind :: enum u8 {
Keyword_Anyopaque, Keyword_Anyopaque,
Keyword_Bool, Keyword_Bool,
Keyword_Int, Keyword_Int,
Keyword_Uint,
Keyword_Float, Keyword_Float,
Keyword_Range, Keyword_Range,
Keyword_I8, Keyword_I8,
@@ -118,7 +120,7 @@ Kind :: enum u8 {
} }
is_keyword :: proc(kind: Kind) -> bool { is_keyword :: proc(kind: Kind) -> bool {
return kind >= .Keyword_Func && kind <= .Keyword_C_Longdouble return kind >= .Keyword_Test && kind <= .Keyword_C_Longdouble
} }
Token :: struct { Token :: struct {
+23 -1
View File
@@ -44,6 +44,7 @@ BOOL :: Type(29)
FLOAT :: Type(30) FLOAT :: Type(30)
RANGE :: Type(31) RANGE :: Type(31)
ANYOPAQUE :: Type(32) ANYOPAQUE :: Type(32)
UINT :: Type(33)
DYNAMIC_START :: Type(64) DYNAMIC_START :: Type(64)
@@ -59,6 +60,7 @@ Kind :: enum u8 {
Void, Void,
Anyopaque, Anyopaque,
Int_Constraint, Int_Constraint,
Uint_Constraint,
Float_Constraint, Float_Constraint,
Range_Constraint, Range_Constraint,
Scalar, Scalar,
@@ -595,6 +597,8 @@ kind :: proc(value: Type, store: ^Store = nil) -> Kind {
return .Anyopaque return .Anyopaque
case INT: case INT:
return .Int_Constraint return .Int_Constraint
case UINT:
return .Uint_Constraint
case FLOAT: case FLOAT:
return .Float_Constraint return .Float_Constraint
case RANGE: case RANGE:
@@ -642,7 +646,7 @@ is_bool :: proc(value: Type) -> bool {
} }
is_constraint :: 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 // 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 { switch constraint {
case INT: case INT:
return inferred if is_concrete_integer(inferred) else INVALID return inferred if is_concrete_integer(inferred) else INVALID
case UINT:
return inferred if is_unsigned(inferred) else INVALID
case FLOAT: case FLOAT:
if is_float(inferred) { if is_float(inferred) {
return inferred return inferred
@@ -673,6 +679,8 @@ constraint_accepts :: proc(constraint, concrete: Type, store: ^Store = nil) -> b
switch constraint { switch constraint {
case INT: case INT:
return is_concrete_integer(concrete) return is_concrete_integer(concrete)
case UINT:
return is_unsigned(concrete)
case FLOAT: case FLOAT:
return is_float(concrete) return is_float(concrete)
case RANGE: case RANGE:
@@ -1686,6 +1694,19 @@ smallest_signed_for_literal :: proc(value: i64) -> Type {
return I64 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 { name :: proc(value: Type) -> string {
switch value { switch value {
case INVALID: return "<invalid>" case INVALID: return "<invalid>"
@@ -1693,6 +1714,7 @@ name :: proc(value: Type) -> string {
case ANYOPAQUE: return "anyopaque" case ANYOPAQUE: return "anyopaque"
case BOOL: return "bool" case BOOL: return "bool"
case INT: return "int" case INT: return "int"
case UINT: return "uint"
case FLOAT: return "float" case FLOAT: return "float"
case RANGE: return "range" case RANGE: return "range"
case I8: return "i8" case I8: return "i8"
+286 -1
View File
@@ -1485,6 +1485,7 @@ main func() void {
_ = maxval!(u8, u16) _ = maxval!(u8, u16)
_ = minval!(1) _ = minval!(1)
_ = maxval!(int) _ = maxval!(int)
_ = maxval!(uint)
_ = maxval!(f32) _ = maxval!(f32)
_ = maxval!(bool) _ = maxval!(bool)
_ = maxval!(Named) _ = maxval!(Named)
@@ -1513,7 +1514,7 @@ main func() void {
} }
testing.expect_value(t, bad_arity, 2) testing.expect_value(t, bad_arity, 2)
testing.expect(t, bad_type) testing.expect(t, bad_type)
testing.expect_value(t, bad_target, 5) testing.expect_value(t, bad_target, 6)
} }
@(test) @(test)
@@ -12778,3 +12779,287 @@ main func() void {
testing.expect(t, types.is_slice(nested_field, &hir_module.types)) 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) 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] [grammars.brolang]
repository = "file:///Users/valdemar/Developer/Personal/Languages/brolang" repository = "file:///Users/valdemar/Developer/Personal/Languages/brolang"
rev = "03f5445509575fb72226e8b4bbf230d7905b7b8e" rev = "71357b2866d2a558f80b6d5cc65761fb853e74d3"
path = "tree-sitter-brolang" path = "tree-sitter-brolang"
+2
View File
@@ -35,6 +35,7 @@
(type_declaration name: (identifier) @type) (type_declaration name: (identifier) @type)
(function_declaration name: (identifier) @function) (function_declaration name: (identifier) @function)
(test_declaration name: (identifier) @function)
(parameter name: (identifier) @variable.parameter) (parameter name: (identifier) @variable.parameter)
(intrinsic_call_expression function: (identifier) @function.builtin) (intrinsic_call_expression function: (identifier) @function.builtin)
@@ -53,6 +54,7 @@
[ [
"func" "func"
"test"
"c_func" "c_func"
"struct" "struct"
"c_struct" "c_struct"
+15
View File
@@ -103,6 +103,9 @@ print_usage :: proc() {
fmt.eprintln( fmt.eprintln(
" brolang build [root] (reads root/build.bro; writes root/build/name)", " 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( fmt.eprintln(
" brolang new <project-path>", " brolang new <project-path>",
) )
@@ -122,6 +125,10 @@ is_build_command :: proc(arg: string) -> bool {
return arg == "build" return arg == "build"
} }
is_test_command :: proc(arg: string) -> bool {
return arg == "test"
}
is_new_command :: proc(arg: string) -> bool { is_new_command :: proc(arg: string) -> bool {
return arg == "new" return arg == "new"
} }
@@ -609,6 +616,14 @@ main :: proc() {
root := os2.args[2] if len(os2.args) == 3 else "" root := os2.args[2] if len(os2.args) == 3 else ""
os2.exit(compiler.run_build(root)) 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) >= 2 && is_new_command(os2.args[1]) {
if len(os2.args) != 3 { if len(os2.args) != 3 {
print_usage() 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( _top_level_declaration: $ => choice(
$.import_declaration, $.import_declaration,
$.test_import_declaration,
$.function_declaration, $.function_declaration,
$.test_declaration,
$.type_declaration, $.type_declaration,
$.global_constant_declaration, $.global_constant_declaration,
$.global_variable_declaration, $.global_variable_declaration,
@@ -53,6 +55,20 @@ module.exports = grammar({
field('path', $.string), 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( function_declaration: $ => seq(
optional('hide'), optional('hide'),
field('name', $.identifier), field('name', $.identifier),
@@ -212,7 +228,7 @@ module.exports = grammar({
_type_constant: $ => seq(optional('-'), choice($.integer, $.character)), _type_constant: $ => seq(optional('-'), choice($.integer, $.character)),
builtin_type: _ => choice( builtin_type: _ => choice(
'void', 'anyopaque', 'bool', 'int', 'float', 'range', 'void', 'anyopaque', 'bool', 'int', 'uint', 'float', 'range',
'i8', 'i16', 'i32', 'i64', 'u8', 'u16', 'u32', 'u64', 'i8', 'i16', 'i32', 'i64', 'u8', 'u16', 'u32', 'u64',
'isize', 'usize', 'f32', 'f64', 'isize', 'usize', 'f32', 'f64',
'c_char', 'c_schar', 'c_uchar', 'c_short', 'c_ushort', 'c_char', 'c_schar', 'c_uchar', 'c_short', 'c_ushort',
@@ -35,6 +35,7 @@
(type_declaration name: (identifier) @type) (type_declaration name: (identifier) @type)
(function_declaration name: (identifier) @function) (function_declaration name: (identifier) @function)
(test_declaration name: (identifier) @function)
(parameter name: (identifier) @variable.parameter) (parameter name: (identifier) @variable.parameter)
(intrinsic_call_expression function: (identifier) @function.builtin) (intrinsic_call_expression function: (identifier) @function.builtin)
@@ -53,6 +54,7 @@
[ [
"func" "func"
"test"
"c_func" "c_func"
"struct" "struct"
"c_struct" "c_struct"
+72
View File
@@ -26,10 +26,18 @@
"type": "SYMBOL", "type": "SYMBOL",
"name": "import_declaration" "name": "import_declaration"
}, },
{
"type": "SYMBOL",
"name": "test_import_declaration"
},
{ {
"type": "SYMBOL", "type": "SYMBOL",
"name": "function_declaration" "name": "function_declaration"
}, },
{
"type": "SYMBOL",
"name": "test_declaration"
},
{ {
"type": "SYMBOL", "type": "SYMBOL",
"name": "type_declaration" "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": { "function_declaration": {
"type": "SEQ", "type": "SEQ",
"members": [ "members": [
@@ -1408,6 +1476,10 @@
"type": "STRING", "type": "STRING",
"value": "int" "value": "int"
}, },
{
"type": "STRING",
"value": "uint"
},
{ {
"type": "STRING", "type": "STRING",
"value": "float" "value": "float"
+58
View File
@@ -1852,6 +1852,14 @@
"type": "import_declaration", "type": "import_declaration",
"named": true "named": true
}, },
{
"type": "test_declaration",
"named": true
},
{
"type": "test_import_declaration",
"named": true
},
{ {
"type": "type_declaration", "type": "type_declaration",
"named": true "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", "type": "tuple_literal",
"named": true, "named": true,
@@ -2737,6 +2787,10 @@
"type": "struct", "type": "struct",
"named": false "named": false
}, },
{
"type": "test",
"named": false
},
{ {
"type": "true", "type": "true",
"named": false "named": false
@@ -2761,6 +2815,10 @@
"type": "u8", "type": "u8",
"named": false "named": false
}, },
{
"type": "uint",
"named": false
},
{ {
"type": "undefined", "type": "undefined",
"named": true "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 (expression
(sink)) (sink))
(expression (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 Expanded match arms
@@ -373,7 +396,7 @@ Expanded match arms
Kind :: enum { one, two } Kind :: enum { one, two }
Value :: union(enum) { number i32, empty void } Value :: union(enum) { number i32, empty void }
test func(kind Kind, value Value) void { visit func(kind Kind, value Value) void {
match kind { match kind {
.one: {} .one: {}
expand |tag|: _ = tag expand |tag|: _ = tag
@@ -470,3 +493,19 @@ test func(kind Kind, value Value) void {
(sink)) (sink))
(expression (expression
(identifier)))))))))))) (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 {} hide helper func() void {}
# <- keyword # <- keyword
value uint :: 1
# ^^^^ type.builtin
main func() void { main func() void {
_ = sizeof!(i32) _ = sizeof!(i32)
# ^^^^^^ function.builtin # ^^^^^^ function.builtin