From 73684f856a96bc3490927d0932d67fd3c498f85e Mon Sep 17 00:00:00 2001 From: hl-valdemar Date: Thu, 29 Oct 2026 19:31:54 +0100 Subject: [PATCH] feat: run existing ArrayList with imported C allocator --- README.md | 8 +- TODO.md | 11 +- source/ast/ast.hon | 36 ++++ source/compilation/compilation.test.hon | 54 +++++ source/diagnostics/diagnostics.hon | 2 + source/interpreter/interpreter.hon | 7 + source/lexer/scan.hon | 7 +- source/lexer/scan.test.hon | 4 +- source/lexer/token.hon | 6 +- source/llvm/instructions.hon | 16 ++ source/llvm/llvm.hon | 4 +- source/lower/lower.hon | 22 +- source/lower/lower.test.hon | 73 +++++++ source/parser/parse.hon | 80 +++++-- source/parser/parse.test.hon | 65 ++++++ source/sema/check/check.hon | 92 +++++++- source/sema/resolve.hon | 22 ++ source/sema/specialize/specialize.hon | 52 ++++- source/sema/specialize/specialize.test.hon | 129 +++++++++++ tests/native.py | 239 ++++++++++++++++++++- 20 files changed, 875 insertions(+), 54 deletions(-) create mode 100644 source/sema/specialize/specialize.test.hon diff --git a/README.md b/README.md index 0de080a..4c44d5e 100644 --- a/README.md +++ b/README.md @@ -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. -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 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. -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. @@ -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. +`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. 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. diff --git a/TODO.md b/TODO.md index 4bb7910..0e205e4 100644 --- a/TODO.md +++ b/TODO.md @@ -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. `some!` preserves nested optional presence; ordinary procedures initialize concrete 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 `@source/compilation`, with compilation-wide AST node identities. 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] 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 -attributes must be diagnosed, never ignored. the scalar-function milestone did not +visibility follows Bro's public-by-default contract; `@hide:file` isolates private +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. 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] 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. - - [ ] 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`. - [ ] 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. diff --git a/source/ast/ast.hon b/source/ast/ast.hon index fe1b602..89e81c6 100644 --- a/source/ast/ast.hon +++ b/source/ast/ast.hon @@ -208,6 +208,10 @@ Node :: struct { main NodeData = NodeData{ node_id = no_node } data0 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 @@ -1109,6 +1113,38 @@ function_external proc(tree @Tree, id NodeId) bool { 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 { node :: get_node(&tree.storage, id) if (node.kind != .func_decl and node.kind != .expr_function_type) return false diff --git a/source/compilation/compilation.test.hon b/source/compilation/compilation.test.hon index dbbf065..0ffa4e4 100644 --- a/source/compilation/compilation.test.hon +++ b/source/compilation/compilation.test.hon @@ -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) + } + } +} diff --git a/source/diagnostics/diagnostics.hon b/source/diagnostics/diagnostics.hon index d6a5026..1dc9451 100644 --- a/source/diagnostics/diagnostics.hon +++ b/source/diagnostics/diagnostics.hon @@ -70,6 +70,7 @@ Code :: enum { write_before_initialization initialization_cycle integer_overflow + division_by_zero import_cycle invalid_import_path package_unavailable @@ -213,6 +214,7 @@ messages :: $enummap.init(Code, []u8, { write_before_initialization = "local value was assigned before initialization", initialization_cycle = "global initialization cycle", integer_overflow = "integer arithmetic overflow", + division_by_zero = "integer division by zero", import_cycle = "package import cycle", invalid_import_path = "invalid import path", package_unavailable = "cannot load package directory", diff --git a/source/interpreter/interpreter.hon b/source/interpreter/interpreter.hon index 1685ffc..f5336b2 100644 --- a/source/interpreter/interpreter.hon +++ b/source/interpreter/interpreter.hon @@ -590,6 +590,8 @@ execute proc( .add, .subtract, .multiply, .divide: { left :: frames.items[frame_index].values[usize(instruction.left)] 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 { result f64 := 0 match instruction.op { @@ -924,6 +926,10 @@ checked_integer proc(op ir.Op, left i64, right i64) ?i64 { } return left * right } + .divide: { + if (left == minimum and right == -1) return null + return divtrunc!(left, right) + } 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 return left * right } + .divide: return divtrunc!(left, right) else: unreachable } } diff --git a/source/lexer/scan.hon b/source/lexer/scan.hon index 8b8647b..1f804c7 100644 --- a/source/lexer/scan.hon +++ b/source/lexer/scan.hon @@ -42,6 +42,8 @@ keywords strmap.StaticStringMap(TokenKind) :: strmap.init([ { "or", .or }, { "mut", .mut }, { "struct", .struct }, + { "c_struct", .c_struct }, + { "opaque", .opaque }, { "enum", .enum }, { "union", .union }, { "distinct", .distinct }, @@ -169,8 +171,9 @@ scan proc( if (char == '.' and cursor + 1 < program.len and program[cursor + 1] == '.') { inclusive :: cursor + 2 < program.len and program[cursor + 2] == '=' - width :: if (inclusive) 3 else 2 - range_kind :: if (inclusive) .range_inclusive else .range + variadic :: cursor + 2 < program.len and program[cursor + 2] == '.' + 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{ span = make_span(source, cursor, cursor + width), kind = range_kind, diff --git a/source/lexer/scan.test.hon b/source/lexer/scan.test.hon index 8fd3cc4..14f04f4 100644 --- a/source/lexer/scan.test.hon +++ b/source/lexer/scan.test.hon @@ -150,10 +150,10 @@ scans_ranges_without_consuming_float_dots test { state := init(mem.c_allocator) defer deinit(&state) 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, .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) for (expected) |kind, index| try testing.expect_equal(kind, state.tokens.items[index].kind) diff --git a/source/lexer/token.hon b/source/lexer/token.hon index f5e5e9d..189a2de 100644 --- a/source/lexer/token.hon +++ b/source/lexer/token.hon @@ -28,7 +28,9 @@ TokenKind :: enum { defer, errdefer true, false and, or - struct, enum, union + struct, c_struct + opaque + enum, union distinct, alias import, test mut @@ -53,7 +55,7 @@ TokenKind :: enum { plus, minus star, slash - range, range_inclusive + range, range_inclusive, ellipsis at, ampersand, caret, dollar question, semicolon pipe diff --git a/source/llvm/instructions.hon b/source/llvm/instructions.hon index 35ff732..e9febc7 100644 --- a/source/llvm/instructions.hon +++ b/source/llvm/instructions.hon @@ -316,6 +316,22 @@ emit_instruction proc(state @mut State, block @ir.Block, instruction ir.Instruct else: unreachable } 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 { opcode []u8 :: match instruction.op { .add: "add" diff --git a/source/llvm/llvm.hon b/source/llvm/llvm.hon index 0caa50b..aa69f58 100644 --- a/source/llvm/llvm.hon +++ b/source/llvm/llvm.hon @@ -64,9 +64,10 @@ emit proc( try bytes_constant(output, enummap.get(diagnostics.Code, []u8, &diagnostics.messages, diagnostic.code)?, false) try io.write_all(output, "\n") } - memory_codes [6]diagnostics.Code :: [ + memory_codes [7]diagnostics.Code :: [ .null_unwrap, .invalid_slice_bounds, .sentinel_mismatch, .overlapping_memory_copy, .unsupported_foreign_call, .invalid_pointer_access, + .division_by_zero, ] for (memory_codes) |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" .unsupported_foreign_call: "unsupported_foreign_call" .invalid_pointer_access: "invalid_pointer_access" + .division_by_zero: "division_by_zero" else: unreachable } } diff --git a/source/lower/lower.hon b/source/lower/lower.hon index fcbbfa7..bba2ac2 100644 --- a/source/lower/lower.hon +++ b/source/lower/lower.hon @@ -133,6 +133,7 @@ lower_module proc( id :: ast.package_decl(tree, i) match ast.decl_kind(tree, id) { .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)) storage[usize(id)] = Storage{ kind = .function, index = u32(function) } } @@ -157,7 +158,9 @@ lower_module proc( for (0..ast.package_decl_count(tree)) |i| { id :: ast.package_decl(tree, i) 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: { 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) @@ -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) 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")) { checked :: typecheck.type_of(state.checked, ast.call_arg(tree, id, 0)) type :: lower_type(state, checked) diff --git a/source/lower/lower.test.hon b/source/lower/lower.test.hon index 42e0e55..ccaf1dc 100644 --- a/source/lower/lower.test.hon +++ b/source/lower/lower.test.hon @@ -2014,3 +2014,76 @@ fallible_source_handlers_do_not_catch_runtime_allocation_failures test { } 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) + } + } +} diff --git a/source/parser/parse.hon b/source/parser/parse.hon index 47486d5..d16ef56 100644 --- a/source/parser/parse.hon +++ b/source/parser/parse.hon @@ -114,6 +114,24 @@ parse_package proc(state @mut State) ast.NodeId ! (mem.AllocError | diagnostics. @hide:file 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 ( 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)) @@ -158,7 +176,7 @@ parse_func_decl proc(state @mut State) ast.NodeId ! ParseError { params arraylist.ArrayList(ast.NodeId) := arraylist.init(state.allocator) defer arraylist.deinit(¶ms) - try parse_parameters(state, ¶ms) + variadic :: try parse_parameters(state, ¶ms) _ = try consume(state, .close_paren) 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) @@ -177,17 +195,24 @@ parse_func_decl proc(state @mut State) ast.NodeId ! ParseError { body, ) if (external) ast.set_function_external(state.builder, id) + if (variadic) ast.set_callable_variadic(state.builder, id) return id } @hide:file parse_parameters proc( state @mut State, params @mut arraylist.ArrayList(ast.NodeId), -) void ! ParseError { +) bool ! ParseError { skip_newlines(state) names arraylist.ArrayList(ParameterName) := arraylist.init(state.allocator) defer arraylist.deinit(&names) 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) while true { start :: current(state).span.start @@ -221,6 +246,7 @@ parse_parameters proc( skip_newlines(state) } } + return false } @hide:file @@ -278,7 +304,7 @@ parse_statement proc(state @mut State, allow_else bool) ast.NodeId ! ParseError if ( next_is(state, .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, .distinct) 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) .defer, .errdefer: return try parse_cleanup(state, allow_else) .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: { expression :: try parse_expr(state) 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) condition :: try parse_guard(state) capture :: try parse_optional_capture(state) - braced :: next_is(state, .open_curly) then_block :: try parse_arm(state, true) - # only a braced arm permits its else on a later line. after_then :: state.next_token - if (braced) skip_newlines(state) + skip_newlines(state) 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 alternative = try parse_arm(state, true) } else { @@ -744,7 +768,8 @@ parse_type_atom proc(state @mut State) ast.NodeId ! ParseError { state.next_token += 1 mutable :: next_is(state, .mut) 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) return try ast.add_pointer_type(state.builder, ast.Span{ start = token.span.start, @@ -792,8 +817,14 @@ parse_type_atom proc(state @mut State) ast.NodeId ! ParseError { count, element, mutable, ) } - if next_is(state, .proc) 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, .proc) or next_is(state, .c_func)) return try parse_function_type(state, current(state).span, false) + 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, .union) return try parse_union_type(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 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) params := arraylist.init(ast.NodeId, state.allocator) defer arraylist.deinit(¶ms) - try parse_parameters(state, ¶ms) + variadic :: try parse_parameters(state, ¶ms) _ = try consume(state, .close_paren) result :: try parse_type(state) 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) 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 starts_type proc(kind lexer.TokenKind) bool { 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 == .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 { return match kind { .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 else: false } @@ -1126,8 +1161,11 @@ parse_match_arm proc(state @mut State) ast.NodeId ! ParseError { @hide:file parse_record_type proc(state @mut State) ast.NodeId ! ParseError { - keyword :: try consume(state, .struct) - return try parse_record(state, null, keyword.span.start, true) + keyword :: try consume_either(state, &[.struct, .c_struct]) + 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 @@ -1631,8 +1669,8 @@ named_binding_starts proc(state @State) bool { parse_primary proc(state @mut State) ast.NodeId ! ParseError { token :: current(state) match token.kind { - .at, .star, .question, .proc: return try parse_type(state) - .struct: return try parse_record_type(state) + .at, .star, .question, .proc, .c_func, .opaque: return try parse_type(state) + .struct, .c_struct: return try parse_record_type(state) .enum: return try parse_enum_type(state) .union: return try parse_union_type(state) .distinct, .alias: return try parse_type_wrapper(state) diff --git a/source/parser/parse.test.hon b/source/parser/parse.test.hon index 2e34f70..51be906 100644 --- a/source/parser/parse.test.hon +++ b/source/parser/parse.test.hon @@ -1975,3 +1975,68 @@ rejects_malformed_inline_match_captures test { 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) +} diff --git a/source/sema/check/check.hon b/source/sema/check/check.hon index 286f9c2..451dd3e 100644 --- a/source/sema/check/check.hon +++ b/source/sema/check/check.hon @@ -312,7 +312,11 @@ check_elaborated proc( id :: ast.package_decl(tree, i) match ast.decl_kind(tree, 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) .import: {} } @@ -436,6 +440,12 @@ check_elaborated proc( try require_numeric(&state, id, expression.left) 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: {} } } @@ -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: {} } } @@ -522,9 +538,11 @@ check_elaborated proc( for (0..ast.package_decl_count(tree)) |i| { id :: ast.package_decl(tree, i) 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| { previous :: ast.package_decl(tree, j) 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)) { 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 ( (element_binding.kind == .builtin 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))) 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) .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) { 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 if (type_syntax(ast.expr_kind(state.tree, initializer))) { 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 { 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 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 function :: ast.function_decl(tree, id) 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) else { 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) for (0..ast.function_param_count(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 collect_capture proc(state @mut State, id, body ast.NodeId) void ! mem.AllocError { 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) { target :: binding.data.declaration + if (ast.function_external(tree, target)) try collect_foreign_symbol(state, target) role :: state.cells[usize(target)].role if ( 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_short: .i16 .c_ushort: .u16 + .unavailable: .none } } @@ -1840,7 +1879,8 @@ addressable_target proc(state @mut State, id ast.NodeId) bool { @hide:file same_signature proc(state @State, left, right ast.NodeId) bool { 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 if (ast.function_param_count(tree, left) != ast.function_param_count(tree, right)) return false 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)] b :: state.pointers.items[usize(expected.pointer_type)] 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) 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 type := Type{ kind = .function, function = id } state.cells[usize(id)].type = type - state.pointer_depth += 1 - defer state.pointer_depth -= 1 result :: ast.callable_result(state.tree, id) result_type :: try check_annotation(state, result, true) 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) for 0..ast.function_param_count(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.types[usize(parameter)] = 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, parameter, id) } @@ -2387,10 +2429,16 @@ check_container_type proc(state @mut State, id ast.NodeId) Type ! (mem.AllocErro @hide:file 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 } +@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 builtin_name proc(state @State, id ast.NodeId) []u8 { 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) count :: ast.call_arg_count(state.tree, id) 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") for (0..count) |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) } else try collect_expression(state, argument) try depend(state, argument, id) } 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 } } @@ -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) span :: ast.node_span(state.tree, id) 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") memory :: mem.eql(name, "memcopy") or mem.eql(name, "memset") 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) { - 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) 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) { result :: state.cells[root(state, usize(id))].expected 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 { 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 (type.kind == .function) return c_abi_type(state, type) 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) return state.enums.items[usize(type.enum_type)].explicit_backing if (type.kind == .array) { @@ -3379,6 +3448,7 @@ c_record_field_type proc(state @State, type Type, depth usize) bool { @hide:file 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 if (cached.kind == .invalid) return cached if (!state.cells[usize(id)].record_checking and (cached.kind == .record or cached.kind == .type_value)) { diff --git a/source/sema/resolve.hon b/source/sema/resolve.hon index 5b9bd12..32aef60 100644 --- a/source/sema/resolve.hon +++ b/source/sema/resolve.hon @@ -18,6 +18,7 @@ BuiltinType :: enum { c_int, c_uint, c_long, c_ulong, c_longlong, c_ulonglong c_char, c_uchar, c_short, c_ushort type + unavailable } @hide:file @@ -53,9 +54,13 @@ builtins [_]Builtin :: [ { name = "c_longlong", type = .c_longlong }, { name = "c_ulonglong", type = .c_ulonglong }, { name = "c_char", type = .c_char }, + { name = "c_schar", type = .c_char }, { name = "c_uchar", type = .c_uchar }, { name = "c_short", type = .c_short }, { 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 }, ] @@ -194,6 +199,7 @@ resolve_packages proc( for (0..ast.package_decl_count_at(tree, root)) |i| { id :: ast.package_decl_at(tree, root, i) file_scopes[usize(ast.node_span(tree, id).source)].parent = scope + if (ast.file_local(tree, id)) continue 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) @@ -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 (roots) |root| try resolve_package(&state, root, file_scopes) try canonicalize_aliases(&state) @@ -316,6 +334,10 @@ predeclare proc( name_span ast.Span, ) void ! (mem.AllocError | diagnostics.CapacityError) { 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) |_| { diagnostic :: try diagnostics.add(state.diagnostics, .duplicate_name, name_span) state.bindings[usize(id)] = Binding{ diff --git a/source/sema/specialize/specialize.hon b/source/sema/specialize/specialize.hon index 315131c..15c610d 100644 --- a/source/sema/specialize/specialize.hon +++ b/source/sema/specialize/specialize.hon @@ -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_string: return try builtin(state, id, .string) .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: { 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 @@ -4447,6 +4462,25 @@ expression_type proc(state @mut State, id ast.NodeId, depth usize) ast.NodeId ! ) ) 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 slot :: type_record_slot(state, base, view.member) 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) 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) + # 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) - actual_kind :: ast.expr_kind(&state.tree, actual) + actual_kind :: ast.expr_kind(&state.tree, concrete) match kind { .pointer_type: { if (actual_kind != .pointer_type) return false 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: { element := no_node - if (actual_kind == .slice_type) element = ast.slice_type(&state.tree, actual).element - if (actual_kind == .array_type) element = ast.array_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, concrete).element if (element == no_node) return false return try infer(state, function, ast.slice_type(&state.tree, pattern).element, element, arguments, contextual) } .array_type: { if (actual_kind != .array_type) return false 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 (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) } .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) } .call: { @@ -4558,7 +4596,7 @@ infer proc(state @mut State, function, pattern, actual ast.NodeId, arguments []m for (0..(state.instances.items.len)) |i| { instance :: state.instances.items[i] 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) if (count != instance.arguments.len) return false for (0..count) |j| { diff --git a/source/sema/specialize/specialize.test.hon b/source/sema/specialize/specialize.test.hon new file mode 100644 index 0000000..48bb2bf --- /dev/null +++ b/source/sema/specialize/specialize.test.hon @@ -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, + ) +} diff --git a/tests/native.py b/tests/native.py index bd3f690..b7bff14 100644 --- a/tests/native.py +++ b/tests/native.py @@ -11,6 +11,66 @@ HONEY = ROOT / "build/honey" # Explicit expectations guard against frontend bugs shared by both execution paths. 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": ( """ 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 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"] = ( f""" seed [2]mut {integer_type} :: [{minimum}, {maximum}] @@ -6967,7 +7038,22 @@ main proc() int { return missing } emitted = run(str(HONEY), "emit-llvm", str(single)) assert emitted.returncode == 0 and emitted.stderr == b"", outcome(emitted) 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): @@ -7626,6 +7712,153 @@ def check_dependency_provenance(temporary, executable): executable.unlink() 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): source = temporary / "allocator_libc.hon" source.write_text(textwrap.dedent(""" @@ -8299,6 +8532,8 @@ def main(): check(source, executable, (stdout, error)) count += 1 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_namespace_packages(temporary, executable) count += check_record_packages(temporary, executable) @@ -8325,7 +8560,7 @@ def main(): assert not executable.exists() invalid = run(str(HONEY), "build", str(source), "-o", str(executable), "-O3") 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__":