feat: run existing ArrayList with imported C allocator

This commit is contained in:
2026-10-29 19:31:54 +01:00
parent 026fe36325
commit 73684f856a
20 changed files with 875 additions and 54 deletions
+6 -2
View File
@@ -22,7 +22,7 @@ run a source file with the interpreter:
bare `import "../library"` and aliased `library :: import "@library"` declarations load dependencies. relative paths start at the importing package directory; `@` starts at the nearest ancestor containing `build.hon` or `build.bro`, falling back to the entry package directory. package directories are canonicalized through symlinks and loaded once. cycles, missing/empty dependencies and invalid import paths produce source diagnostics. absolute paths and empty, escaped or control-containing import strings are rejected. production loading ignores test-only imports. bare `import "../library"` and aliased `library :: import "@library"` declarations load dependencies. relative paths start at the importing package directory; `@` starts at the nearest ancestor containing `build.hon` or `build.bro`, falling back to the entry package directory. package directories are canonicalized through symlinks and loaded once. cycles, missing/empty dependencies and invalid import paths produce source diagnostics. absolute paths and empty, escaped or control-containing import strings are rejected. production loading ignores test-only imports.
imports bind file-local namespaces, never dependency declarations in the importing package. bare imports use the canonical package directory's final name; directories without a valid identifier name require an explicit alias. sibling files can use the same alias for different packages, but an alias cannot conflict with a package declaration, builtin type or another import in its own file. local shadowing remains forbidden. `library.add(...)` selects a public declaration and uses ordinary signature checking, function values and IR calls without a runtime namespace value. qualified record types and constructors such as `library.Point` and `library.Point { x = 1 }` use the same declaration identity. imported namespaces are not re-exported; missing members are diagnosed. visibility attributes are not implemented; unsupported attributes produce syntax diagnostics. imports bind file-local namespaces, never dependency declarations in the importing package. bare imports use the canonical package directory's final name; directories without a valid identifier name require an explicit alias. sibling files can use the same alias for different packages, but an alias cannot conflict with a package declaration, builtin type or another import in its own file. local shadowing remains forbidden. `library.add(...)` selects a public declaration and uses ordinary signature checking, function values and IR calls without a runtime namespace value. qualified record types and constructors such as `library.Point` and `library.Point { x = 1 }` use the same declaration identity. imported namespaces are not re-exported; missing members are diagnosed. declarations are public by default; `@hide:file` limits a declaration to its originating file, allowing independent private names in sibling files. other visibility attributes produce syntax diagnostics.
```hon ```hon
add proc(left, right int) int { add proc(left, right int) int {
@@ -186,7 +186,9 @@ fallible values use explicit `T ! E` channels, where `E` is an enum, record or t
`memcopy!(destination, source)` copies matching typed slices or pointers to arrays, checking equal lengths and rejecting nonempty overlap. `memset!(destination, value)` repeats complete typed values, including aggregate fields, rather than repeating a byte. destinations require writable elements; empty and zero-sized copies/sets do not access storage. interpreter byte views synchronize with the original typed storage so casts and memory operations preserve aliases. `memcopy!(destination, source)` copies matching typed slices or pointers to arrays, checking equal lengths and rejecting nonempty overlap. `memset!(destination, value)` repeats complete typed values, including aggregate fields, rather than repeating a byte. destinations require writable elements; empty and zero-sized copies/sets do not access storage. interpreter byte views synchronize with the original typed storage so casts and memory operations preserve aliases.
native external declarations use `malloc c_func(size c_ulong) ?*mut anyopaque`, without a body. supported signatures contain concrete integer/float/boolean scalars, data pointers, optional data pointers and `void` results; aggregate C ABI and variadics remain outside this gate. Darwin `c_char`/`c_uchar`, `c_short`/`c_ushort`, `c_int`/`c_uint`, `c_long`/`c_ulong` and `c_longlong`/`c_ulonglong` map to the corresponding 8/16/32/64-bit integer representations. executable checks cover `malloc`, `realloc`, `free` and `posix_memalign` through allocator-style callbacks. foreign calls trap only when reached in the interpreter; arbitrary interpreter FFI is not supported. compiling the existing generic standard library remains a later gate. native external declarations use `malloc c_func(size c_ulong) ?*mut anyopaque`, without a body. supported signatures contain concrete integer/float/boolean scalars, data/function pointers, nullable pointers and `void` results. generated bindings may declare `c_struct` records, nominal pointer-only `opaque` types and `*c_func(...) Result` callback signatures. unused foreign declarations do not force ABI validation or native symbol emission; demanding a variadic or aggregate-by-value signature diagnoses unsupported operands. Darwin `c_char`/`c_uchar`, `c_short`/`c_ushort`, `c_int`/`c_uint`, `c_long`/`c_ulong` and `c_longlong`/`c_ulonglong` map to the corresponding 8/16/32/64-bit integer representations; `c_double` maps to `f64`, while demanded `c_float` and `c_longdouble` types remain unsupported. foreign calls trap only when reached in the interpreter; arbitrary interpreter FFI is not supported.
the existing `std/arraylist` and `std/mem` work without replacement bindings or library code. interpreter/native checks cover initialization, reserve/growth, append/pop/clear, allocation failure without losing the original list, zero-sized elements and deferred cleanup on error. native checks use the real `mem.c_allocator` and imported `ffi/c` declarations for aggregate element storage and over-aligned allocation/reallocation; interpreter allocation checks use a fixed-capacity test allocator, not foreign calls. hash maps, broader metadata operations and formatted IO remain later acceptance gates.
records use `Point :: struct { x i16, y i16 = 2 }`, with comma- or newline-separated fields. named record declarations have nominal identity; two separately declared records are not interchangeable. construct them with `Point { x = 40 }` or a contextual keyed literal, such as `point Point := { x = 40 }`. omitted fields use their declared defaults; missing required fields, duplicate/unknown fields, incompatible values and recursive by-value layouts are diagnosed. explicit initializers run in source order, followed by omitted defaults in declaration order. an explicit value overrides its field's default without evaluating that default. records use `Point :: struct { x i16, y i16 = 2 }`, with comma- or newline-separated fields. named record declarations have nominal identity; two separately declared records are not interchangeable. construct them with `Point { x = 40 }` or a contextual keyed literal, such as `point Point := { x = 40 }`. omitted fields use their declared defaults; missing required fields, duplicate/unknown fields, incompatible values and recursive by-value layouts are diagnosed. explicit initializers run in source order, followed by omitted defaults in declaration order. an explicit value overrides its field's default without evaluating that default.
@@ -238,6 +240,8 @@ the conversion policy is value-preserving by default: any future implicit wideni
explicit numeric conversion uses `T(value)`, such as `u8(index)`, `usize(byte)` or `f64(count)`, with exactly one numeric argument. integer conversions trap with `integer arithmetic overflow` if the value is outside the destination range; float-to-integer conversion truncates toward zero, then checks bounds, rejecting NaN and infinity. integer-to-float conversion permits rounding, including `f64(9007199254740993)`. operands retain their independent types rather than being narrowed by the result context; a positive constant integer conversion operand above `i64`'s maximum defaults to `u64`. boolean conversions, arbitrary-width integers, `f32` and implicit typed-value widening are not implemented. native storage uses LLVM `i8`/`i16`/`i32`/`i64`, including array elements and function parameters/results. explicit numeric conversion uses `T(value)`, such as `u8(index)`, `usize(byte)` or `f64(count)`, with exactly one numeric argument. integer conversions trap with `integer arithmetic overflow` if the value is outside the destination range; float-to-integer conversion truncates toward zero, then checks bounds, rejecting NaN and infinity. integer-to-float conversion permits rounding, including `f64(9007199254740993)`. operands retain their independent types rather than being narrowed by the result context; a positive constant integer conversion operand above `i64`'s maximum defaults to `u64`. boolean conversions, arbitrary-width integers, `f32` and implicit typed-value widening are not implemented. native storage uses LLVM `i8`/`i16`/`i32`/`i64`, including array elements and function parameters/results.
`minval!(T)` and `maxval!(T)` return exact concrete integer bounds, including distinct integer types. `divtrunc!(left, right)` requires matching integer types and truncates toward zero, with ordinary contextual literal inference and single left-to-right operand evaluation. integer division by zero and signed minimum divided by `-1` trap before executing host/LLVM division; `/` remains floating-point division. these builtins also work in checked compile-time execution.
`@std/strconv.parse_float(allocator, text)` parses a complete borrowed `[]u8` decimal into `f64`. it accepts an optional sign, decimal point, and exponent; whitespace, embedded NUL bytes, trailing input, hexadecimal, infinities, and NaNs return `invalid_number`. finite subnormals and signed zero are preserved; overflow and nonzero values rounded to zero return `out_of_range`. allocation failures propagate as `out_of_memory`, and temporary storage is always released. the Darwin backend uses libc's C-locale conversion without changing the process locale; rounding follows the caller's floating-point rounding mode. numeric lowering uses this interface, keeping exact integer-to-float validation compiler-specific. `@std/strconv.parse_float(allocator, text)` parses a complete borrowed `[]u8` decimal into `f64`. it accepts an optional sign, decimal point, and exponent; whitespace, embedded NUL bytes, trailing input, hexadecimal, infinities, and NaNs return `invalid_number`. finite subnormals and signed zero are preserved; overflow and nonzero values rounded to zero return `out_of_range`. allocation failures propagate as `out_of_memory`, and temporary storage is always released. the Darwin backend uses libc's C-locale conversion without changing the process locale; rounding follows the caller's floating-point rounding mode. numeric lowering uses this interface, keeping exact integer-to-float validation compiler-specific.
type errors carry source spans and diagnostic identities; invalid operands propagate their existing identity without duplicate errors, while independent expressions, arguments and nested bodies are still checked. inference cycles without a type anchor are diagnosed. function signatures are checked independently of their bodies. bare `return` and same-line `return expression` are supported inside function bodies and must match the declared result type. nonvoid return-path checking is structural: every path must return or terminate in a diagnostic trap, and both conditional successors are considered without constant folding. a missing `else` or a loop permits fallthrough, even for a literal `true` condition; a later return can close that path. statements after a return and untaken branches or loops are still type-checked but are not executed. type errors carry source spans and diagnostic identities; invalid operands propagate their existing identity without duplicate errors, while independent expressions, arguments and nested bodies are still checked. inference cycles without a type anchor are diagnosed. function signatures are checked independently of their bodies. bare `return` and same-line `return expression` are supported inside function bodies and must match the declared result type. nonvoid return-path checking is structural: every path must return or terminate in a diagnostic trap, and both conditional successors are considered without constant folding. a missing `else` or a loop permits fallthrough, even for a literal `true` condition; a later return can close that path. statements after a return and untaken branches or loops are still type-checked but are not executed.
+8 -3
View File
@@ -53,6 +53,9 @@ the existing `meta.EnumFieldStruct` and `enummap.EnumMap`, `init` and `get` work
general type-valued execution, broader materialization and full library acceptance remain open. general type-valued execution, broader materialization and full library acceptance remain open.
`some!` preserves nested optional presence; ordinary procedures initialize concrete `some!` preserves nested optional presence; ordinary procedures initialize concrete
runtime `undefined` locals and compute fully initialized materializable constants. runtime `undefined` locals and compute fully initialized materializable constants.
the existing `ArrayList` uses nested generic inference, file-local helpers and
the real imported C allocator through native execution; interpreter checks use
non-FFI allocators and cover zero-sized storage, growth, failure and cleanup.
the driver retains frontend storage and source provenance through the driver retains frontend storage and source provenance through
`@source/compilation`, with compilation-wide AST node identities. `@source/compilation`, with compilation-wide AST node identities.
file commands retain single-file behavior; directory commands compile the file commands retain single-file behavior; directory commands compile the
@@ -72,8 +75,9 @@ needs records, pointers, generics and C calls.
- [x] preserve originating filenames and spans in dependency diagnostics and native runtime traps; retain demand-driven diagnostic recovery. - [x] preserve originating filenames and spans in dependency diagnostics and native runtime traps; retain demand-driven diagnostic recovery.
- [x] verify sibling source files, transitive/shared imports, same-spelled declarations in different packages, alias conflicts, missing packages/members, argument errors and dependency-file diagnostics. preserve existing single-file commands. - [x] verify sibling source files, transitive/shared imports, same-spelled declarations in different packages, alias conflicts, missing packages/members, argument errors and dependency-file diagnostics. preserve existing single-file commands.
visibility follows Bro's public-by-default contract; unsupported visibility visibility follows Bro's public-by-default contract; `@hide:file` isolates private
attributes must be diagnosed, never ignored. the scalar-function milestone did not declarations from sibling files and import selection. other unsupported attributes
are diagnosed. the scalar-function milestone did not
include record fields or imported types; those now work for the record/tuple slice. include record fields or imported types; those now work for the record/tuple slice.
test discovery and FFI remain outside these completed slices. test discovery and FFI remain outside these completed slices.
@@ -117,7 +121,8 @@ test discovery and FFI remain outside these completed slices.
- [x] composed inline-match checkpoint: composed error sets expand remaining variants through interpreter/native execution, including nested/repeated/aliased enum, tagged-union and named struct members. all-void sets use one static tag capture; payload-bearing sets use exact immutable by-value payloads and optional static tags, including void members. catch captures, nested inline tag matches, generic calls, owned aggregate copies, deferred cleanup and error returns retain ordinary semantics. constant inputs and type-producing bodies select generated storage with cached nominal identity; struct variant names remain identifiers in generated constants rather than embedding declarations. invalid compositions/captures, narrow payload types, redundant arms, imported dependencies and repeatable compilation retain diagnostics and ownership. member discovery shares the type-control bound; pointer captures and metadata reflection remain open. - [x] composed inline-match checkpoint: composed error sets expand remaining variants through interpreter/native execution, including nested/repeated/aliased enum, tagged-union and named struct members. all-void sets use one static tag capture; payload-bearing sets use exact immutable by-value payloads and optional static tags, including void members. catch captures, nested inline tag matches, generic calls, owned aggregate copies, deferred cleanup and error returns retain ordinary semantics. constant inputs and type-producing bodies select generated storage with cached nominal identity; struct variant names remain identifiers in generated constants rather than embedding declarations. invalid compositions/captures, narrow payload types, redundant arms, imported dependencies and repeatable compilation retain diagnostics and ownership. member discovery shares the type-control bound; pointer captures and metadata reflection remain open.
- [x] type-metadata reflection checkpoint: `typeinfo!(T)` uses the actual `@std/meta` ABI to classify supported types and expose array child/length, immediate distinct backing, record names/tuple/layout and enum/record fields through interpreter/native execution. readonly owned metadata collections retain exact nominal/type leaves, ordinal indices, composed enum identity, checked projections, by-value iteration/copies and compile-time procedure boundaries. ordinary/inline matches select runtime bodies; reflected fields drive `field!`, generics and generated storage/defaults. arity/type/import/ABI failures, match/capture validation, bounds, readonly updates, runtime escape rejection, imported provenance, shared construction bounds and repeatable compilation retain diagnostics and ownership. existing `meta.EnumFieldStruct` and `enummap.EnumMap` generated storage are verified without replacement library code; full `enummap.init/get` is covered below, while general type-valued execution and broader constant materialization remain open. - [x] type-metadata reflection checkpoint: `typeinfo!(T)` uses the actual `@std/meta` ABI to classify supported types and expose array child/length, immediate distinct backing, record names/tuple/layout and enum/record fields through interpreter/native execution. readonly owned metadata collections retain exact nominal/type leaves, ordinal indices, composed enum identity, checked projections, by-value iteration/copies and compile-time procedure boundaries. ordinary/inline matches select runtime bodies; reflected fields drive `field!`, generics and generated storage/defaults. arity/type/import/ABI failures, match/capture validation, bounds, readonly updates, runtime escape rejection, imported provenance, shared construction bounds and repeatable compilation retain diagnostics and ownership. existing `meta.EnumFieldStruct` and `enummap.EnumMap` generated storage are verified without replacement library code; full `enummap.init/get` is covered below, while general type-valued execution and broader constant materialization remain open.
- [x] enum-map acceptance checkpoint: existing `enummap.init/get` run through interpreter/native execution with sparse/aliased/composed enum keys, absent entries and owned aggregate values. contextual `.tag` and aggregate arguments reuse key/value types inferred from rigid generated-map arguments; unresolved generics and rigid conflicts still diagnose. `some!` constructs exact, single-evaluation optional payloads and preserves nested present-null values through compile-time results, cache keys and generated defaults. concrete mutable runtime locals initialize from `undefined`, including nested record/array updates, pointer access and by-value copies; ordinary compile-time procedures materialize fully initialized storage and reject incomplete snapshots. native undefined storage is poison, not zeroed or per-leaf checked. `get` now explicitly returns `null` on lookup fallthrough, preserving the ordinary nonvoid return contract. - [x] enum-map acceptance checkpoint: existing `enummap.init/get` run through interpreter/native execution with sparse/aliased/composed enum keys, absent entries and owned aggregate values. contextual `.tag` and aggregate arguments reuse key/value types inferred from rigid generated-map arguments; unresolved generics and rigid conflicts still diagnose. `some!` constructs exact, single-evaluation optional payloads and preserves nested present-null values through compile-time results, cache keys and generated defaults. concrete mutable runtime locals initialize from `undefined`, including nested record/array updates, pointer access and by-value copies; ordinary compile-time procedures materialize fully initialized storage and reject incomplete snapshots. native undefined storage is poison, not zeroed or per-leaf checked. `get` now explicitly returns `null` on lookup fallthrough, preserving the ordinary nonvoid return contract.
- [ ] remaining gate: general type-valued compile-time execution, broader constant materialization (including untagged unions and storage-bearing values), plus full existing-library acceptance for `ArrayList`, hash maps, broader `meta` operations and `io.print`. required expression-valued control flow, compound updates, formatting parsing and imported C-binding support may interleave with later gates. - [x] array-list acceptance checkpoint: existing `std/arraylist` and `std/mem` initialize, reserve/grow, append/pop/clear and release ordinary/aggregate/zero-sized storage without replacement library code. nested generic inference canonicalizes generated types through pointers and derives exact slice `.ptr` types; rigid conflicts remain diagnostic. `minval!`/`maxval!` preserve integer types and checked `divtrunc!` traps on zero and signed overflow through interpreter/native execution. file-local helpers retain real `@hide:file` visibility, newline `else` preserves match default arms, and repeated `_` parameters remain sinks. generated C records, nominal opaque pointers and foreign callbacks load from existing `ffi/c`; unused unsupported signatures neither diagnose nor emit external symbols, while demanded unsupported ABI/types diagnose. interpreter fixed-capacity allocator checks cover failure without losing storage and deferred error cleanup; native `mem.c_allocator` checks cover aggregate growth and over-aligned allocation/reallocation at `-O0`/`-O2`. arbitrary interpreter FFI, variadic/aggregate C calls and broader library acceptance remain open.
- [ ] remaining gate: general type-valued compile-time execution, broader constant materialization (including untagged unions and storage-bearing values), plus full existing-library acceptance for hash maps, broader `meta` operations and `io.print`. required expression-valued control flow, compound updates, formatting parsing and broader imported C-binding support may interleave with later gates.
- [ ] remaining source constructs: expression-valued control flow and `yield`, compound updates and multiline strings. examples: `source/parser/parse.hon`, `source/llvm/runtime.hon`. - [ ] remaining source constructs: expression-valued control flow and `yield`, compound updates and multiline strings. examples: `source/parser/parse.hon`, `source/llvm/runtime.hon`.
- [ ] standard-library milestone: compile/run a multi-package program exercising existing allocation and IO, including error cleanup; no replacement miniature standard library. - [ ] standard-library milestone: compile/run a multi-package program exercising existing allocation and IO, including error cleanup; no replacement miniature standard library.
- [ ] native driver integration: required C ABI declarations, `process.Init` startup arguments/IO, actual CLI exit statuses, file loading, temporary files and spawning Clang. Honey-generated programs currently accept only parameterless `main` and print its result; Bro's bootstrap startup support is not Honey backend support. - [ ] native driver integration: required C ABI declarations, `process.Init` startup arguments/IO, actual CLI exit statuses, file loading, temporary files and spawning Clang. Honey-generated programs currently accept only parameterless `main` and print its result; Bro's bootstrap startup support is not Honey backend support.
+36
View File
@@ -208,6 +208,10 @@ Node :: struct {
main NodeData = NodeData{ node_id = no_node } main NodeData = NodeData{ node_id = no_node }
data0 NodeData = NodeData{ node_id = no_node } data0 NodeData = NodeData{ node_id = no_node }
data1 NodeData = NodeData{ node_id = no_node } data1 NodeData = NodeData{ node_id = no_node }
file_local bool = false
foreign_type bool = false
variadic bool = false
is_opaque bool = false
} }
@hide:file @hide:file
@@ -1109,6 +1113,38 @@ function_external proc(tree @Tree, id NodeId) bool {
return tree.storage.extra.items[usize(node.data1.extra) - 1] != 0 return tree.storage.extra.items[usize(node.data1.extra) - 1] != 0
} }
set_file_local proc(builder @mut Builder, id NodeId) void {
builder.storage.nodes.items[usize(id)].file_local = true
}
file_local proc(tree @Tree, id NodeId) bool {
return get_node(&tree.storage, id).file_local
}
set_callable_foreign proc(builder @mut Builder, id NodeId) void {
builder.storage.nodes.items[usize(id)].foreign_type = true
}
callable_foreign proc(tree @Tree, id NodeId) bool {
return function_external(tree, id) or get_node(&tree.storage, id).foreign_type
}
set_callable_variadic proc(builder @mut Builder, id NodeId) void {
builder.storage.nodes.items[usize(id)].variadic = true
}
callable_variadic proc(tree @Tree, id NodeId) bool {
return get_node(&tree.storage, id).variadic
}
set_record_opaque proc(builder @mut Builder, id NodeId) void {
builder.storage.nodes.items[usize(id)].is_opaque = true
}
record_opaque proc(tree @Tree, id NodeId) bool {
return get_node(&tree.storage, id).is_opaque
}
function_generic proc(tree @Tree, id NodeId) bool { function_generic proc(tree @Tree, id NodeId) bool {
node :: get_node(&tree.storage, id) node :: get_node(&tree.storage, id)
if (node.kind != .func_decl and node.kind != .expr_function_type) return false if (node.kind != .func_decl and node.kind != .expr_function_type) return false
+54
View File
@@ -738,3 +738,57 @@ dependency_traps_keep_original_spans_and_remain_demand_driven test {
} }
} }
} }
file_local_declarations_are_isolated_from_siblings_and_import_selection test {
callers [4][]u8 :: [
"dep :: import \"../dependency\"\nmain proc() int { return dep.answer() }\n",
"dep :: import \"../dependency\"\nmain proc() int { return dep.sibling() }\n",
"dep :: import \"../dependency\"\nmain proc() int { return dep.leak() }\n",
"dep :: import \"../dependency\"\nmain proc() int { return dep.helper() }\n",
]
for callers |caller, index| {
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
session := init(mem.c_allocator)
defer deinit(&session)
root :: try add_package(&session)
dependency :: try add_package(&session)
root_bytes := try owned_text(caller)
defer arraylist.deinit(&root_bytes)
first_bytes := try owned_text(
`@hide:file
`helper proc() int { return 42 }
`@hide:file secret :: 7
`answer proc() int { return helper() }
,
)
defer arraylist.deinit(&first_bytes)
second_bytes := try owned_text(
`@hide:file
`helper proc() int { return 99 }
`sibling proc() int { return helper() }
`leak proc() int { return secret }
,
)
defer arraylist.deinit(&second_bytes)
root_source :: try add_source(&session, root, "app/main.hon", &root_bytes, &diagnostic_store)
_ = try add_source(&session, dependency, "dependency/first.hon", &first_bytes, &diagnostic_store)
_ = try add_source(&session, dependency, "dependency/second.hon", &second_bytes, &diagnostic_store)
try arraylist.append(&session.imports, Import{
node = declaration(&session, root_source, 0).node,
target = dependency,
})
module := try compile(&session, root, &diagnostic_store)
defer ir.deinit(&module)
outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
defer interpreter.deinit(&outcome)
if (index < 2) {
try testing.expect(outcome.trap == null)
expected :: if (index == 0) i64(42) else i64(99)
try testing.expect_equal(expected, outcome.value.data.integer)
} else {
diagnostic :: diagnostics.get(&diagnostic_store, outcome.trap?)?
try testing.expect_equal(diagnostics.Code.unknown_name, diagnostic.code)
}
}
}
+2
View File
@@ -70,6 +70,7 @@ Code :: enum {
write_before_initialization write_before_initialization
initialization_cycle initialization_cycle
integer_overflow integer_overflow
division_by_zero
import_cycle import_cycle
invalid_import_path invalid_import_path
package_unavailable package_unavailable
@@ -213,6 +214,7 @@ messages :: $enummap.init(Code, []u8, {
write_before_initialization = "local value was assigned before initialization", write_before_initialization = "local value was assigned before initialization",
initialization_cycle = "global initialization cycle", initialization_cycle = "global initialization cycle",
integer_overflow = "integer arithmetic overflow", integer_overflow = "integer arithmetic overflow",
division_by_zero = "integer division by zero",
import_cycle = "package import cycle", import_cycle = "package import cycle",
invalid_import_path = "invalid import path", invalid_import_path = "invalid import path",
package_unavailable = "cannot load package directory", package_unavailable = "cannot load package directory",
+7
View File
@@ -590,6 +590,8 @@ execute proc(
.add, .subtract, .multiply, .divide: { .add, .subtract, .multiply, .divide: {
left :: frames.items[frame_index].values[usize(instruction.left)] left :: frames.items[frame_index].values[usize(instruction.left)]
right :: frames.items[frame_index].values[usize(instruction.right)] right :: frames.items[frame_index].values[usize(instruction.right)]
if (instruction.op == .divide and instruction.type != .float and right.data.unsigned == 0)
return try runtime_trap(diagnostic_store, .division_by_zero, instruction.span)
if instruction.type == .float { if instruction.type == .float {
result f64 := 0 result f64 := 0
match instruction.op { match instruction.op {
@@ -924,6 +926,10 @@ checked_integer proc(op ir.Op, left i64, right i64) ?i64 {
} }
return left * right return left * right
} }
.divide: {
if (left == minimum and right == -1) return null
return divtrunc!(left, right)
}
else: unreachable else: unreachable
} }
} }
@@ -964,6 +970,7 @@ checked_unsigned proc(op ir.Op, left u64, right u64) ?u64 {
if (right != 0 and left > divtrunc!(maxval!(u64), right)) return null if (right != 0 and left > divtrunc!(maxval!(u64), right)) return null
return left * right return left * right
} }
.divide: return divtrunc!(left, right)
else: unreachable else: unreachable
} }
} }
+5 -2
View File
@@ -42,6 +42,8 @@ keywords strmap.StaticStringMap(TokenKind) :: strmap.init([
{ "or", .or }, { "or", .or },
{ "mut", .mut }, { "mut", .mut },
{ "struct", .struct }, { "struct", .struct },
{ "c_struct", .c_struct },
{ "opaque", .opaque },
{ "enum", .enum }, { "enum", .enum },
{ "union", .union }, { "union", .union },
{ "distinct", .distinct }, { "distinct", .distinct },
@@ -169,8 +171,9 @@ scan proc(
if (char == '.' and cursor + 1 < program.len and program[cursor + 1] == '.') { if (char == '.' and cursor + 1 < program.len and program[cursor + 1] == '.') {
inclusive :: cursor + 2 < program.len and program[cursor + 2] == '=' inclusive :: cursor + 2 < program.len and program[cursor + 2] == '='
width :: if (inclusive) 3 else 2 variadic :: cursor + 2 < program.len and program[cursor + 2] == '.'
range_kind :: if (inclusive) .range_inclusive else .range width :: if (inclusive or variadic) 3 else 2
range_kind :: if (inclusive) .range_inclusive else if (variadic) .ellipsis else .range
try add_token(state, Token{ try add_token(state, Token{
span = make_span(source, cursor, cursor + width), span = make_span(source, cursor, cursor + width),
kind = range_kind, kind = range_kind,
+2 -2
View File
@@ -150,10 +150,10 @@ scans_ranges_without_consuming_float_dots test {
state := init(mem.c_allocator) state := init(mem.c_allocator)
defer deinit(&state) defer deinit(&state)
try scan(&state, "0..10 0..=10 1.5..2.5 |@item, _| ... 1.", &pool, &diagnostic_store) try scan(&state, "0..10 0..=10 1.5..2.5 |@item, _| ... 1.", &pool, &diagnostic_store)
expected [19]TokenKind :: [ expected [18]TokenKind :: [
.int, .range, .int, .int, .range_inclusive, .int, .int, .range, .int, .int, .range_inclusive, .int,
.float, .range, .float, .pipe, .at, .ident, .comma, .ident, .pipe, .float, .range, .float, .pipe, .at, .ident, .comma, .ident, .pipe,
.range, .dot, .invalid, .eof, .ellipsis, .invalid, .eof,
] ]
try testing.expect_equal(expected.len, state.tokens.items.len) try testing.expect_equal(expected.len, state.tokens.items.len)
for (expected) |kind, index| try testing.expect_equal(kind, state.tokens.items[index].kind) for (expected) |kind, index| try testing.expect_equal(kind, state.tokens.items[index].kind)
+4 -2
View File
@@ -28,7 +28,9 @@ TokenKind :: enum {
defer, errdefer defer, errdefer
true, false true, false
and, or and, or
struct, enum, union struct, c_struct
opaque
enum, union
distinct, alias distinct, alias
import, test import, test
mut mut
@@ -53,7 +55,7 @@ TokenKind :: enum {
plus, minus plus, minus
star, slash star, slash
range, range_inclusive range, range_inclusive, ellipsis
at, ampersand, caret, dollar at, ampersand, caret, dollar
question, semicolon question, semicolon
pipe pipe
+16
View File
@@ -316,6 +316,22 @@ emit_instruction proc(state @mut State, block @ir.Block, instruction ir.Instruct
else: unreachable else: unreachable
} }
try io.print(" %r{} = {s} double %r{}, %r{}\n", output, {result, opcode, left, right}) try io.print(" %r{} = {s} double %r{}, %r{}\n", output, {result, opcode, left, right})
} else if (instruction.op == .divide) {
bits :: ir.integer_bits(instruction.type)
zero :: fresh(state)
try io.print(" %r{} = icmp eq i{} %r{}, 0\n", output, {zero, bits, right})
try check_failure(state, zero, .division_by_zero, instruction.span)
if (!ir.unsigned_integer(instruction.type)) {
minimum :: fresh(state)
negative_one :: fresh(state)
overflow :: fresh(state)
try io.print(" %r{} = icmp eq i{} %r{}, {}\n", output, {minimum, bits, left, ir.integer_min(instruction.type)})
try io.print(" %r{} = icmp eq i{} %r{}, -1\n", output, {negative_one, bits, right})
try io.print(" %r{} = and i1 %r{}, %r{}\n", output, {overflow, minimum, negative_one})
try check_failure(state, overflow, .integer_overflow, instruction.span)
}
opcode []u8 :: if (ir.unsigned_integer(instruction.type)) "udiv" else "sdiv"
try io.print(" %r{} = {s} i{} %r{}, %r{}\n", output, {result, opcode, bits, left, right})
} else { } else {
opcode []u8 :: match instruction.op { opcode []u8 :: match instruction.op {
.add: "add" .add: "add"
+3 -1
View File
@@ -64,9 +64,10 @@ emit proc(
try bytes_constant(output, enummap.get(diagnostics.Code, []u8, &diagnostics.messages, diagnostic.code)?, false) try bytes_constant(output, enummap.get(diagnostics.Code, []u8, &diagnostics.messages, diagnostic.code)?, false)
try io.write_all(output, "\n") try io.write_all(output, "\n")
} }
memory_codes [6]diagnostics.Code :: [ memory_codes [7]diagnostics.Code :: [
.null_unwrap, .invalid_slice_bounds, .sentinel_mismatch, .null_unwrap, .invalid_slice_bounds, .sentinel_mismatch,
.overlapping_memory_copy, .unsupported_foreign_call, .invalid_pointer_access, .overlapping_memory_copy, .unsupported_foreign_call, .invalid_pointer_access,
.division_by_zero,
] ]
for (memory_codes) |code| { for (memory_codes) |code| {
try io.print("@memory_{s} = private constant ", output, {memory_symbol(code)}) try io.print("@memory_{s} = private constant ", output, {memory_symbol(code)})
@@ -502,6 +503,7 @@ memory_symbol proc(code diagnostics.Code) []u8 {
.overlapping_memory_copy: "overlapping_memory_copy" .overlapping_memory_copy: "overlapping_memory_copy"
.unsupported_foreign_call: "unsupported_foreign_call" .unsupported_foreign_call: "unsupported_foreign_call"
.invalid_pointer_access: "invalid_pointer_access" .invalid_pointer_access: "invalid_pointer_access"
.division_by_zero: "division_by_zero"
else: unreachable else: unreachable
} }
} }
+21 -1
View File
@@ -133,6 +133,7 @@ lower_module proc(
id :: ast.package_decl(tree, i) id :: ast.package_decl(tree, i)
match ast.decl_kind(tree, id) { match ast.decl_kind(tree, id) {
.function: { .function: {
if (ast.function_external(tree, id) and typecheck.type_of(checked, id).kind == .none) continue
function :: try add_function(&module, ast.node_span(tree, id)) function :: try add_function(&module, ast.node_span(tree, id))
storage[usize(id)] = Storage{ kind = .function, index = u32(function) } storage[usize(id)] = Storage{ kind = .function, index = u32(function) }
} }
@@ -157,7 +158,9 @@ lower_module proc(
for (0..ast.package_decl_count(tree)) |i| { for (0..ast.package_decl_count(tree)) |i| {
id :: ast.package_decl(tree, i) id :: ast.package_decl(tree, i)
match ast.decl_kind(tree, id) { match ast.decl_kind(tree, id) {
.function: try lower_function(&state, id) .function: {
if (storage[usize(id)].kind == .function) try lower_function(&state, id)
}
.statement: { .statement: {
if (ast.expr_kind(tree, ast.statement_decl(tree, id).initializer) == .alias_expr) continue if (ast.expr_kind(tree, ast.statement_decl(tree, id).initializer) == .alias_expr) continue
if (typecheck.type_of(checked, id).kind != .type_value) try lower_global(&state, id) if (typecheck.type_of(checked, id).kind != .type_value) try lower_global(&state, id)
@@ -1735,6 +1738,23 @@ lower_builtin proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError
present :: try constant(state, ir.Value{ type = .bool, data = ir.ValueData{ boolean = true } }, span) present :: try constant(state, ir.Value{ type = .bool, data = ir.ValueData{ boolean = true } }, span)
return try record_pair(state, try array_shape(state, type), present, payload, span) return try record_pair(state, try array_shape(state, type), present, payload, span)
} }
if (mem.eql(name, "maxval") or mem.eql(name, "minval")) {
type :: lower_type(state, typecheck.type_of(state.checked, id))
value := ir.Value{ type = type }
if (mem.eql(name, "maxval")) value.data = ir.ValueData{ unsigned = ir.integer_max(type) }
else value.data = ir.ValueData{ integer = ir.integer_min(type) }
return try constant(state, value, span)
}
if (mem.eql(name, "divtrunc")) {
left :: try lower_expression(state, ast.call_arg(tree, id, 0))
if (state.terminated) return ir.no_value
right :: try lower_expression(state, ast.call_arg(tree, id, 1))
if (state.terminated) return ir.no_value
return try emit(state, ir.Instruction{
op = .divide, type = lower_type(state, typecheck.type_of(state.checked, id)),
left = left, right = right, span = span,
})
}
if (mem.eql(name, "sizeof") or mem.eql(name, "alignof")) { if (mem.eql(name, "sizeof") or mem.eql(name, "alignof")) {
checked :: typecheck.type_of(state.checked, ast.call_arg(tree, id, 0)) checked :: typecheck.type_of(state.checked, ast.call_arg(tree, id, 0))
type :: lower_type(state, checked) type :: lower_type(state, checked)
+73
View File
@@ -2014,3 +2014,76 @@ fallible_source_handlers_do_not_catch_runtime_allocation_failures test {
} }
try testing.expect(succeeded and failures > 0) try testing.expect(succeeded and failures > 0)
} }
keeps_generated_foreign_declarations_demand_driven_and_traps_interpreted_calls test {
source ::
`Handle :: opaque
`Packet :: c_struct { context ?*mut Handle, callback ?*c_func(_ c_int, _ c_int) void }
`unsupported c_func(_ Packet) Packet
`variadic c_func(_ c_int, ...) c_int
`single c_func(_ c_float) c_float
`extended c_func(_ c_longdouble) c_longdouble
`double c_func(_ c_double) c_double
`malloc c_func(_ c_ulong) ?*mut anyopaque
`@hide:file
`helper proc() int { if (true) return 42
`else return 0 }
`main proc() int { return helper() }
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
defer interpreter.deinit(&outcome)
try testing.expect(outcome.trap == null)
try testing.expect_equal(i64(42), outcome.value.data.integer)
foreign_module := try compile_owned(
"realloc c_func(_ ?*mut anyopaque, _ c_ulong) ?*mut anyopaque\nmain proc() bool { return realloc(null, 8) == null }\n",
&diagnostic_store,
)
defer ir.deinit(&foreign_module)
foreign_outcome := try interpreter.run(mem.c_allocator, &foreign_module, &diagnostic_store)
defer interpreter.deinit(&foreign_outcome)
try expect_trap(foreign_outcome, &diagnostic_store, .unsupported_foreign_call)
}
diagnoses_demanded_unsupported_foreign_signatures_and_attributes test {
sources [5][]u8 :: [
"take c_func(_ c_int, ...) c_int\nmain proc() c_int { return take(1) }\n",
"Handle :: opaque\ntake c_func(_ Handle) void\nmain proc() void { take(undefined) }\n",
"A :: opaque\nB :: opaque\nmain proc() void { a ?*A :: null\n b ?*B :: a }\n",
"@other:file\nhidden :: 1\nmain proc() int { return hidden }\n",
"@hide:package\nhidden :: 1\nmain proc() int { return hidden }\n",
]
for sources |source| {
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module)
outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
defer interpreter.deinit(&outcome)
try testing.expect(outcome.trap != null)
}
}
rejects_demanded_unavailable_c_floating_types_without_unknown_names test {
sources [2][]u8 :: [
"single c_func() c_float\nmain proc() void { single() }\n",
"extended c_func() c_longdouble\nmain proc() void { extended() }\n",
]
for sources |source| {
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
module := try compile_owned(source, &diagnostic_store)
defer ir.deinit(&module)
outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
defer interpreter.deinit(&outcome)
try expect_trap(outcome, &diagnostic_store, .invalid_operand)
for 0..diagnostics.count(&diagnostic_store) |index| {
diagnostic :: diagnostics.get(&diagnostic_store, diagnostics.DiagnosticId(index))?
try testing.expect(diagnostic.code != .unknown_name)
}
}
}
+59 -21
View File
@@ -114,6 +114,24 @@ parse_package proc(state @mut State) ast.NodeId ! (mem.AllocError | diagnostics.
@hide:file @hide:file
parse_decl proc(state @mut State) ast.NodeId ! ParseError { parse_decl proc(state @mut State) ast.NodeId ! ParseError {
if (next_is(state, .at)) {
start :: try consume(state, .at)
name :: try consume(state, .ident)
_ = try consume(state, .colon)
qualifier :: try consume(state, .ident)
skip_newlines(state)
declaration :: try parse_decl(state)
if (
!mem.eql(state.builder.source[usize(name.span.start)..usize(name.span.end)], "hide")
or !mem.eql(state.builder.source[usize(qualifier.span.start)..usize(qualifier.span.end)], "file")
) {
span :: ast.Span{ start = start.span.start, end = qualifier.span.end, source = start.span.source }
diagnostic :: try diagnostics.add(state.diagnostics, .unexpected_token, span)
return try ast.add_invalid_decl(state.builder, span, diagnostic)
}
ast.set_file_local(state.builder, declaration)
return declaration
}
if ( if (
next_is(state, .import) or next_is(state, .test) next_is(state, .import) or next_is(state, .test)
or (next_is(state, .ident) and next_n_is(state, 1, .double_colon) and next_n_is(state, 2, .import)) or (next_is(state, .ident) and next_n_is(state, 1, .double_colon) and next_n_is(state, 2, .import))
@@ -158,7 +176,7 @@ parse_func_decl proc(state @mut State) ast.NodeId ! ParseError {
params arraylist.ArrayList(ast.NodeId) := arraylist.init(state.allocator) params arraylist.ArrayList(ast.NodeId) := arraylist.init(state.allocator)
defer arraylist.deinit(&params) defer arraylist.deinit(&params)
try parse_parameters(state, &params) variadic :: try parse_parameters(state, &params)
_ = try consume(state, .close_paren) _ = try consume(state, .close_paren)
return_type :: try parse_type(state) return_type :: try parse_type(state)
body :: if (external) try ast.add_block(state.builder, ast.builder_span(state.builder, return_type), &[]) else try parse_block(state) body :: if (external) try ast.add_block(state.builder, ast.builder_span(state.builder, return_type), &[]) else try parse_block(state)
@@ -177,17 +195,24 @@ parse_func_decl proc(state @mut State) ast.NodeId ! ParseError {
body, body,
) )
if (external) ast.set_function_external(state.builder, id) if (external) ast.set_function_external(state.builder, id)
if (variadic) ast.set_callable_variadic(state.builder, id)
return id return id
} }
@hide:file @hide:file
parse_parameters proc( parse_parameters proc(
state @mut State, params @mut arraylist.ArrayList(ast.NodeId), state @mut State, params @mut arraylist.ArrayList(ast.NodeId),
) void ! ParseError { ) bool ! ParseError {
skip_newlines(state) skip_newlines(state)
names arraylist.ArrayList(ParameterName) := arraylist.init(state.allocator) names arraylist.ArrayList(ParameterName) := arraylist.init(state.allocator)
defer arraylist.deinit(&names) defer arraylist.deinit(&names)
while !next_is(state, .close_paren) { while !next_is(state, .close_paren) {
if (next_is(state, .ellipsis)) {
state.next_token += 1
skip_newlines(state)
if (!next_is(state, .close_paren)) return .unexpected_token
return true
}
arraylist.clear(&names) arraylist.clear(&names)
while true { while true {
start :: current(state).span.start start :: current(state).span.start
@@ -221,6 +246,7 @@ parse_parameters proc(
skip_newlines(state) skip_newlines(state)
} }
} }
return false
} }
@hide:file @hide:file
@@ -278,7 +304,7 @@ parse_statement proc(state @mut State, allow_else bool) ast.NodeId ! ParseError
if ( if (
next_is(state, .ident) next_is(state, .ident)
and (next_n_is(state, 1, .ident) and (next_n_is(state, 1, .ident)
or next_n_is(state, 1, .struct) or next_n_is(state, 1, .struct) or next_n_is(state, 1, .c_struct) or next_n_is(state, 1, .opaque)
or next_n_is(state, 1, .enum) or next_n_is(state, 1, .union) or next_n_is(state, 1, .enum) or next_n_is(state, 1, .union)
or next_n_is(state, 1, .distinct) or next_n_is(state, 1, .distinct)
or next_n_is(state, 1, .match) or next_n_is(state, 1, .match)
@@ -298,7 +324,7 @@ parse_statement proc(state @mut State, allow_else bool) ast.NodeId ! ParseError
.break, .continue: return try parse_loop_control(state, allow_else) .break, .continue: return try parse_loop_control(state, allow_else)
.defer, .errdefer: return try parse_cleanup(state, allow_else) .defer, .errdefer: return try parse_cleanup(state, allow_else)
.open_curly: return try ast.add_block_stmt(state.builder, try parse_block(state)) .open_curly: return try ast.add_block_stmt(state.builder, try parse_block(state))
.ident, .int, .float, .string, .true, .false, .minus, .bang, .open_paren, .open_bracket, .struct, .ident, .int, .float, .string, .true, .false, .minus, .bang, .open_paren, .open_bracket, .struct, .c_struct, .opaque, .c_func,
.enum, .union, .distinct, .alias, .dot, .match, .ampersand, .at, .star, .question, .null, .undefined, .try, .dollar: { .enum, .union, .distinct, .alias, .dot, .match, .ampersand, .at, .star, .question, .null, .undefined, .try, .dollar: {
expression :: try parse_expr(state) expression :: try parse_expr(state)
if next_is(state, .equal) return try parse_assignment(state, expression, allow_else) if next_is(state, .equal) return try parse_assignment(state, expression, allow_else)
@@ -406,13 +432,11 @@ parse_if proc(state @mut State) ast.NodeId ! ParseError {
keyword :: try consume(state, .if) keyword :: try consume(state, .if)
condition :: try parse_guard(state) condition :: try parse_guard(state)
capture :: try parse_optional_capture(state) capture :: try parse_optional_capture(state)
braced :: next_is(state, .open_curly)
then_block :: try parse_arm(state, true) then_block :: try parse_arm(state, true)
# only a braced arm permits its else on a later line.
after_then :: state.next_token after_then :: state.next_token
if (braced) skip_newlines(state) skip_newlines(state)
alternative ?ast.NodeId := null alternative ?ast.NodeId := null
if next_is(state, .else) { if (next_is(state, .else) and !next_n_is(state, 1, .colon)) {
state.next_token += 1 state.next_token += 1
alternative = try parse_arm(state, true) alternative = try parse_arm(state, true)
} else { } else {
@@ -744,7 +768,8 @@ parse_type_atom proc(state @mut State) ast.NodeId ! ParseError {
state.next_token += 1 state.next_token += 1
mutable :: next_is(state, .mut) mutable :: next_is(state, .mut)
if (mutable) state.next_token += 1 if (mutable) state.next_token += 1
if (next_is(state, .proc)) return try parse_function_type(state, token.span, mutable or token.kind == .star) if (next_is(state, .proc) or next_is(state, .c_func))
return try parse_function_type(state, token.span, mutable or (token.kind == .star and !next_is(state, .c_func)))
element :: try parse_type_atom(state) element :: try parse_type_atom(state)
return try ast.add_pointer_type(state.builder, ast.Span{ return try ast.add_pointer_type(state.builder, ast.Span{
start = token.span.start, start = token.span.start,
@@ -792,8 +817,14 @@ parse_type_atom proc(state @mut State) ast.NodeId ! ParseError {
count, element, mutable, count, element, mutable,
) )
} }
if next_is(state, .proc) return try parse_function_type(state, current(state).span, false) if (next_is(state, .proc) or next_is(state, .c_func)) return try parse_function_type(state, current(state).span, false)
if next_is(state, .struct) return try parse_record_type(state) if (next_is(state, .struct) or next_is(state, .c_struct)) return try parse_record_type(state)
if (next_is(state, .opaque)) {
token :: try consume(state, .opaque)
id :: try ast.add_record_type(state.builder, token.span, &[])
ast.set_record_opaque(state.builder, id)
return id
}
if next_is(state, .enum) return try parse_enum_type(state) if next_is(state, .enum) return try parse_enum_type(state)
if next_is(state, .union) return try parse_union_type(state) if next_is(state, .union) return try parse_union_type(state)
if next_is(state, .match) return try parse_match(state) if next_is(state, .match) return try parse_match(state)
@@ -824,11 +855,12 @@ parse_type_atom proc(state @mut State) ast.NodeId ! ParseError {
@hide:file @hide:file
parse_function_type proc(state @mut State, start ast.Span, invalid bool) ast.NodeId ! ParseError { parse_function_type proc(state @mut State, start ast.Span, invalid bool) ast.NodeId ! ParseError {
_ = try consume(state, .proc) external :: next_is(state, .c_func)
_ = try consume_either(state, &[.proc, .c_func])
_ = try consume(state, .open_paren) _ = try consume(state, .open_paren)
params := arraylist.init(ast.NodeId, state.allocator) params := arraylist.init(ast.NodeId, state.allocator)
defer arraylist.deinit(&params) defer arraylist.deinit(&params)
try parse_parameters(state, &params) variadic :: try parse_parameters(state, &params)
_ = try consume(state, .close_paren) _ = try consume(state, .close_paren)
result :: try parse_type(state) result :: try parse_type(state)
span :: ast.Span{ span :: ast.Span{
@@ -840,15 +872,18 @@ parse_function_type proc(state @mut State, start ast.Span, invalid bool) ast.Nod
diagnostic :: try diagnostics.add(state.diagnostics, .invalid_pointer_type, span) diagnostic :: try diagnostics.add(state.diagnostics, .invalid_pointer_type, span)
return try ast.add_invalid_expr(state.builder, span, diagnostic) return try ast.add_invalid_expr(state.builder, span, diagnostic)
} }
return try ast.add_function_type(state.builder, span, result, params.items) id :: try ast.add_function_type(state.builder, span, result, params.items)
if (external) ast.set_callable_foreign(state.builder, id)
if (variadic) ast.set_callable_variadic(state.builder, id)
return id
} }
@hide:file @hide:file
starts_type proc(kind lexer.TokenKind) bool { starts_type proc(kind lexer.TokenKind) bool {
return ( return (
kind == .ident or kind == .open_bracket or kind == .struct kind == .ident or kind == .open_bracket or kind == .struct or kind == .c_struct or kind == .opaque
or kind == .enum or kind == .union or kind == .distinct or kind == .alias or kind == .match or kind == .enum or kind == .union or kind == .distinct or kind == .alias or kind == .match
or kind == .at or kind == .star or kind == .question or kind == .open_paren or kind == .proc or kind == .at or kind == .star or kind == .question or kind == .open_paren or kind == .proc or kind == .c_func
) )
} }
@@ -856,7 +891,7 @@ starts_type proc(kind lexer.TokenKind) bool {
member_name proc(kind lexer.TokenKind) bool { member_name proc(kind lexer.TokenKind) bool {
return match kind { return match kind {
.ident, .if, .else, .for, .inline, .while, .proc, .return, .break, .continue, .ident, .if, .else, .for, .inline, .while, .proc, .return, .break, .continue,
.true, .false, .and, .or, .mut, .struct, .enum, .union, .distinct, .true, .false, .and, .or, .mut, .struct, .c_struct, .opaque, .enum, .union, .distinct,
.alias, .match, .import, .test, .c_func, .null, .undefined, .try, .catch, .defer, .errdefer: true .alias, .match, .import, .test, .c_func, .null, .undefined, .try, .catch, .defer, .errdefer: true
else: false else: false
} }
@@ -1126,8 +1161,11 @@ parse_match_arm proc(state @mut State) ast.NodeId ! ParseError {
@hide:file @hide:file
parse_record_type proc(state @mut State) ast.NodeId ! ParseError { parse_record_type proc(state @mut State) ast.NodeId ! ParseError {
keyword :: try consume(state, .struct) keyword :: try consume_either(state, &[.struct, .c_struct])
return try parse_record(state, null, keyword.span.start, true) id :: try parse_record(state, null, keyword.span.start, true)
if (keyword.kind == .c_struct and ast.builder_expr_kind(state.builder, id) == .record_type)
ast.set_record_c_layout(state.builder, id)
return id
} }
@hide:file @hide:file
@@ -1631,8 +1669,8 @@ named_binding_starts proc(state @State) bool {
parse_primary proc(state @mut State) ast.NodeId ! ParseError { parse_primary proc(state @mut State) ast.NodeId ! ParseError {
token :: current(state) token :: current(state)
match token.kind { match token.kind {
.at, .star, .question, .proc: return try parse_type(state) .at, .star, .question, .proc, .c_func, .opaque: return try parse_type(state)
.struct: return try parse_record_type(state) .struct, .c_struct: return try parse_record_type(state)
.enum: return try parse_enum_type(state) .enum: return try parse_enum_type(state)
.union: return try parse_union_type(state) .union: return try parse_union_type(state)
.distinct, .alias: return try parse_type_wrapper(state) .distinct, .alias: return try parse_type_wrapper(state)
+65
View File
@@ -1975,3 +1975,68 @@ rejects_malformed_inline_match_captures test {
try testing.expect(diagnostics.count(&diagnostic_store) > 0) try testing.expect(diagnostics.count(&diagnostic_store) > 0)
} }
} }
parses_generated_foreign_bindings_and_preserves_callable_metadata test {
strings := strpool.init(mem.c_allocator)
defer strpool.deinit(&strings)
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
source ::
`Handle :: opaque
`Record :: c_struct {
` context ?*mut Handle
` callback ?*c_func(_ c_int, _ ?*anyopaque) void
`}
`printf c_func(_ ?*c_char, ...) c_int
`@hide:file
`helper proc() int {
` if (true) return 42
` else return 0
`}
scan_state := lexer.init(mem.c_allocator)
defer lexer.deinit(&scan_state)
try lexer.scan(&scan_state, source, &strings, &diagnostic_store)
tree := try parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store)
defer ast.deinit(&tree)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
try testing.expect_equal(4, ast.package_decl_count(&tree))
handle_type :: ast.statement_decl(&tree, ast.package_decl(&tree, 0)).initializer
record :: ast.statement_decl(&tree, ast.package_decl(&tree, 1)).initializer
try testing.expect(ast.record_opaque(&tree, handle_type))
try testing.expect(ast.record_c_layout(&tree, record))
callback :: ast.type_operand(&tree, ast.field(&tree, ast.record_field(&tree, record, 1)).type?)
try testing.expect(ast.callable_foreign(&tree, callback))
try testing.expect_equal(2, ast.function_param_count(&tree, callback))
try testing.expect(ast.callable_variadic(&tree, ast.package_decl(&tree, 2)))
helper :: ast.package_decl(&tree, 3)
try testing.expect(ast.file_local(&tree, helper))
branch :: ast.if_stmt(&tree, ast.block_statement(&tree, ast.function_decl(&tree, helper).body, 0))
try testing.expect(branch.else_block != null)
}
keeps_match_default_arm_after_newline_single_statement_if test {
strings := strpool.init(mem.c_allocator)
defer strpool.deinit(&strings)
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
source ::
`main proc(value int) void {
` match value {
` 1: if (true) return
` else: return
` }
`}
scan_state := lexer.init(mem.c_allocator)
defer lexer.deinit(&scan_state)
try lexer.scan(&scan_state, source, &strings, &diagnostic_store)
tree := try parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store)
defer ast.deinit(&tree)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
body :: ast.function_decl(&tree, ast.package_decl(&tree, 0)).body
expression :: ast.statement_expr(&tree, ast.block_statement(&tree, body, 0))
try testing.expect_equal(2, ast.match_arm_count(&tree, expression))
first :: ast.match_arm_view(&tree, ast.match_arm(&tree, expression, 0))
branch :: ast.if_stmt(&tree, ast.block_statement(&tree, first.body, 0))
try testing.expect(branch.else_block == null)
try testing.expect(ast.match_arm_view(&tree, ast.match_arm(&tree, expression, 1)).catch_all)
}
+81 -11
View File
@@ -312,7 +312,11 @@ check_elaborated proc(
id :: ast.package_decl(tree, i) id :: ast.package_decl(tree, i)
match ast.decl_kind(tree, id) { match ast.decl_kind(tree, id) {
.statement: try collect_declaration(&state, id) .statement: try collect_declaration(&state, id)
.function: try collect_function(&state, id) .function: {
if (!ast.function_external(tree, id)
or mem.eql(ast.source_text(tree, ast.function_decl(tree, id).name), "main"))
try collect_function(&state, id)
}
.invalid: collect_invalid(&state, id) .invalid: collect_invalid(&state, id)
.import: {} .import: {}
} }
@@ -436,6 +440,12 @@ check_elaborated proc(
try require_numeric(&state, id, expression.left) try require_numeric(&state, id, expression.left)
try require_numeric(&state, id, expression.right) try require_numeric(&state, id, expression.right)
} }
.builtin_call: {
if (mem.eql(builtin_name(&state, id), "divtrunc") and ast.call_arg_count(tree, id) == 2) {
try require_numeric(&state, id, ast.call_arg(tree, id, 0))
try require_numeric(&state, id, ast.call_arg(tree, id, 1))
}
}
else: {} else: {}
} }
} }
@@ -463,6 +473,12 @@ check_elaborated proc(
} }
} }
} }
.builtin_call: {
if (mem.eql(builtin_name(&state, id), "divtrunc") and ast.call_arg_count(tree, id) == 2) {
try numeric_operand(&state, id, ast.call_arg(tree, id, 0))
try numeric_operand(&state, id, ast.call_arg(tree, id, 1))
}
}
else: {} else: {}
} }
} }
@@ -522,9 +538,11 @@ check_elaborated proc(
for (0..ast.package_decl_count(tree)) |i| { for (0..ast.package_decl_count(tree)) |i| {
id :: ast.package_decl(tree, i) id :: ast.package_decl(tree, i)
if (ast.decl_kind(tree, id) != .function or !ast.function_external(tree, id)) continue if (ast.decl_kind(tree, id) != .function or !ast.function_external(tree, id)) continue
if (cells[usize(id)].role == .none) continue
for (0..i) |j| { for (0..i) |j| {
previous :: ast.package_decl(tree, j) previous :: ast.package_decl(tree, j)
if (ast.decl_kind(tree, previous) != .function or !ast.function_external(tree, previous)) continue if (ast.decl_kind(tree, previous) != .function or !ast.function_external(tree, previous)) continue
if (cells[usize(previous)].role == .none) continue
if (ast.name_id(tree, id) == ast.name_id(tree, previous) and !same_signature(&state, id, previous)) { if (ast.name_id(tree, id) == ast.name_id(tree, previous) and !same_signature(&state, id, previous)) {
try fail(&state, id, .type_mismatch, ast.function_decl(tree, id).name) try fail(&state, id, .type_mismatch, ast.function_decl(tree, id).name)
} }
@@ -846,7 +864,7 @@ check_annotation proc(state @mut State, id ast.NodeId, allow_void bool) Type ! (
if ( if (
(element_binding.kind == .builtin (element_binding.kind == .builtin
and (element_binding.data.builtin == .int or element_binding.data.builtin == .float)) and (element_binding.data.builtin == .int or element_binding.data.builtin == .float))
or element.kind == .void or element.kind == .range or element.kind == .void or element.kind == .range or element.kind == .anyopaque
) { ) {
element = invalid_type(try diagnostics.add(state.diagnostics, .invalid_array_element_type, ast.node_span(state.tree, view.element))) element = invalid_type(try diagnostics.add(state.diagnostics, .invalid_array_element_type, ast.node_span(state.tree, view.element)))
state.types[usize(view.element)] = element state.types[usize(view.element)] = element
@@ -879,6 +897,8 @@ check_annotation proc(state @mut State, id ast.NodeId, allow_void bool) Type ! (
.invalid: type = invalid_type(binding.data.diagnostic) .invalid: type = invalid_type(binding.data.diagnostic)
.builtin: { .builtin: {
type.kind = builtin_kind(binding.data.builtin) type.kind = builtin_kind(binding.data.builtin)
if (binding.data.builtin == .unavailable)
type = invalid_type(try diagnostics.add(state.diagnostics, .invalid_operand, ast.node_span(state.tree, id)))
if (type.kind == .anyopaque and state.pointer_depth == 0) { if (type.kind == .anyopaque and state.pointer_depth == 0) {
type = invalid_type(try diagnostics.add(state.diagnostics, .expected_type, ast.node_span(state.tree, id))) type = invalid_type(try diagnostics.add(state.diagnostics, .expected_type, ast.node_span(state.tree, id)))
} }
@@ -893,6 +913,8 @@ check_annotation proc(state @mut State, id ast.NodeId, allow_void bool) Type ! (
initializer :: ast.statement_decl(state.tree, target).initializer initializer :: ast.statement_decl(state.tree, target).initializer
if (type_syntax(ast.expr_kind(state.tree, initializer))) { if (type_syntax(ast.expr_kind(state.tree, initializer))) {
type = try check_type_expression(state, initializer, true) type = try check_type_expression(state, initializer, true)
if (type.kind == .anyopaque and state.pointer_depth == 0)
type = invalid_type(try diagnostics.add(state.diagnostics, .expected_type, ast.node_span(state.tree, id)))
} else { } else {
type = invalid_type(try diagnostics.add(state.diagnostics, .expected_type, ast.node_span(state.tree, id))) type = invalid_type(try diagnostics.add(state.diagnostics, .expected_type, ast.node_span(state.tree, id)))
} }
@@ -952,6 +974,7 @@ collect_declaration proc(state @mut State, id ast.NodeId) void ! (mem.AllocError
@hide:file @hide:file
collect_function proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) { collect_function proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
if (state.cells[usize(id)].role == .function) return
tree :: state.tree tree :: state.tree
function :: ast.function_decl(tree, id) function :: ast.function_decl(tree, id)
state.cells[usize(id)].role = .function state.cells[usize(id)].role = .function
@@ -977,6 +1000,7 @@ collect_function proc(state @mut State, id ast.NodeId) void ! (mem.AllocError |
if (!ast.function_external(tree, id)) try collect_block(state, function.body, function.return_type, false) if (!ast.function_external(tree, id)) try collect_block(state, function.body, function.return_type, false)
else { else {
if (mem.eql(ast.source_text(tree, function.name), "main")) try fail(state, id, .invalid_entry, function.name) if (mem.eql(ast.source_text(tree, function.name), "main")) try fail(state, id, .invalid_entry, function.name)
if (ast.callable_variadic(tree, id)) try fail(state, id, .invalid_operand, function.span)
if (!c_abi_type(state, state.types[usize(function.return_type)])) try fail(state, id, .invalid_operand, function.span) if (!c_abi_type(state, state.types[usize(function.return_type)])) try fail(state, id, .invalid_operand, function.span)
for (0..ast.function_param_count(tree, id)) |i| { for (0..ast.function_param_count(tree, id)) |i| {
parameter :: ast.function_param(tree, id, i) parameter :: ast.function_param(tree, id, i)
@@ -985,6 +1009,19 @@ collect_function proc(state @mut State, id ast.NodeId) void ! (mem.AllocError |
} }
} }
@hide:file
collect_foreign_symbol proc(state @mut State, target ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
if (state.cells[usize(target)].role == .function) return
tree :: state.tree
# declarations of the same external symbol must agree even across package namespaces.
for 0..ast.package_decl_count(tree) |i| {
id :: ast.package_decl(tree, i)
if (ast.decl_kind(tree, id) == .function and ast.function_external(tree, id)
and ast.name_id(tree, id) == ast.name_id(tree, target))
try collect_function(state, id)
}
}
@hide:file @hide:file
collect_capture proc(state @mut State, id, body ast.NodeId) void ! mem.AllocError { collect_capture proc(state @mut State, id, body ast.NodeId) void ! mem.AllocError {
state.cells[usize(id)].role = .loop_capture state.cells[usize(id)].role = .loop_capture
@@ -1252,6 +1289,7 @@ collect_expression proc(state @mut State, id ast.NodeId) void ! (mem.AllocError
} }
if (binding.kind == .declaration) { if (binding.kind == .declaration) {
target :: binding.data.declaration target :: binding.data.declaration
if (ast.function_external(tree, target)) try collect_foreign_symbol(state, target)
role :: state.cells[usize(target)].role role :: state.cells[usize(target)].role
if ( if (
role != .parameter and role != .loop_capture and ast.decl_kind(tree, target) == .statement role != .parameter and role != .loop_capture and ast.decl_kind(tree, target) == .statement
@@ -1441,6 +1479,7 @@ builtin_kind proc(type sema.BuiltinType) TypeKind {
.c_uchar: .u8 .c_uchar: .u8
.c_short: .i16 .c_short: .i16
.c_ushort: .u16 .c_ushort: .u16
.unavailable: .none
} }
} }
@@ -1840,7 +1879,8 @@ addressable_target proc(state @mut State, id ast.NodeId) bool {
@hide:file @hide:file
same_signature proc(state @State, left, right ast.NodeId) bool { same_signature proc(state @State, left, right ast.NodeId) bool {
if (left == right) return true if (left == right) return true
if (ast.function_external(state.tree, left) != ast.function_external(state.tree, right)) return false if (ast.callable_foreign(state.tree, left) != ast.callable_foreign(state.tree, right)) return false
if (ast.callable_variadic(state.tree, left) != ast.callable_variadic(state.tree, right)) return false
tree :: state.tree tree :: state.tree
if (ast.function_param_count(tree, left) != ast.function_param_count(tree, right)) return false if (ast.function_param_count(tree, left) != ast.function_param_count(tree, right)) return false
left_result :: ast.callable_result(tree, left) left_result :: ast.callable_result(tree, left)
@@ -1900,7 +1940,7 @@ assignable_type proc(state @State, actual, expected Type) bool {
a :: state.pointers.items[usize(actual.pointer_type)] a :: state.pointers.items[usize(actual.pointer_type)]
b :: state.pointers.items[usize(expected.pointer_type)] b :: state.pointers.items[usize(expected.pointer_type)]
if (a.many != b.many or (!a.mutable and b.mutable)) return false if (a.many != b.many or (!a.mutable and b.mutable)) return false
if (b.element.kind == .anyopaque) return true if (b.element.kind == .anyopaque and b.element.nominal == ast.NodeId(maxval!(u32))) return true
if (b.mutable) return same_type(state, a.element, b.element) if (b.mutable) return same_type(state, a.element, b.element)
return assignable_type(state, a.element, b.element) return assignable_type(state, a.element, b.element)
} }
@@ -2320,11 +2360,11 @@ check_function_type proc(state @mut State, id ast.NodeId) Type ! (mem.AllocError
if (cached.kind != .none) return cached if (cached.kind != .none) return cached
type := Type{ kind = .function, function = id } type := Type{ kind = .function, function = id }
state.cells[usize(id)].type = type state.cells[usize(id)].type = type
state.pointer_depth += 1
defer state.pointer_depth -= 1
result :: ast.callable_result(state.tree, id) result :: ast.callable_result(state.tree, id)
result_type :: try check_annotation(state, result, true) result_type :: try check_annotation(state, result, true)
if (result_type.kind == .invalid) type = result_type if (result_type.kind == .invalid) type = result_type
if (ast.callable_variadic(state.tree, id) or (ast.callable_foreign(state.tree, id) and !c_abi_type(state, result_type)))
type = invalid_type(try diagnostics.add(state.diagnostics, .invalid_operand, ast.node_span(state.tree, id)))
try depend(state, result, id) try depend(state, result, id)
for 0..ast.function_param_count(state.tree, id) |i| { for 0..ast.function_param_count(state.tree, id) |i| {
parameter :: ast.function_param(state.tree, id, i) parameter :: ast.function_param(state.tree, id, i)
@@ -2334,6 +2374,8 @@ check_function_type proc(state @mut State, id ast.NodeId) Type ! (mem.AllocError
state.cells[usize(parameter)].type = parameter_type state.cells[usize(parameter)].type = parameter_type
state.types[usize(parameter)] = parameter_type state.types[usize(parameter)] = parameter_type
if (parameter_type.kind == .invalid and type.kind != .invalid) type = parameter_type if (parameter_type.kind == .invalid and type.kind != .invalid) type = parameter_type
if (ast.callable_foreign(state.tree, id) and !c_abi_type(state, parameter_type))
type = invalid_type(try diagnostics.add(state.diagnostics, .invalid_operand, ast.node_span(state.tree, parameter)))
try depend(state, annotation, parameter) try depend(state, annotation, parameter)
try depend(state, parameter, id) try depend(state, parameter, id)
} }
@@ -2387,10 +2429,16 @@ check_container_type proc(state @mut State, id ast.NodeId) Type ! (mem.AllocErro
@hide:file @hide:file
c_abi_type proc(state @State, type Type) bool { c_abi_type proc(state @State, type Type) bool {
if (type.kind == .optional) return state.arrays.items[usize(type.array_type)].element.kind == .pointer if (type.kind == .optional) return c_abi_pointer(state, state.arrays.items[usize(type.array_type)].element)
if (type.kind == .function) return ast.callable_foreign(state.tree, type.function) and !ast.callable_variadic(state.tree, type.function)
return integer_kind(type.kind) or type.kind == .float or type.kind == .bool or type.kind == .pointer or type.kind == .void or type.kind == .invalid return integer_kind(type.kind) or type.kind == .float or type.kind == .bool or type.kind == .pointer or type.kind == .void or type.kind == .invalid
} }
@hide:file
c_abi_pointer proc(state @State, type Type) bool {
return type.kind == .pointer or (type.kind == .function and c_abi_type(state, type))
}
@hide:file @hide:file
builtin_name proc(state @State, id ast.NodeId) []u8 { builtin_name proc(state @State, id ast.NodeId) []u8 {
return ast.literal_text(state.tree, ast.builtin_call(state.tree, id).name) return ast.literal_text(state.tree, ast.builtin_call(state.tree, id).name)
@@ -2401,15 +2449,21 @@ collect_builtin proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | d
name :: builtin_name(state, id) name :: builtin_name(state, id)
count :: ast.call_arg_count(state.tree, id) count :: ast.call_arg_count(state.tree, id)
layout :: mem.eql(name, "sizeof") or mem.eql(name, "alignof") layout :: mem.eql(name, "sizeof") or mem.eql(name, "alignof")
limit :: mem.eql(name, "maxval") or mem.eql(name, "minval")
cast :: mem.eql(name, "ptrcast") or mem.eql(name, "constcast") cast :: mem.eql(name, "ptrcast") or mem.eql(name, "constcast")
for (0..count) |i| { for (0..count) |i| {
argument :: ast.call_arg(state.tree, id, i) argument :: ast.call_arg(state.tree, id, i)
if (layout or (cast and count == 2 and i == 0)) { if (layout or limit or (cast and count == 2 and i == 0)) {
_ = try check_annotation(state, argument, true) _ = try check_annotation(state, argument, true)
} else try collect_expression(state, argument) } else try collect_expression(state, argument)
try depend(state, argument, id) try depend(state, argument, id)
} }
if (layout) state.cells[usize(id)].type = Type{ kind = .usize } if (layout) state.cells[usize(id)].type = Type{ kind = .usize }
if (limit and count == 1) state.cells[usize(id)].type = state.types[usize(ast.call_arg(state.tree, id, 0))]
if (mem.eql(name, "divtrunc")) {
state.cells[usize(id)].numeric = true
state.cells[usize(id)].producer = id
}
if (mem.eql(name, "memcopy") or mem.eql(name, "memset")) state.cells[usize(id)].type = Type{ kind = .void } if (mem.eql(name, "memcopy") or mem.eql(name, "memset")) state.cells[usize(id)].type = Type{ kind = .void }
} }
@@ -2483,16 +2537,30 @@ infer_builtin proc(state @mut State, id ast.NodeId, defaults bool) Type ! (mem.A
count :: ast.call_arg_count(state.tree, id) count :: ast.call_arg_count(state.tree, id)
span :: ast.node_span(state.tree, id) span :: ast.node_span(state.tree, id)
layout :: mem.eql(name, "sizeof") or mem.eql(name, "alignof") layout :: mem.eql(name, "sizeof") or mem.eql(name, "alignof")
limit :: mem.eql(name, "maxval") or mem.eql(name, "minval")
division :: mem.eql(name, "divtrunc")
cast :: mem.eql(name, "ptrcast") or mem.eql(name, "constcast") cast :: mem.eql(name, "ptrcast") or mem.eql(name, "constcast")
memory :: mem.eql(name, "memcopy") or mem.eql(name, "memset") memory :: mem.eql(name, "memcopy") or mem.eql(name, "memset")
some :: mem.eql(name, "some") some :: mem.eql(name, "some")
valid_count :: (layout and count == 1) or (cast and (count == 1 or count == 2)) or (memory and count == 2) or (some and count == 1) valid_count :: ((layout or limit) and count == 1) or (cast and (count == 1 or count == 2)) or ((memory or division) and count == 2) or (some and count == 1)
if (!valid_count) { if (!valid_count) {
code diagnostics.Code :: if (layout or cast or memory or some) .argument_count_mismatch else .invalid_operand code diagnostics.Code :: if (layout or limit or division or cast or memory or some) .argument_count_mismatch else .invalid_operand
if (defaults) try fail(state, id, code, span) if (defaults) try fail(state, id, code, span)
return group_type(state, id) return group_type(state, id)
} }
if (layout) return group_type(state, id) if (layout) return group_type(state, id)
if (limit) {
type :: state.types[usize(ast.call_arg(state.tree, id, 0))]
if (defaults and type.kind != .invalid and !integer_kind(type.kind)) try fail(state, id, .invalid_operand, span)
return group_type(state, id)
}
if (division) {
left :: try infer_shape(state, ast.call_arg(state.tree, id, 0), defaults)
right :: try infer_shape(state, ast.call_arg(state.tree, id, 1), defaults)
if (defaults and left.kind != .invalid and right.kind != .invalid
and (!integer_kind(left.kind) or !same_type(state, left, right))) try fail(state, id, .invalid_operand, span)
return group_type(state, id)
}
if (some) { if (some) {
result :: state.cells[root(state, usize(id))].expected result :: state.cells[root(state, usize(id))].expected
if (result.kind != .optional) { if (result.kind != .optional) {
@@ -3359,8 +3427,9 @@ intern_record proc(state @mut State, declaration ast.NodeId, fields []mut Record
c_record_field_type proc(state @State, type Type, depth usize) bool { c_record_field_type proc(state @State, type Type, depth usize) bool {
if (depth >= 128 or type.nominal != ast.NodeId(maxval!(u32))) return false if (depth >= 128 or type.nominal != ast.NodeId(maxval!(u32))) return false
if (numeric_kind(type.kind) or type.kind == .bool or type.kind == .pointer) return true if (numeric_kind(type.kind) or type.kind == .bool or type.kind == .pointer) return true
if (type.kind == .function) return c_abi_type(state, type)
if (type.kind == .optional) if (type.kind == .optional)
return state.arrays.items[usize(type.array_type)].element.kind == .pointer return c_abi_pointer(state, state.arrays.items[usize(type.array_type)].element)
if (type.kind == .enumeration) if (type.kind == .enumeration)
return state.enums.items[usize(type.enum_type)].explicit_backing return state.enums.items[usize(type.enum_type)].explicit_backing
if (type.kind == .array) { if (type.kind == .array) {
@@ -3379,6 +3448,7 @@ c_record_field_type proc(state @State, type Type, depth usize) bool {
@hide:file @hide:file
check_record proc(state @mut State, id ast.NodeId, nominal bool) Type ! (mem.AllocError | diagnostics.CapacityError) { check_record proc(state @mut State, id ast.NodeId, nominal bool) Type ! (mem.AllocError | diagnostics.CapacityError) {
if (ast.record_opaque(state.tree, id)) return Type{ kind = .anyopaque, nominal = id }
cached := state.cells[usize(id)].type cached := state.cells[usize(id)].type
if (cached.kind == .invalid) return cached if (cached.kind == .invalid) return cached
if (!state.cells[usize(id)].record_checking and (cached.kind == .record or cached.kind == .type_value)) { if (!state.cells[usize(id)].record_checking and (cached.kind == .record or cached.kind == .type_value)) {
+22
View File
@@ -18,6 +18,7 @@ BuiltinType :: enum {
c_int, c_uint, c_long, c_ulong, c_longlong, c_ulonglong c_int, c_uint, c_long, c_ulong, c_longlong, c_ulonglong
c_char, c_uchar, c_short, c_ushort c_char, c_uchar, c_short, c_ushort
type type
unavailable
} }
@hide:file @hide:file
@@ -53,9 +54,13 @@ builtins [_]Builtin :: [
{ name = "c_longlong", type = .c_longlong }, { name = "c_longlong", type = .c_longlong },
{ name = "c_ulonglong", type = .c_ulonglong }, { name = "c_ulonglong", type = .c_ulonglong },
{ name = "c_char", type = .c_char }, { name = "c_char", type = .c_char },
{ name = "c_schar", type = .c_char },
{ name = "c_uchar", type = .c_uchar }, { name = "c_uchar", type = .c_uchar },
{ name = "c_short", type = .c_short }, { name = "c_short", type = .c_short },
{ name = "c_ushort", type = .c_ushort }, { name = "c_ushort", type = .c_ushort },
{ name = "c_double", type = .f64 },
{ name = "c_float", type = .unavailable },
{ name = "c_longdouble", type = .unavailable },
{ name = "type", type = .type }, { name = "type", type = .type },
] ]
@@ -194,6 +199,7 @@ resolve_packages proc(
for (0..ast.package_decl_count_at(tree, root)) |i| { for (0..ast.package_decl_count_at(tree, root)) |i| {
id :: ast.package_decl_at(tree, root, i) id :: ast.package_decl_at(tree, root, i)
file_scopes[usize(ast.node_span(tree, id).source)].parent = scope file_scopes[usize(ast.node_span(tree, id).source)].parent = scope
if (ast.file_local(tree, id)) continue
match ast.decl_kind(tree, id) { match ast.decl_kind(tree, id) {
.statement: try predeclare(&state, scope, id, ast.statement_decl(tree, id).name) .statement: try predeclare(&state, scope, id, ast.statement_decl(tree, id).name)
.function: try predeclare(&state, scope, id, ast.function_decl(tree, id).name) .function: try predeclare(&state, scope, id, ast.function_decl(tree, id).name)
@@ -207,6 +213,18 @@ resolve_packages proc(
} }
} }
} }
for (roots) |root| {
for (0..ast.package_decl_count_at(tree, root)) |i| {
id :: ast.package_decl_at(tree, root, i)
if (!ast.file_local(tree, id)) continue
scope :: &file_scopes[usize(ast.node_span(tree, id).source)]
match ast.decl_kind(tree, id) {
.statement: try predeclare(&state, scope, id, ast.statement_decl(tree, id).name)
.function: try predeclare(&state, scope, id, ast.function_decl(tree, id).name)
.import, .invalid: {}
}
}
}
for (imports) |imported| try resolve_import(&state, &file_scopes[usize(ast.node_span(tree, imported.node).source)], imported) for (imports) |imported| try resolve_import(&state, &file_scopes[usize(ast.node_span(tree, imported.node).source)], imported)
for (roots) |root| try resolve_package(&state, root, file_scopes) for (roots) |root| try resolve_package(&state, root, file_scopes)
try canonicalize_aliases(&state) try canonicalize_aliases(&state)
@@ -316,6 +334,10 @@ predeclare proc(
name_span ast.Span, name_span ast.Span,
) void ! (mem.AllocError | diagnostics.CapacityError) { ) void ! (mem.AllocError | diagnostics.CapacityError) {
name :: ast.name_id(state.tree, id) name :: ast.name_id(state.tree, id)
if (mem.eql(ast.source_text(state.tree, name_span), "_")) {
state.bindings[usize(id)] = Binding{ kind = .declaration, data = BindingData{ declaration = id } }
return
}
if hashmap.get(&scope.names, name) |_| { if hashmap.get(&scope.names, name) |_| {
diagnostic :: try diagnostics.add(state.diagnostics, .duplicate_name, name_span) diagnostic :: try diagnostics.add(state.diagnostics, .duplicate_name, name_span)
state.bindings[usize(id)] = Binding{ state.bindings[usize(id)] = Binding{
+45 -7
View File
@@ -4349,6 +4349,21 @@ expression_type proc(state @mut State, id ast.NodeId, depth usize) ast.NodeId !
.literal_bool: return try builtin(state, id, .bool) .literal_bool: return try builtin(state, id, .bool)
.literal_string: return try builtin(state, id, .string) .literal_string: return try builtin(state, id, .string)
.literal_void: return try builtin(state, id, .void) .literal_void: return try builtin(state, id, .void)
.builtin_call: {
name :: ast.literal_text(&state.tree, ast.builtin_call(&state.tree, id).name)
count :: ast.call_arg_count(&state.tree, id)
if ((mem.eql(name, "sizeof") or mem.eql(name, "alignof")) and count == 1)
return try builtin(state, id, .usize)
if ((mem.eql(name, "maxval") or mem.eql(name, "minval")) and count == 1)
return try canonical_type(state, ast.call_arg(&state.tree, id, 0))
if (mem.eql(name, "divtrunc") and count == 2) {
left :: ast.call_arg(&state.tree, id, 0)
right :: ast.call_arg(&state.tree, id, 1)
operand :: if (flexible(state, left)) right else left
return try expression_type(state, operand, depth + 1)
}
return no_node
}
.catch_expr: { .catch_expr: {
type :: try expression_type(state, ast.catch_expr(&state.tree, id).operand, depth + 1) type :: try expression_type(state, ast.catch_expr(&state.tree, id).operand, depth + 1)
if (type == no_node or ast.expr_kind(&state.tree, type) != .fallible_type) return no_node if (type == no_node or ast.expr_kind(&state.tree, type) != .fallible_type) return no_node
@@ -4447,6 +4462,25 @@ expression_type proc(state @mut State, id ast.NodeId, depth usize) ast.NodeId !
) )
) )
return try builtin(state, id, .usize) return try builtin(state, id, .usize)
if (mem.eql(member, "ptr")) {
element := no_node
mutable := false
if (base_kind == .array_type) {
element = ast.array_type(&state.tree, base).element
mutable = ast.array_type(&state.tree, base).mutable
}
if (base_kind == .slice_type) {
element = ast.slice_type(&state.tree, base).element
mutable = ast.slice_type(&state.tree, base).mutable
}
if (binding(state, base).kind == .builtin and binding(state, base).data.builtin == .string)
element = try builtin(state, id, .u8)
if (element != no_node) {
result :: try ast.add_pointer_type(state.builder, view.span, element, mutable, true)
try refresh(state)
return result
}
}
if (base_kind != .record_type and base_kind != .union_type) return no_node if (base_kind != .record_type and base_kind != .union_type) return no_node
slot :: type_record_slot(state, base, view.member) slot :: type_record_slot(state, base, view.member)
if (slot == no_index) return no_node if (slot == no_index) return no_node
@@ -4524,32 +4558,36 @@ infer proc(state @mut State, function, pattern, actual ast.NodeId, arguments []m
resolved :: binding(state, pattern) resolved :: binding(state, pattern)
if (resolved.kind == .declaration and parameter_index(state, function, resolved.data.declaration) != no_index) if (resolved.kind == .declaration and parameter_index(state, function, resolved.data.declaration) != no_index)
return try infer_value(state, function, pattern, Value{ kind = .type, node = actual }, arguments, contextual) return try infer_value(state, function, pattern, Value{ kind = .type, node = actual }, arguments, contextual)
# only normalize the actual type: the pattern may still contain unbound
# parameters or a type-generator call whose arguments must be inferred.
concrete :: try canonical_type(state, actual)
if (concrete == no_node) return false
kind :: ast.expr_kind(&state.tree, pattern) kind :: ast.expr_kind(&state.tree, pattern)
actual_kind :: ast.expr_kind(&state.tree, actual) actual_kind :: ast.expr_kind(&state.tree, concrete)
match kind { match kind {
.pointer_type: { .pointer_type: {
if (actual_kind != .pointer_type) return false if (actual_kind != .pointer_type) return false
return try infer(state, function, ast.pointer_type(&state.tree, pattern).element, return try infer(state, function, ast.pointer_type(&state.tree, pattern).element,
ast.pointer_type(&state.tree, actual).element, arguments, contextual) ast.pointer_type(&state.tree, concrete).element, arguments, contextual)
} }
.slice_type: { .slice_type: {
element := no_node element := no_node
if (actual_kind == .slice_type) element = ast.slice_type(&state.tree, actual).element if (actual_kind == .slice_type) element = ast.slice_type(&state.tree, concrete).element
if (actual_kind == .array_type) element = ast.array_type(&state.tree, actual).element if (actual_kind == .array_type) element = ast.array_type(&state.tree, concrete).element
if (element == no_node) return false if (element == no_node) return false
return try infer(state, function, ast.slice_type(&state.tree, pattern).element, element, arguments, contextual) return try infer(state, function, ast.slice_type(&state.tree, pattern).element, element, arguments, contextual)
} }
.array_type: { .array_type: {
if (actual_kind != .array_type) return false if (actual_kind != .array_type) return false
p :: ast.array_type(&state.tree, pattern) p :: ast.array_type(&state.tree, pattern)
a :: ast.array_type(&state.tree, actual) a :: ast.array_type(&state.tree, concrete)
if (p.count) |count| { if (p.count) |count| {
if (a.count == null or !try infer_value(state, function, count, try evaluate(state, a.count?), arguments, false)) return false if (a.count == null or !try infer_value(state, function, count, try evaluate(state, a.count?), arguments, false)) return false
} }
return try infer(state, function, p.element, a.element, arguments, contextual) return try infer(state, function, p.element, a.element, arguments, contextual)
} }
.optional_type: { .optional_type: {
element :: if (actual_kind == .optional_type) ast.type_operand(&state.tree, actual) else actual element :: if (actual_kind == .optional_type) ast.type_operand(&state.tree, concrete) else concrete
return try infer(state, function, ast.type_operand(&state.tree, pattern), element, arguments, contextual) return try infer(state, function, ast.type_operand(&state.tree, pattern), element, arguments, contextual)
} }
.call: { .call: {
@@ -4558,7 +4596,7 @@ infer proc(state @mut State, function, pattern, actual ast.NodeId, arguments []m
for (0..(state.instances.items.len)) |i| { for (0..(state.instances.items.len)) |i| {
instance :: state.instances.items[i] instance :: state.instances.items[i]
if (instance.type == no_node or instance.function != callee.data.declaration) continue if (instance.type == no_node or instance.function != callee.data.declaration) continue
if (try canonical_type(state, instance.type) != actual) continue if (try canonical_type(state, instance.type) != concrete) continue
count :: ast.call_arg_count(&state.tree, pattern) count :: ast.call_arg_count(&state.tree, pattern)
if (count != instance.arguments.len) return false if (count != instance.arguments.len) return false
for (0..count) |j| { for (0..count) |j| {
+129
View File
@@ -0,0 +1,129 @@
import "@std/mem"
import "@source/ast"
import "@source/diagnostics"
import "@source/strpool"
sema :: import "@source/sema"
typecheck :: import "@source/sema/check"
import "@std/testing"
import "@source/lexer"
import "@source/parser"
@hide:file
expect_specialization proc(
source []u8, expected diagnostics.Code, expected_count usize,
) void ! (mem.AllocError | diagnostics.CapacityError | strpool.InternError | testing.Error) {
strings := strpool.init(mem.c_allocator)
defer strpool.deinit(&strings)
diagnostic_store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&diagnostic_store)
scan_state := lexer.init(mem.c_allocator)
defer lexer.deinit(&scan_state)
try lexer.scan(&scan_state, source, &strings, &diagnostic_store)
builder := ast.init_builder(mem.c_allocator, source)
defer ast.deinit_builder(&builder)
root :: try parser.parse_source(&builder, scan_state.tokens.items, &diagnostic_store)
tree := ast.view(&builder, root)
resolution := try sema.resolve(mem.c_allocator, &tree, &strings, &diagnostic_store)
defer sema.deinit(&resolution)
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
elaborated := try elaborate(mem.c_allocator, &builder, root, &resolution, &strings, &diagnostic_store)
defer deinit(&elaborated)
tree = ast.view(&builder, root)
checked := try typecheck.check_elaborated(mem.c_allocator, &tree, &resolution, elaborated.constraints.items, &diagnostic_store)
defer typecheck.deinit(&checked)
if (expected_count == 0) {
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
for (0..ast.package_decl_count(&tree)) |i| {
declaration :: ast.package_decl(&tree, i)
if (ast.node_class(&tree, declaration) != .function) continue
try testing.expect(!ast.function_generic(&tree, declaration))
for (0..ast.function_param_count(&tree, declaration)) |j|
try testing.expect(!ast.param(&tree, ast.function_param(&tree, declaration, j)).comptime)
}
} else {
count := usize(0)
for (diagnostic_store.list.items) |diagnostic| {
if (diagnostic.code == expected) count += 1
}
try testing.expect_equal(expected_count, count)
}
}
infers_nested_nominal_pointer_parameters_and_readonly_slice_views test {
try expect_specialization(
`Storage proc($T type) type { return struct { items []mut T, capacity usize } }
`make proc($T type, values []mut T) Storage(T) {
` return Storage(T) { items = values, capacity = values.len }
`}
`observe proc($T type, values []T) usize { return values.len }
`reserve proc($T type, storage @mut Storage(T), count usize) void {
` _ = observe(storage.items.ptr[..count])
`}
`append proc($T type, storage @mut Storage(T), value T) void {
` reserve(storage, storage.items.len)
` storage.items[0] = value
`}
`release proc($T type, storage @mut Storage(T)) void {
` allocation :: storage.items.ptr[..storage.capacity]
` _ = observe(allocation)
`}
`forward proc(storage @mut Storage(i16)) void {
` append(storage, 7)
` release(storage)
`}
`main proc() i16 {
` values [2]mut i16 := [1, 2]
` storage := make(i16, values[..])
` forward(&storage)
` append(&storage, 9)
` release(&storage)
` return storage.items[0]
`}
, .cannot_infer_generic, 0,
)
}
rejects_conflicting_rigid_types_and_unrelated_nominal_generators test {
try expect_specialization(
`same proc($T type, left, right T) void {}
`main proc() void { same(i16(1), u8(2)) }
, .cannot_infer_generic, 1,
)
try expect_specialization(
`Box proc($T type) type { return struct { value T } }
`Other proc($T type) type { return struct { value T } }
`read proc($T type, value @Box(T)) void {}
`main proc() void {
` value Other(i16) :: { value = 1 }
` read(&value)
`}
, .cannot_infer_generic, 1,
)
try expect_specialization(
`unknown proc($T type, value usize) void {}
`main proc() void { unknown(1) }
, .cannot_infer_generic, 1,
)
}
preserves_rigid_inference_before_contextual_literals test {
try expect_specialization(
`same proc($T type, left, right T) T { return right }
`main proc() i16 { return same(i16(1), 2) }
, .cannot_infer_generic, 0,
)
}
infers_integer_builtin_results_from_their_type_or_rigid_operand test {
try expect_specialization(
`same proc($T type, left, right T) T { return right }
`main proc() i16 {
` upper :: same(maxval!(i16), i16(1))
` lower :: same(minval!(i16), i16(2))
` left :: same(divtrunc!(i16(8), 2), i16(3))
` right :: same(divtrunc!(8, i16(2)), i16(4))
` return upper + lower + left + right
`}
, .cannot_infer_generic, 0,
)
}
+237 -2
View File
@@ -11,6 +11,66 @@ HONEY = ROOT / "build/honey"
# Explicit expectations guard against frontend bugs shared by both execution paths. # Explicit expectations guard against frontend bugs shared by both execution paths.
CASES = { CASES = {
"integer_divtrunc_preserves_sign_and_single_evaluation": (
"""
next proc(calls @mut i16) i16 { calls^ = calls^ + 1
return calls^ + 5 }
quotient proc(value i16) i16 { return divtrunc!(value, 3) }
main proc() [6]i16 {
calls i16 := 0
result :: divtrunc!(next(&calls), next(&calls))
return [result, calls, quotient(-7), quotient(7),
$(quotient(-7)), divtrunc!(-7, i16(-3))]
}
""", b"[0, 2, -2, 2, -2, 2]\n", None,
),
"integer_divtrunc_rejects_zero": (
"main proc() u64 { return divtrunc!(maxval!(u64), 0) }",
b"", b"integer division by zero",
),
"integer_divtrunc_checks_narrow_signed_overflow": (
"main proc() i8 { return divtrunc!(minval!(i8), -1) }",
b"", b"integer arithmetic overflow",
),
"integer_divtrunc_checks_full_width_signed_overflow": (
"main proc() i64 { return divtrunc!(minval!(i64), -1) }",
b"", b"integer arithmetic overflow",
),
"integer_divtrunc_rejects_float_operands": (
"main proc() f64 { return divtrunc!(7.0, 3.0) }",
b"", b"operator requires compatible operand types",
),
"integer_limits_reject_noninteger_types": (
"main proc() bool { return maxval!(bool) }",
b"", b"operator requires compatible operand types",
),
"integer_divtrunc_rejects_rigid_width_conflicts": (
"main proc() i16 { return divtrunc!(i8(7), i16(3)) }",
b"", b"operator requires compatible operand types",
),
"c_layout_records_and_nominal_opaque_pointers": (
"""
Handle :: opaque
Other :: opaque
Header :: c_struct { tag c_uchar, value c_ulong }
main proc() c_ulong {
header Header := { tag = 2, value = 40 }
if (sizeof!(Header) != 16 or alignof!(Header) != 8) return 0
pointer ?*mut Handle :: ptrcast!(&header)
restored *mut Header :: ptrcast!(pointer?)
return restored.value + c_ulong(restored.tag)
}
""", b"42\n", None,
),
"c_opaque_pointer_types_remain_nominal": (
"""
Left :: opaque
Right :: opaque
consume proc(value ?*Left) bool { return value == null }
main proc() bool { value ?*Right :: null
return consume(value) }
""", b"", b"incompatible value type",
),
"runtime_undefined_nested_storage_and_copy_isolation": ( "runtime_undefined_nested_storage_and_copy_isolation": (
""" """
Item :: struct { values [2]mut i16, label string } Item :: struct { values [2]mut i16, label string }
@@ -6643,6 +6703,17 @@ for integer_type, bits, signed in (
): ):
minimum = -(1 << (bits - 1)) if signed else 0 minimum = -(1 << (bits - 1)) if signed else 0
maximum = (1 << (bits - int(signed))) - 1 maximum = (1 << (bits - int(signed))) - 1
CASES[f"{integer_type}_limits_and_truncating_division"] = (
f"""
identity proc(value {integer_type}) {integer_type} {{ return value }}
main proc() [4]{integer_type} {{
return [identity(minval!({integer_type})), identity(maxval!({integer_type})),
divtrunc!(maxval!({integer_type}), 2), divtrunc!(minval!({integer_type}), 2)]
}}
""",
f"[{minimum}, {maximum}, {maximum // 2}, {-((-minimum) // 2) if minimum < 0 else 0}]\n".encode(),
None,
)
CASES[f"{integer_type}_storage"] = ( CASES[f"{integer_type}_storage"] = (
f""" f"""
seed [2]mut {integer_type} :: [{minimum}, {maximum}] seed [2]mut {integer_type} :: [{minimum}, {maximum}]
@@ -6967,7 +7038,22 @@ main proc() int { return missing }
emitted = run(str(HONEY), "emit-llvm", str(single)) emitted = run(str(HONEY), "emit-llvm", str(single))
assert emitted.returncode == 0 and emitted.stderr == b"", outcome(emitted) assert emitted.returncode == 0 and emitted.stderr == b"", outcome(emitted)
check(single.parent, executable, (b"42\n", b"duplicate name")) check(single.parent, executable, (b"42\n", b"duplicate name"))
return len(cases) + 6 put("hidden/a.hon", "@hide:file\nhelper proc() int { return 20 }\nfirst proc() int { return helper() }\n")
put("hidden/b.hon", "@hide:file\nhelper proc() int { return 22 }\nsecond proc() int { return helper() }\n")
visibility_cases = (
('import "../hidden"\nmain proc() int { return hidden.first() + hidden.second() }\n', b"42\n", None),
('import "../hidden"\nmain proc() int { return hidden.helper() }\n', b"", b"unknown name"),
('@hide:file\nprivate proc() int { return 42 }\nmain proc() int { return private() }\n', b"42\n", None),
('@hide:package\nprivate proc() int { return 42 }\nmain proc() int { return private() }\n', b"", b"unexpected token"),
)
sibling.write_text("sibling proc(n int) int { return n }\n")
for text, stdout, error in visibility_cases:
main.write_text(text)
check(app, executable, (stdout, error))
sibling.write_text("@hide:file\nprivate proc() int { return 42 }\n")
main.write_text("main proc() int { return private() }\n")
check(app, executable, (b"", b"unknown name"))
return len(cases) + len(visibility_cases) + 7
def check_record_packages(temporary, executable): def check_record_packages(temporary, executable):
@@ -7626,6 +7712,153 @@ def check_dependency_provenance(temporary, executable):
executable.unlink() executable.unlink()
return len(cases) * 2 return len(cases) * 2
def check_arraylist_packages(temporary, executable):
project = temporary / "arraylist"
project.mkdir()
(project / "build.hon").write_text("# project root\n")
for package in ("std", "ffi"):
(project / package).symlink_to(ROOT / package, target_is_directory=True)
source = project / "main.hon"
# A fixed-capacity test allocator lets the interpreter exercise the real
# library's growth/failure paths without executing foreign calls.
source.write_text(textwrap.dedent("""
import "@std/arraylist"
import "@std/mem"
State :: struct { storage *mut i16, fail bool = false, allocations usize = 0, releases usize = 0 }
allocate proc(context ?@mut anyopaque, size usize, alignment usize) ?*mut u8 {
state @mut State :: ptrcast!(context?)
state.allocations = state.allocations + 1
if (state.fail or size > sizeof!([64]i16) or alignment > alignof!(i16)) return null
return ptrcast!(state.storage)
}
resize proc(
context ?@mut anyopaque, _ ?*mut u8, _ usize, size usize, alignment usize,
) ?*mut u8 { return allocate(context, size, alignment) }
release proc(context ?@mut anyopaque, _ ?*mut u8, _ usize, _ usize) void {
state @mut State :: ptrcast!(context?)
state.releases = state.releases + 1
}
table mem.AllocatorVTable :: { alloc = allocate, realloc = resize, free = release }
fail proc(allocator mem.Allocator) void ! mem.AllocError {
list := arraylist.init(i16, allocator)
defer arraylist.deinit(&list)
try arraylist.append(&list, 42)
return .out_of_memory
}
main proc() i16 ! mem.AllocError {
storage [64]mut i16 := undefined
state State := { storage = (&storage).ptr }
allocator mem.Allocator :: { context = &state, vtable = &table }
list := arraylist.init(i16, allocator)
if (list.items.len != 0 or list.capacity != 0 or state.allocations != 0) return -1
if (arraylist.pop(&list) != null) return -2
for (i16(0)..20) |value| { try arraylist.append(&list, value) }
if (list.items.len != 20 or list.capacity < 20) return -3
for list.items |value, index| { if (value != i16(index)) return -4 }
previous_capacity :: list.capacity
previous_calls :: state.allocations
try arraylist.reserve(&list, previous_capacity)
if (state.allocations != previous_calls) return -5
state.fail = true
arraylist.reserve(&list, previous_capacity + 1) catch |err| {
if (err != .out_of_memory) return -6
}
if (list.capacity != previous_capacity or list.items.len != 20 or list.items[19] != 19) return -7
if (state.allocations != previous_calls + 1) return -8
if (arraylist.pop(&list)? != 19 or list.items.len != 19) return -9
arraylist.clear(&list)
if (list.items.len != 0 or list.capacity != previous_capacity) return -10
state.fail = false
try arraylist.append(&list, 42)
if (list.items[0] != 42 or state.allocations != previous_calls + 1) return -11
arraylist.deinit(&list)
if (list.items.len != 0 or list.capacity != 0 or state.releases != 1) return -12
arraylist.deinit(&list)
if (state.releases != 1) return -13
fail(allocator) catch {}
if (state.releases != 2) return -14
state.fail = true
arraylist.append(&list, 1) catch {}
if (list.items.len != 0 or list.capacity != 0) return -15
return 42
}
""").strip() + "\n")
check(source, executable, (b"42\n", None))
source.write_text(textwrap.dedent("""
import "@std/arraylist"
import "@std/mem"
Empty :: struct {}
main proc() i16 ! mem.AllocError {
list := arraylist.init(Empty, mem.c_allocator)
defer arraylist.deinit(&list)
try arraylist.reserve(&list, 20)
for (0..21) |_| { try arraylist.append(&list, Empty{}) }
if (list.items.len != 21 or list.capacity < 21) return -1
if (arraylist.pop(&list) == null or list.items.len != 20) return -2
arraylist.clear(&list)
if (list.items.len != 0 or arraylist.pop(&list) != null) return -3
return 42
}
""").strip() + "\n")
check(source, executable, (b"42\n", None))
native_cases = (
"""
import "@std/arraylist"
import "@std/mem"
Item :: struct { number i16, values [2]mut i16 }
main proc() i16 ! mem.AllocError {
list := arraylist.init(Item, mem.c_allocator)
defer arraylist.deinit(&list)
for (i16(0)..32) |value| {
try arraylist.append(&list, Item{ number = value, values = [value, 2] })
}
if (list.items.len != 32 or list.capacity < 32) return -1
saved :: list.items[20]
list.items[20].values[0] = 0
if (saved.values[0] != 20) return -2
for list.items |item, index| { if (item.number != i16(index)) return -3 }
if (arraylist.pop(&list)?.number != 31) return -4
arraylist.clear(&list)
try arraylist.append(&list, saved)
return list.items[0].number + list.items[0].values[0] + list.items[0].values[1]
}
""",
"""
import "@std/mem"
main proc() i16 {
allocator :: mem.c_allocator
if (mem.raw_alloc(allocator, 16, 3) != null) return -1
initial :: mem.raw_alloc(allocator, 16, 64)
if initial |bytes| {
values *mut i16 :: ptrcast!(bytes)
values[0] = 40
values[1] = 2
resized :: mem.raw_realloc(allocator, bytes, 16, 128, 64)
if resized |grown| {
defer mem.raw_free(allocator, grown, 128, 64)
result *i16 :: ptrcast!(grown)
return result[0] + result[1]
}
mem.raw_free(allocator, bytes, 16, 64)
}
return -2
}
""",
)
for text in native_cases:
source.write_text(textwrap.dedent(text).strip() + "\n")
interpreted = run(str(HONEY), "run", str(source))
assert interpreted.returncode == 1 and interpreted.stdout == b"", outcome(interpreted)
assert b"foreign function calls are unsupported by the interpreter" in interpreted.stderr, outcome(interpreted)
assert len(interpreted.stderr.splitlines()) == 1, outcome(interpreted)
for optimization in ("-O0", "-O2"):
built = run(str(HONEY), "build", str(source), "-o", str(executable), optimization)
assert outcome(built) == (0, b"", b""), outcome(built)
assert outcome(run(str(executable))) == (0, b"42\n", b""), optimization
executable.unlink()
return 2, len(native_cases)
def check_allocator_libc(temporary, executable): def check_allocator_libc(temporary, executable):
source = temporary / "allocator_libc.hon" source = temporary / "allocator_libc.hon"
source.write_text(textwrap.dedent(""" source.write_text(textwrap.dedent("""
@@ -8299,6 +8532,8 @@ def main():
check(source, executable, (stdout, error)) check(source, executable, (stdout, error))
count += 1 count += 1
allocator_count = check_allocator_libc(temporary, executable) allocator_count = check_allocator_libc(temporary, executable)
arraylist_count, arraylist_native_count = check_arraylist_packages(temporary, executable)
count += arraylist_count
count += check_packages(temporary, executable) count += check_packages(temporary, executable)
count += check_namespace_packages(temporary, executable) count += check_namespace_packages(temporary, executable)
count += check_record_packages(temporary, executable) count += check_record_packages(temporary, executable)
@@ -8325,7 +8560,7 @@ def main():
assert not executable.exists() assert not executable.exists()
invalid = run(str(HONEY), "build", str(source), "-o", str(executable), "-O3") invalid = run(str(HONEY), "build", str(source), "-o", str(executable), "-O3")
assert invalid.returncode == 2 and b"usage:" in invalid.stderr, outcome(invalid) assert invalid.returncode == 2 and b"usage:" in invalid.stderr, outcome(invalid)
print(f"{count} programs matched interpreter at -O0 and -O2; {allocator_count} native allocator program, LLVM output and CLI errors passed") print(f"{count} programs matched interpreter at -O0 and -O2; {allocator_count + arraylist_native_count} native allocator programs, LLVM output and CLI errors passed")
if __name__ == "__main__": if __name__ == "__main__":