diff --git a/README.md b/README.md index 67f46a1..412ae84 100644 --- a/README.md +++ b/README.md @@ -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 ``. 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 ``. 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. diff --git a/TODO.md b/TODO.md index bee74b1..2d52eaa 100644 --- a/TODO.md +++ b/TODO.md @@ -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`. diff --git a/source/ast/ast.hon b/source/ast/ast.hon index c1c6eca..b0828e7 100644 --- a/source/ast/ast.hon +++ b/source/ast/ast.hon @@ -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 diff --git a/source/lower/lower.hon b/source/lower/lower.hon index 4ef1649..f23b54b 100644 --- a/source/lower/lower.hon +++ b/source/lower/lower.hon @@ -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 diff --git a/source/parser/parse.hon b/source/parser/parse.hon index cbe4077..5ff06d3 100644 --- a/source/parser/parse.hon +++ b/source/parser/parse.hon @@ -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(¶ms) - 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(¶ms, param_id) - } - if next_is(state, .comma) _ = try consume(state, .comma) - } - + try parse_parameters(state, ¶ms) _ = 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(¶ms) + try parse_parameters(state, ¶ms) + _ = 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, diff --git a/source/sema/check/check.hon b/source/sema/check/check.hon index 182eef0..7e00fdc 100644 --- a/source/sema/check/check.hon +++ b/source/sema/check/check.hon @@ -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 diff --git a/source/sema/resolve.hon b/source/sema/resolve.hon index ca313bc..f752b38 100644 --- a/source/sema/resolve.hon +++ b/source/sema/resolve.hon @@ -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) diff --git a/tests/native.py b/tests/native.py index 54fdf35..f533372 100644 --- a/tests/native.py +++ b/tests/native.py @@ -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):