feat: support callback signatures and many-pointer addition

This commit is contained in:
2026-10-26 11:32:29 +01:00
parent 676f3666b4
commit 7856f4f963
8 changed files with 417 additions and 50 deletions
+4 -2
View File
@@ -79,6 +79,8 @@ the parser supports arithmetic expressions with nested parenthesized groups, suc
function calls are postfix expressions: `f()`, `f(a, b)`, and `factory()(x)`. calls bind tighter than unary and binary arithmetic and can appear in initializers or as expression statements inside function bodies. argument lists allow newlines and a trailing comma; outside parentheses, newlines remain statement boundaries. type checking validates callable values, argument counts and argument types.
callback signatures use `@proc(value i16) i16`, including grouped parameter names and trailing commas. compatible named functions can initialize callbacks, which may be passed, returned, replaced and stored in records/arrays; `Callback :: alias @proc(value i16) i16` names a reusable signature. parameter/result types match structurally, retaining nominal record identity and exact pointer mutability. callback fields call directly, such as `allocator.vtable.alloc(context, size, alignment)` once the remaining argument types are supported. `@mut proc` and `*proc` are invalid; `@mut Callback` instead points to writable callback storage.
`true` and `false` are boolean literals; `bool` is available for parameters, storage and results. `==`, `!=`, `<`, `<=`, `>` and `>=` compare compatible numeric operands and produce `bool`; `==` and `!=` also accept boolean operands. comparisons preserve contextual numeric inference without making their operands boolean. arithmetic binds tighter than relational comparisons, which bind tighter than equality; comparisons associate left to right. boolean arithmetic/ordering and string/function/void comparisons are invalid; there is no implicit numeric-to-boolean conversion.
`!`, `and` and `or` require boolean operands and produce `bool`. prefix `!` and `-` bind tighter than arithmetic; equality binds tighter than `and`, which binds tighter than `or`. binary operators associate left to right, while prefixes nest right to left. operands evaluate left to right: `false and rhs` and `true or rhs` skip the right operand, including calls, lazy global initialization and runtime traps. both operands are still parsed, resolved and type-checked; errors in a skipped operand remain source diagnostics without forcing a runtime trap. this applies in initializers, assignments, returns, call arguments and conditions. [programs/boolean_conditions.hon](programs/boolean_conditions.hon) demonstrates combined predicates; `./build/honey run programs/boolean_conditions.hon` prints `true`.
@@ -95,7 +97,7 @@ indices require `usize`; integer literals and flexible inferred values receive t
data pointers use `@T` for one value and `*T` for many values, with `mut` after the pointer marker for writable pointees. `&value` borrows addressable initialized storage; postfix `^` reads or writes the pointee. pointer mutability is independent of binding mutability: an immutable pointer binding or parameter can mutate its `mut` pointee. mutable pointers weaken to readonly pointers with the same child type. address-taking also accepts an expected many-pointer type, such as `pointer *mut i16 :: &values[0]`; arbitrary temporaries are not addressable.
pointer-to-record field selection dereferences implicitly. pointers to arrays support bounded indexing, `.len`, and `.ptr`; `.ptr` returns a many-pointer and retains both pointer and array-element mutability restrictions. many-pointer indices require `usize` and caller-provided valid storage. pointers preserve alias identity through calls, returns, globals and aggregate copies; dereferenced aggregates still copy by value. `==` and `!=` compare locations. recursive records may contain pointers to themselves without recursive by-value layout. CLI results render pointer leaves as `<pointer>`. pointee lifetimes remain caller-managed; there is no borrow checker. function-pointer signatures, general pointer arithmetic, slices, optionals, pointer casts and allocator libc calls remain unimplemented.
pointer-to-record field selection dereferences implicitly. pointers to arrays support bounded indexing, `.len`, and `.ptr`; `.ptr` returns a many-pointer and retains both pointer and array-element mutability restrictions. many-pointer indices require `usize` and caller-provided valid storage. many-pointer `pointer + offset` requires a `usize` offset, advances by the pointee's storage size and preserves its type/mutability; single-pointer addition, reversed addition and pointer subtraction are invalid, matching Bro. operands evaluate left to right once; one-past pointers can be formed but not dereferenced. pointers preserve alias identity through calls, returns, globals and aggregate copies; dereferenced aggregates still copy by value. `==` and `!=` compare locations. recursive records may contain pointers to themselves without recursive by-value layout. CLI results render pointer leaves as `<pointer>`. pointee lifetimes remain caller-managed; there is no borrow checker. slices, optionals, layout/cast builtins and allocator libc calls remain unimplemented.
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.
@@ -129,7 +131,7 @@ resolver scopes use `@std/hashmap.AutoHashMap(K, V)`, which shares `init`, `put`
`sema.resolve` returns an owned binding table indexed by AST `NodeId`, without modifying the AST. `sema.binding` identifies a declaration, package namespace, builtin type, or invalid binding carrying its diagnostic ID; nodes with no binding return `.none`. `sema.resolve_packages` additionally borrows package roots and import links, predeclares every package before resolving qualified uses, and keeps import aliases in their originating source file. selected declarations have the same binding identity as unqualified declarations. release the result with `sema.deinit`. unknown names and duplicate declarations add source-spanned diagnostics; invalid syntax and failed imports retain their existing diagnostic identity. same-scope duplicates retain the first declaration for recovery, while forbidden inner declarations invalidate that name's uses rather than silently binding them to an outer declaration.
type checking (`@source/sema/check`) runs after name resolution. `check.check` borrows the AST and binding table and returns owned per-node types, pointer/array/record/enum descriptors and construction mappings; query with `check.type_of`, `check.pointer_type`, `check.array_type`, `check.record_type`, `check.enum_type`, `check.field_index` and `check.initializer_field`, and release with `check.deinit`. declarations infer their initializer types, including forward references and function aliases; function types identify the declaration supplying the callable signature. annotations support builtin types, data pointers, fixed arrays and local/imported records, enums, unions and distinct/alias types; type names are not ordinary runtime values. `void` is valid as a function result or tagged-union payload, not as a parameter or standalone stored value.
type checking (`@source/sema/check`) runs after name resolution. `check.check` borrows the AST and binding table and returns owned per-node types, pointer/array/record/enum descriptors and construction mappings; query with `check.type_of`, `check.pointer_type`, `check.array_type`, `check.record_type`, `check.enum_type`, `check.field_index` and `check.initializer_field`, and release with `check.deinit`. declarations infer their initializer types, including forward references and function aliases; function types identify an AST declaration or explicit signature node supplying the callable signature. annotations support builtin types, data pointers, callback signatures, fixed arrays and local/imported records, enums, unions and distinct/alias types; type names are not ordinary runtime values. `void` is valid as a function result or tagged-union payload, not as a parameter or standalone stored value.
function fallthrough obligations are separate from body types. `check.fallthrough_diagnostic(&result, function)` accepts a function declaration's `NodeId` and returns its original missing-return diagnostic ID, or `null` when no obligation exists. nested body errors retain their original identities. lowering consumes this fact without interpreting diagnostic codes and places its trap only on a surviving fallthrough path; `check.deinit` releases the obligation storage.
+5 -3
View File
@@ -9,8 +9,9 @@ selection through the interpreter and native backend. concrete 8/16/32/64-bit
integers, checked numeric conversions, records/tuples, construction/defaults,
enums, distinct/alias types, overlapping untagged/tagged unions, field reads/writes
and exhaustive statement/value matching work through both execution paths.
source data pointers/references now support aliasing, mutation, calls and aggregate
storage through both execution paths. the driver retains frontend storage
source data pointers/references support aliasing, mutation, calls and aggregate
storage; explicit callback signatures and many-pointer addition work through both
execution paths. 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
package's production files and load its dependencies in separate scopes.
@@ -42,7 +43,8 @@ test discovery and FFI remain outside these completed slices.
- [x] nominal enums and distinct types, transparent declaration/type aliases, overlapping untagged/tagged unions, contextual payload construction, active-field checks and exhaustive scalar/enum/tagged-union matching through interpreter/native execution. pointer captures, inline specialization, sum composition and yielding value blocks remain with their respective later gates.
- [ ] memory representation: source pointers/references, mutable and sentinel slices, optionals, layout/cast builtins and allocator-required libc calls. internal IR array addresses are not source pointers. examples: `std/mem/mem.hon`, `std/arraylist/arraylist.hon`.
- [x] source data pointers/references: `@T`/`*T`, `mut`, address-taking and dereference, pointer-aware fields/indexing, array `.len`/`.ptr`, readonly weakening, location equality, recursive pointer-linked records and shared pointees across interpreter/native calls and aggregate copies.
- [ ] function-pointer signatures and general pointer arithmetic; mutable/sentinel slices, optionals, layout/cast builtins and allocator-required libc calls.
- [x] function-pointer signatures: explicit `@proc(...) Result`, structural signature checking, callbacks in parameters/results/records/arrays, vtable calls and imported signature aliases; many-pointer `+ usize` arithmetic preserves pointee type, mutability, stride and location identity through interpreter/native execution.
- [ ] mutable/sentinel slices, optionals, layout/cast builtins and allocator-required libc calls.
- [ ] fallible results and cleanup: error-set composition, `try`/`catch`, `defer`/`errdefer`; infrastructure errors remain distinct from diagnostic traps. examples: `source/main.hon:41`, `source/sema/check/check.hon:117`.
- [ ] generic specialization and compile-time execution/reflection sufficient for existing `ArrayList`, hash maps, `enummap`, `meta` and `io.print`; include type/value parameters and compile-time generated storage.
- [ ] remaining source constructs: collection/index iteration, expression-valued control flow and `yield`, compound updates and multiline strings. examples: `source/parser/parse.hon`, `source/llvm/runtime.hon`.
+31 -6
View File
@@ -1,5 +1,5 @@
#! grammar:
#! * type ..... -> identifier {'.' member} | ('@' | '*') ['mut'] type | '[' (expr | '_') ']' ['mut'] type | 'struct' '{' fields '}' | 'enum' ['(' type ')'] '{' members '}' | 'union' ['(' (type | 'enum') ')'] '{' fields '}' | ('distinct' | 'alias') type
#! * type ..... -> identifier {'.' member} | ('@' | '*') ['mut'] type | '@proc' '(' parameters ')' type | '[' (expr | '_') ']' ['mut'] type | 'struct' '{' fields '}' | 'enum' ['(' type ')'] '{' members '}' | 'union' ['(' (type | 'enum') ')'] '{' fields '}' | ('distinct' | 'alias') type
#! * binding ... -> :: | :=
#! * literal .. -> int | float | string | 'true' | 'false'
#! * op ....... -> '+' | '-' | '*' | '/' | '==' | '!=' | '<' | '<=' | '>' | '>=' | 'and' | 'or'
@@ -45,6 +45,7 @@ ExprKind :: enum {
range
array_type
pointer_type
function_type
array_literal
record_type
record_literal
@@ -112,6 +113,7 @@ NodeKind :: enum {
expr_range
expr_array_type
expr_pointer_type
expr_function_type
expr_array_literal
expr_record_type
expr_record_literal
@@ -511,6 +513,19 @@ add_pointer_type proc(
}, span)
}
add_function_type proc(
builder @mut Builder, span Span, result NodeId, params []NodeId,
) NodeId ! mem.AllocError {
start :: builder.storage.extra.items.len
for (params) |parameter| _ = try add_extra(builder, u32(parameter))
return try add_node(builder, Node{
kind = .expr_function_type,
main = NodeData{ node_id = result },
data0 = NodeData{ extra = u32(start) },
data1 = NodeData{ extra = u32(builder.storage.extra.items.len) },
}, span)
}
add_array_literal proc(builder @mut Builder, span Span, elements []NodeId) NodeId ! mem.AllocError {
start :: builder.storage.extra.items.len
for (elements) |element| _ = try add_extra(builder, u32(element))
@@ -1032,6 +1047,13 @@ function_decl proc(tree @Tree, id NodeId) FunctionDeclView {
}
}
callable_result proc(tree @Tree, id NodeId) NodeId {
node :: get_node(&tree.storage, id)
if (node.kind == .func_decl) return function_decl(tree, id).return_type
debug.assert(node.kind == .expr_function_type)
return node.main.node_id
}
import_decl proc(tree @Tree, id NodeId) ImportView {
node :: get_node(&tree.storage, id)
debug.assert(node.kind == .import_decl)
@@ -1047,15 +1069,17 @@ import_decl proc(tree @Tree, id NodeId) ImportView {
function_param_count proc(tree @Tree, id NodeId) usize {
node :: get_node(&tree.storage, id)
debug.assert(node.kind == .func_decl)
return usize(node.data1.extra - node.main.extra - 4)
debug.assert(node.kind == .func_decl or node.kind == .expr_function_type)
start :: if (node.kind == .func_decl) node.main.extra + 4 else node.data0.extra
return usize(node.data1.extra - start)
}
function_param proc(tree @Tree, id NodeId, index usize) NodeId {
node :: get_node(&tree.storage, id)
debug.assert(node.kind == .func_decl)
debug.assert(index < usize(node.data1.extra - node.main.extra - 4))
return NodeId(tree.storage.extra.items[usize(node.main.extra) + 4 + index])
debug.assert(node.kind == .func_decl or node.kind == .expr_function_type)
start :: if (node.kind == .func_decl) node.main.extra + 4 else node.data0.extra
debug.assert(index < usize(node.data1.extra - start))
return NodeId(tree.storage.extra.items[usize(start) + index])
}
param proc(tree @Tree, id NodeId) ParamView {
@@ -1103,6 +1127,7 @@ expression_kind proc(kind NodeKind) ExprKind {
.expr_range: .range
.expr_array_type: .array_type
.expr_pointer_type: .pointer_type
.expr_function_type: .function_type
.expr_array_literal: .array_literal
.expr_record_type: .record_type
.expr_record_literal: .record_literal
+9 -1
View File
@@ -1065,7 +1065,7 @@ lower_expression proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocEr
# ranges have no physical value; only lower_for decomposes an iterable.
if (!trap_type(state, id)) unreachable
}
.array_type, .pointer_type, .record_type, .enum_type, .union_type, .distinct_type: {
.array_type, .pointer_type, .function_type, .record_type, .enum_type, .union_type, .distinct_type: {
if (!trap_type(state, id)) {
trap(state, try diagnostics.add(state.diagnostics, .type_not_value, span), span)
}
@@ -1240,6 +1240,14 @@ lower_expression proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocEr
if (state.terminated) return ir.no_value
right :: try lower_expression(state, expression.right)
if (state.terminated or trap_type(state, id)) return ir.no_value
if (expression.operator == .add and type == .pointer) {
pointer :: typecheck.pointer_type(state.checked, typecheck.type_of(state.checked, id).pointer_type)
return try emit(state, ir.Instruction{
op = .pointer_offset, type = .pointer, left = left, right = right,
pointee = lower_type(state, pointer.element),
array_type = try array_shape(state, pointer.element), span = span,
})
}
op ir.Op :: match expression.operator {
.add: .add
.subtract: .subtract
+50 -27
View File
@@ -149,33 +149,7 @@ parse_func_decl proc(state @mut State) ast.NodeId ! ParseError {
params arraylist.ArrayList(ast.NodeId) := arraylist.init(state.allocator)
defer arraylist.deinit(&params)
names arraylist.ArrayList(lexer.Token) := arraylist.init(state.allocator)
defer arraylist.deinit(&names)
while !next_is(state, .close_paren) {
arraylist.clear(&names)
while true {
token :: try consume(state, .ident)
try arraylist.append(&names, token)
if (starts_type(current(state).kind)) break
_ = try consume(state, .comma)
}
type_id :: try parse_type(state)
type_span :: ast.builder_span(state.builder, type_id)
for (names.items) |parameter_name| {
param_id :: try ast.add_param(
state.builder,
ast.Span{ start = parameter_name.span.start, end = type_span.end, source = parameter_name.span.source },
parameter_name.span,
parameter_name.data.str_id,
type_id,
)
try arraylist.append(&params, param_id)
}
if next_is(state, .comma) _ = try consume(state, .comma)
}
try parse_parameters(state, &params)
_ = try consume(state, .close_paren)
return_type :: try parse_type(state)
body :: try parse_block(state)
@@ -195,6 +169,36 @@ parse_func_decl proc(state @mut State) ast.NodeId ! ParseError {
)
}
@hide:file
parse_parameters proc(
state @mut State, params @mut arraylist.ArrayList(ast.NodeId),
) void ! ParseError {
names arraylist.ArrayList(lexer.Token) := arraylist.init(state.allocator)
defer arraylist.deinit(&names)
while !next_is(state, .close_paren) {
arraylist.clear(&names)
while true {
token :: try consume(state, .ident)
try arraylist.append(&names, token)
if (starts_type(current(state).kind)) break
_ = try consume(state, .comma)
}
type_id :: try parse_type(state)
type_span :: ast.builder_span(state.builder, type_id)
for (names.items) |parameter_name| {
param_id :: try ast.add_param(
state.builder,
ast.Span{ start = parameter_name.span.start, end = type_span.end, source = parameter_name.span.source },
parameter_name.span,
parameter_name.data.str_id,
type_id,
)
try arraylist.append(params, param_id)
}
if next_is(state, .comma) _ = try consume(state, .comma)
}
}
@hide:file
parse_block proc(state @mut State) ast.NodeId ! ParseError {
open :: try consume(state, .open_curly)
@@ -610,6 +614,25 @@ parse_type 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)) {
state.next_token += 1
_ = try consume(state, .open_paren)
params := arraylist.init(ast.NodeId, state.allocator)
defer arraylist.deinit(&params)
try parse_parameters(state, &params)
_ = try consume(state, .close_paren)
result :: try parse_type(state)
span :: ast.Span{
start = token.span.start,
end = ast.builder_span(state.builder, result).end,
source = token.span.source,
}
if (mutable or token.kind == .star) {
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)
}
element :: try parse_type(state)
return try ast.add_pointer_type(state.builder, ast.Span{
start = token.span.start,
+71 -10
View File
@@ -351,7 +351,7 @@ check proc(
callee :: ast.call_expr(tree, id).callee
signature :: try infer_shape(&state, callee, false)
if (signature.function != ast.NodeId(maxval!(u32))) {
result :: ast.function_decl(tree, signature.function).return_type
result :: ast.callable_result(tree, signature.function)
seed(&state, id, types[usize(result)])
} else if (signature.kind == .invalid) {
seed(&state, id, signature)
@@ -391,7 +391,7 @@ check proc(
}
.binary: {
expression :: ast.binary_expr(tree, id)
if (logical(expression.operator) or bool_equality(&state, id)) continue
if (logical(expression.operator) or bool_equality(&state, id) or pointer_addition(&state, id)) continue
try require_numeric(&state, id, expression.left)
try require_numeric(&state, id, expression.right)
}
@@ -411,7 +411,7 @@ check proc(
}
.binary: {
expression :: ast.binary_expr(tree, id)
if (logical(expression.operator)) continue
if (logical(expression.operator) or pointer_addition(&state, id)) continue
if (comparison(expression.operator)) {
if (!bool_equality(&state, id)) try comparison_operands(&state, id)
} else {
@@ -1135,7 +1135,7 @@ collect_expression proc(state @mut State, id ast.NodeId) void ! (mem.AllocError
.array_type: {
state.cells[usize(id)].type = invalid_type(try diagnostics.add(state.diagnostics, .type_not_value, ast.node_span(tree, id)))
}
.record_type, .union_type, .enum_type, .distinct_type, .pointer_type: {
.record_type, .union_type, .enum_type, .distinct_type, .pointer_type, .function_type: {
type := try check_type_expression(state, id, state.cells[usize(id)].nominal)
if (type.kind != .invalid) {
type.value_kind = type.kind
@@ -1261,6 +1261,17 @@ logical proc(operator ast.BinaryOperator) bool {
return operator == .logical_and or operator == .logical_or
}
@hide:file
pointer_addition proc(state @mut State, id ast.NodeId) bool {
expression :: ast.binary_expr(state.tree, id)
left :: group_type(state, expression.left)
return (
expression.operator == .add and left.kind == .pointer
and left.nominal == ast.NodeId(maxval!(u32))
and state.pointers.items[usize(left.pointer_type)].many
)
}
@hide:file
bool_equality proc(state @mut State, id ast.NodeId) bool {
expression :: ast.binary_expr(state.tree, id)
@@ -1516,14 +1527,15 @@ 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
tree :: state.tree
if (ast.function_param_count(tree, left) != ast.function_param_count(tree, right)) return false
left_result :: ast.function_decl(tree, left).return_type
right_result :: ast.function_decl(tree, right).return_type
left_result :: ast.callable_result(tree, left)
right_result :: ast.callable_result(tree, right)
if (!same_type(state, state.types[usize(left_result)], state.types[usize(right_result)])) return false
for (0..ast.function_param_count(tree, left)) |i| {
left_param :: ast.function_param(tree, left, i)
right_param :: ast.function_param(tree, right, i)
left_param :: ast.param(tree, ast.function_param(tree, left, i)).type
right_param :: ast.param(tree, ast.function_param(tree, right, i)).type
if (!same_type(state, state.types[usize(left_param)], state.types[usize(right_param)])) return false
}
return true
@@ -1635,6 +1647,12 @@ validate proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnost
left :: state.types[usize(expression.left)]
right :: state.types[usize(expression.right)]
if (left.kind == .invalid or right.kind == .invalid) return
if (pointer_addition(state, id)) {
if (right.kind != .usize or right.nominal != ast.NodeId(maxval!(u32))) {
try fail(state, id, .invalid_operand, expression.span)
}
return
}
if ((expression.operator == .equal or expression.operator == .not_equal)
and (left.kind == .bool or left.kind == .enumeration or left.kind == .pointer)
and (assignable_type(state, left, right) or assignable_type(state, right, left))) return
@@ -1901,7 +1919,7 @@ check_pointer_type proc(state @mut State, id ast.NodeId) Type ! (mem.AllocError
if (element.kind == .invalid) return element
binding :: sema.binding(state.resolution, view.element)
if (element.kind == .none or element.kind == .void or element.kind == .range
or element.kind == .type_value or element.kind == .function
or element.kind == .type_value
or has_inferred_count(state, element)
or (binding.kind == .builtin and (binding.data.builtin == .int or binding.data.builtin == .float))) {
return invalid_type(try diagnostics.add(state.diagnostics, .invalid_pointer_type, ast.node_span(state.tree, id)))
@@ -1909,6 +1927,35 @@ check_pointer_type proc(state @mut State, id ast.NodeId) Type ! (mem.AllocError
return try intern_pointer(state, PointerType{ element = element, mutable = view.mutable, many = view.many })
}
@hide:file
check_function_type proc(state @mut State, id ast.NodeId) Type ! (mem.AllocError | diagnostics.CapacityError) {
cached := state.cells[usize(id)].type
if (cached.kind == .type_value) cached.kind = cached.value_kind
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
try depend(state, result, id)
for 0..ast.function_param_count(state.tree, id) |i| {
parameter :: ast.function_param(state.tree, id, i)
annotation :: ast.param(state.tree, parameter).type
parameter_type :: try check_annotation(state, annotation, false)
state.cells[usize(parameter)].role = .annotation
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
try depend(state, annotation, parameter)
try depend(state, parameter, id)
}
type.function = id
state.cells[usize(id)].type = type
return type
}
@hide:file
intern_array proc(state @mut State, descriptor ArrayType) Type ! mem.AllocError {
for state.arrays.items |existing, i| {
@@ -2037,6 +2084,19 @@ infer_shape proc(state @mut State, id ast.NodeId, defaults bool) Type ! (mem.All
if (state.cells[index].type.kind == .invalid) return state.cells[index].type
state.cells[index].visiting = true
defer state.cells[index].visiting = false
if (state.cells[usize(id)].role == .expression and ast.expr_kind(state.tree, id) == .binary) {
view :: ast.binary_expr(state.tree, id)
if (view.operator == .add) {
_ = try infer_shape(state, view.left, defaults)
if (pointer_addition(state, id)) {
type :: group_type(state, view.left)
state.cells[index].numeric = false
seed(state, id, type)
try demand(state, view.right, Type{ kind = .usize }, .invalid_operand)
return group_type(state, id)
}
}
}
if (state.cells[usize(id)].role == .expression and ast.expr_kind(state.tree, id) == .unary) {
view :: ast.unary_expr(state.tree, id)
if (view.operator == .address_of or view.operator == .dereference) {
@@ -2688,7 +2748,7 @@ type_syntax proc(kind ast.ExprKind) bool {
return (
kind == .record_type or kind == .union_type or kind == .enum_type
or kind == .distinct_type or kind == .alias_expr
or kind == .pointer_type
or kind == .pointer_type or kind == .function_type
)
}
@@ -2718,6 +2778,7 @@ check_type_expression proc(state @mut State, id ast.NodeId, nominal bool) Type !
kind :: ast.expr_kind(state.tree, id)
if (kind == .record_type) return try check_record(state, id, nominal)
if (kind == .pointer_type) return try check_pointer_type(state, id)
if (kind == .function_type) return try check_function_type(state, id)
if (kind == .union_type) {
type :: try check_union(state, id, nominal)
state.cells[usize(id)].type = type
+7
View File
@@ -572,6 +572,13 @@ resolve_expr proc(
}
}
.pointer_type: try resolve_expr(state, scope, ast.pointer_type(tree, id).element)
.function_type: {
try resolve_expr(state, scope, ast.callable_result(tree, id))
for 0..ast.function_param_count(tree, id) |i| {
parameter :: ast.function_param(tree, id, i)
try resolve_expr(state, scope, ast.param(tree, parameter).type)
}
}
.array_type: {
type :: ast.array_type(tree, id)
if (type.count) |count| try resolve_expr(state, scope, count)
+240 -1
View File
@@ -11,6 +11,143 @@ HONEY = ROOT / "build/honey"
# Explicit expectations guard against frontend bugs shared by both execution paths.
CASES = {
"callback_concrete_vtable": (
"""
VTable :: struct { run @proc(context @mut State, amount i16) i16 }
State :: struct { value i16, vtable @VTable }
add proc(context @mut State, amount i16) i16 {
context.value = context.value + amount
return context.value
}
table VTable :: { run = add }
main proc() i16 {
state State := { value = 40, vtable = &table }
return state.vtable.run(&state, 2)
}
""", b"42\n", None,
),
"callback_pass_return_store_replace": (
"""
Callback :: alias @proc(value i16) i16
Holder :: struct { run Callback }
increment proc(value i16) i16 { return value + 1 }
twice proc(value i16) i16 { return value + value }
choose proc(first Callback, second Callback, flag bool) Callback {
if (flag) return first
return second
}
apply proc(callback Callback, value i16) i16 { return callback(value) }
main proc() i16 {
callback @proc(n i16) i16 := increment
callback = twice
holder Holder := { run = increment }
holder.run = callback
callbacks [2]mut Callback := [increment, twice]
callbacks[0] = choose(holder.run, increment, true)
return apply(callbacks[0], 21)
}
""", b"42\n", None,
),
"callback_nested_aggregate_arguments_and_result": (
"""
Payload :: struct { values [2]i16, pointer @mut i16 }
Callback :: alias @proc(input Payload, rows [2][2]i16) Payload
transform proc(input Payload, rows [2][2]i16) Payload {
input.pointer^ = input.pointer^ + rows[1][0]
return { values = [input.values[0] + rows[0][1], rows[1][1]], pointer = input.pointer }
}
select proc(callback Callback) Callback { return callback }
main proc() [3]i16 {
value i16 := 40
callback Callback :: select(transform)
result :: callback({ values = [40, 0], pointer = &value }, [[0, 2], [2, 42]])
return [result.values[0], result.values[1], result.pointer^]
}
""", b"[42, 42, 42]\n", None,
),
"callback_storage_address_is_data_pointer": (
"""
Callback :: alias @proc(value i16) i16
increment proc(value i16) i16 { return value + 1 }
twice proc(value i16) i16 { return value + value }
replace proc(slot @mut Callback, callback Callback) void { slot^ = callback }
main proc() i16 {
callback Callback := increment
replace(&callback, twice)
return (&callback)^(21)
}
""", b"42\n", None,
),
"callback_structural_inline_aggregate_signature": (
"""
identity proc(value struct { values [2]i16, pointer @i16 }) struct { values [2]i16, pointer @i16 } {
return value
}
main proc() i16 {
number i16 :: 2
callback @proc(input struct { values [2]i16, pointer @i16 }) struct { values [2]i16, pointer @i16 } :: identity
result :: callback({ values = [40, 0], pointer = &number })
return result.values[0] + result.pointer^
}
""", b"42\n", None,
),
"callback_parameter_signature_mismatch": (
"wrong proc(value u16) i16 { return 42 }\nmain proc() i16 { callback @proc(value i16) i16 :: wrong\nreturn callback(1) }",
b"", b"incompatible value type",
),
"callback_result_signature_mismatch": (
"wrong proc(value i16) u16 { return 42 }\nmain proc() i16 { callback @proc(value i16) i16 :: wrong\nreturn callback(1) }",
b"", b"incompatible value type",
),
"callback_nested_signature_mismatch": (
"""
wrong proc(value struct { rows [2][2]u16 }) i16 { return 42 }
main proc() i16 {
callback @proc(value struct { rows [2][2]i16 }) i16 :: wrong
return 42
}
""", b"", b"incompatible value type",
),
"callback_pointer_mutability_signature_mismatch": (
"wrong proc(value @mut i16) i16 { return value^ }\nmain proc() i16 { callback @proc(value @i16) i16 :: wrong\nreturn 42 }",
b"", b"incompatible value type",
),
"callback_wrong_argument_type": (
"identity proc(value i16) i16 { return value }\nmain proc() i16 { callback @proc(value i16) i16 :: identity\nreturn callback(true) }",
b"", b"incompatible value type",
),
"callback_wrong_argument_count": (
"identity proc(value i16) i16 { return value }\nmain proc() i16 { callback @proc(value i16) i16 :: identity\nreturn callback() }",
b"", b"argument count does not match",
),
"callback_readonly_vtable_assignment": (
"""
VTable :: struct { run @proc(value i16) i16 }
identity proc(value i16) i16 { return value }
replace proc(table @VTable) void { table.run = identity }
main proc() i16 {
table VTable := { run = identity }
replace(&table)
return 42
}
""", b"", b"pointer assignment requires a mutable pointee",
),
"callback_readonly_array_assignment": (
"identity proc(value i16) i16 { return value }\nmain proc() i16 { callbacks [1]@proc(value i16) i16 := [identity]\ncallbacks[0] = identity\nreturn 42 }",
b"", b"indexed assignment requires a writable array element",
),
"callback_invalid_annotation_recovery": (
"bad proc(callback @proc(value void) i16) void {}\nmain proc() i16 { return 42 }",
b"42\n", b"void cannot be stored",
),
"callback_mutable_signature_rejected": (
"bad proc(callback @mut proc(value i16) i16) void {}\nmain proc() i16 { return 42 }",
b"42\n", b"pointer requires a supported concrete pointee type",
),
"callback_many_signature_rejected": (
"bad proc(callback *proc(value i16) i16) void {}\nmain proc() i16 { return 42 }",
b"42\n", b"pointer requires a supported concrete pointee type",
),
"pointer_assignment_evaluates_rhs_first_once": (
"""
Box :: struct { value i16, count i16 }
@@ -250,6 +387,87 @@ CASES = {
"unused proc() int { return 42^ }\nmain proc() int { return 42 }",
b"42\n", b"dereference requires",
),
"pointer_addition_preserves_alias_and_mutability": (
"""
advance proc(p *mut i16, count usize) *mut i16 { return p + count }
main proc() [3]i16 {
values [3]mut i16 := [1, 2, 3]
pointer :: advance((&values).ptr, 1)
pointer^ = 40
(pointer + 1)^ = pointer^ + 2
return [values[0], values[1], values[2]]
}
""", b"[1, 40, 42]\n", None,
),
"pointer_addition_aggregate_stride_and_identity": (
"""
Point :: struct { x i16, y i64 }
main proc() bool {
values [3]mut Point := [
Point { x = 1, y = 2 }, Point { x = 3, y = 4 }, Point { x = 5, y = 6 },
]
pointer :: (&values).ptr
next :: pointer + (1 + 1)
next.y = 42
last *mut Point :: &values[2]
return (next == last and values[2].y == 42
and pointer + 3 == next + 1)
}
""", b"true\n", None,
),
"pointer_addition_evaluates_operands_once_in_order": (
"""
first proc(count @mut usize, values *mut i16) *mut i16 {
count^ = count^ + 1
return values
}
second proc(count @mut usize) usize {
previous :: count^
count^ = count^ + 1
return previous
}
main proc() [2]i16 {
count usize := 0
values [2]mut i16 := [1, 42]
pointer :: first(&count, (&values).ptr) + second(&count)
return [pointer^, i16(count)]
}
""", b"[42, 2]\n", None,
),
"pointer_addition_zero_sized_elements": (
"""
Empty :: struct {}
main proc() bool {
values [2]mut Empty := [Empty {}, Empty {}]
pointer :: (&values).ptr
return pointer + 2 == pointer
}
""", b"true\n", None,
),
"pointer_addition_requires_usize_offset": (
"main proc() i16 { values [2]mut i16 := [1, 42]\noffset i16 :: 1\nreturn ((&values).ptr + offset)^ }",
b"", b"",
),
"pointer_addition_single_pointer_rejected": (
"main proc() i16 { value i16 := 42\nreturn (&value + 1)^ }",
b"", b"",
),
"pointer_subtraction_rejected": (
"main proc() i16 { values [2]mut i16 := [1, 42]\nreturn ((&values).ptr - 1)^ }",
b"", b"",
),
"pointer_reversed_addition_rejected": (
"main proc() i16 { values [2]mut i16 := [1, 42]\nreturn (1 + (&values).ptr)^ }",
b"", b"",
),
"pointer_addition_preserves_readonly_pointee": (
"main proc() i16 { values [2]i16 := [1, 42]\npointer :: (&values).ptr + 1\npointer^ = 7\nreturn values[1] }",
b"", b"",
),
"pointer_addition_unused_error_recovers": (
"bad proc(p *i16) *i16 { return p + true }\nmain proc() i16 { return 42 }",
b"42\n", b"",
),
"match_float_ranges": (
"""
score proc(n f64) i16 {
@@ -1257,7 +1475,28 @@ def check_record_packages(temporary, executable):
check(app, executable, (b"", b"integer arithmetic overflow"))
trapped = run(str(HONEY), "run", str(app))
assert trapped.stderr.startswith(f"{types.resolve()}:1:".encode()), outcome(trapped)
return 3
types.write_text(
"Point :: struct { x i16, y i16 = 2 }\n"
"Callback :: alias @proc(p @mut Point, amount i16) i16\n"
"VTable :: struct { run Callback }\n"
)
(library / "functions.hon").write_text(
"increment proc(p @mut Point, amount i16) i16 { p.x = p.x + amount\nreturn p.x + p.y }\n"
"invoke proc(table VTable, values *mut Point) i16 { return table.run(&(values + 1)^, 40) }\n"
)
(app / "main.hon").write_text(textwrap.dedent("""
lib :: import "../library"
main proc() i16 {
values [2]mut lib.Point := [lib.Point { x = 7 }, lib.Point { x = 0 }]
callback lib.Callback :: lib.increment
table :: lib.VTable { run = callback }
result :: lib.invoke(table, (&values).ptr)
if (values[0].x != 7 or values[1].x != 40) return -1
return result
}
""").strip() + "\n")
check(app, executable, (b"42\n", None))
return 4
def check_data_model_packages(temporary, executable):