From 6886d743b5b50c635bfd8b8b9fffd7b1860838af Mon Sep 17 00:00:00 2001 From: hl-valdemar Date: Mon, 26 Oct 2026 13:10:52 +0100 Subject: [PATCH] feat: specialize generic type and value parameters --- README.md | 19 + TODO.md | 5 + source/ast/ast.hon | 433 +++++++- source/ast/ast.test.hon | 56 + source/compilation/compilation.hon | 8 +- source/compilation/compilation.test.hon | 34 + source/diagnostics/diagnostics.hon | 8 + source/lexer/scan.hon | 1 + source/lexer/scan.test.hon | 25 +- source/lexer/token.hon | 2 +- source/lower/lower.hon | 67 +- source/parser/parse.hon | 78 +- source/parser/parse.test.hon | 87 +- source/sema/check/check.hon | 41 +- source/sema/resolve.hon | 25 +- source/sema/resolve.test.hon | 2 +- source/sema/specialize/specialize.hon | 1241 +++++++++++++++++++++++ tests/native.py | 470 +++++++++ 18 files changed, 2499 insertions(+), 103 deletions(-) create mode 100644 source/sema/specialize/specialize.hon diff --git a/README.md b/README.md index ff031a4..9d8976d 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,25 @@ 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. +generic procedures mark compile-time parameters with `$`, such as `identity proc($T type, value T) T`. pass all parameters explicitly (`identity(i16, 42)`) or omit every compile-time argument when runtime argument types determine them (`identity(value)`). inference follows pointers, arrays, slices, optionals and specialized record constructors, including array counts; conflicting or undetermined arguments are diagnosed. specializations are cached by declaration and compile-time arguments, retain nominal generated-record identity, and erase compile-time parameters from runtime calls. + +```hon +Fixed proc($T type, $N usize) type { + return struct { values [N]mut T } +} + +first proc($T type, $N usize, value @Fixed(T, N)) T { + return value.values[0] +} + +main proc() i16 { + fixed Fixed(i16, 2) :: { values = [42, 0] } + return first(&fixed) +} +``` + +the specialization checkpoint supports type arguments and constant integer, boolean, string, callback and null value arguments; declared value-parameter types survive specialization, including narrow checked arithmetic and optional captures/unwrap. integer arguments are range-checked even when unused, and erased argument expressions still receive ordinary checking. immutable constant expressions may use unary signs, `!`, `+`, `-`, `*`, comparisons and logical operators. type-producing procedures require compile-time parameters (or no parameters) and support immutable locals, conditional returns, nested type-producing calls and generated record/array storage, including pointer-recursive records. runtime values and mutable bindings cannot supply compile-time arguments. runaway specialization is diagnosed at 128 nested evaluation/specialization calls or 4096 instances. arbitrary compile-time procedure execution, reflection, `inline` iteration and full generic standard-library compilation remain outside this checkpoint. + 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. diff --git a/TODO.md b/TODO.md index e30f72e..f456002 100644 --- a/TODO.md +++ b/TODO.md @@ -17,6 +17,9 @@ executed foreign calls remain diagnostic traps in the interpreter. explicit fallible channels, composed error sets, `try`/`catch` and scoped LIFO `defer`/`errdefer` run through both paths; source errors are values, not diagnostic traps or compiler infrastructure failures. +cached generic specialization and type-producing procedures support explicit/inferred +compile-time arguments and generated record/array storage through both paths; +general compile-time execution/reflection and generic library acceptance remain open. 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 @@ -53,6 +56,8 @@ test discovery and FFI remain outside these completed slices. - [x] mutable/sentinel slices with checked bounds, borrowed aliases and string views; optional values/pointers/callbacks, null checks, unwrap and conditional/loop captures; target layout, pointer/const casts and typed memory copy/set through interpreter/native execution. scalar/pointer C ABI declarations build/run allocator callbacks using `malloc`, `realloc`, `free` and `posix_memalign`; arbitrary interpreter FFI, generic standard-library compilation and native startup/IO remain with later gates. - [x] fallible results and cleanup: explicit `T ! E`, error-set composition, `try`/`catch`, `defer`/`errdefer`, error captures and matched error residuals through interpreter/native execution; infrastructure errors remain distinct from diagnostic traps. fallible `void` may complete on fallthrough; unhandled entry errors exit with status `1`. inferred `T!` signatures and yielding catch blocks remain with inference/value-control-flow work. examples: `source/main.hon`, `source/sema/check/check.hon`. - [ ] 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. + - [x] specialization checkpoint: explicit/inferred type and integer/boolean/string/callback/null value parameters, cached concrete runtime calls, nominal type-producing procedures, generated record/array storage and recursive data pointers through interpreter/native execution. preserve declared value types, source diagnostics and repeatable compilation; runtime values cannot supply compile-time arguments. + - [ ] remaining gate: general compile-time procedure execution, compile-time branch/inline expansion and reflection (`typeinfo!`, `field!`, `struct_type!`, generated type/default arrays), plus existing-library acceptance. required collection/index iteration, expression-valued control flow, compound updates and formatting parsing may interleave with the next source-construct gate. - [ ] 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`. - [ ] 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 a3d257c..dd119d4 100644 --- a/source/ast/ast.hon +++ b/source/ast/ast.hon @@ -1,10 +1,11 @@ #! grammar: -#! * type ..... -> type_sum ['!' type]; type_sum -> type_atom {'|' type_atom}; type_atom includes '(' type ')', identifier {'.' member}, '?' type_atom, ('@' | '*') ['mut'] type_atom, '@proc' '(' parameters ')' type, '[' (expr | '_') ']' ['mut'] type_atom, '[' [';' expr] ']' ['mut'] type_atom, record/enum/union declarations, and ('distinct' | 'alias') type. +#! * type ..... -> type_sum ['!' type]; type_sum -> type_atom {'|' type_atom}; type_atom includes '(' type ')', identifier {'.' member | '(' [expr {',' expr} [',']] ')'}, '?' type_atom, ('@' | '*') ['mut'] type_atom, ['@'] 'proc' '(' parameters ')' type, '[' (expr | '_') ']' ['mut'] type_atom, '[' [';' expr] ']' ['mut'] type_atom, record/enum/union declarations, and ('distinct' | 'alias') type. #! * binding ... -> :: | := #! * literal .. -> int | float | string | 'true' | 'false' | 'null' #! * op ....... -> '+' | '-' | '*' | '/' | '==' | '!=' | '<' | '<=' | '>' | '>=' | 'and' | 'or' #! * expr ..... -> identifier | literal | type | '.' member | '[' [expr {',' expr} [',']] ']' | [type] '{' [initializer {',' initializer} [',']] '}' | '(' expr ')' | ('-' | '!' | '&' | 'try') expr | expr ('^' | '?') | expr op expr | expr '(' [expr {',' expr} [',']] ')' | identifier '!' '(' [expr {',' expr} [',']] ')' | expr '[' expr ']' | expr '[' [expr] '..' [expr] [';' expr] ']' | expr '.' (member | int) | expr ('..' | '..=') expr | 'match' expr '{' match_arms '}' | expr 'catch' ['|' identifier '|'] (expr | block) #! * decl ..... -> identifier [type] binding expr | identifier 'c_func' '(' parameters ')' type | ['test'] [identifier '::'] 'import' string +#! * parameters -> [['$'] identifier {',' ['$'] identifier} type {',' ['$'] identifier {',' ['$'] identifier} type} [',']]; '$' marks each compile-time parameter independently. #! * block .... -> '{' {stmt} '}' #! * arm ...... -> block | stmt (single statements become canonical blocks) #! * match_arm -> (expr {',' expr} ['|' identifier '|'] | 'else') ':' (block | stmt | expr); arms are comma/newline separated. @@ -67,6 +68,7 @@ ExprKind :: enum { sum_type try_expr catch_expr + typed_constant } DeclKind :: enum { @@ -169,6 +171,7 @@ NodeKind :: enum { stmt_defer stmt_errdefer stmt_block + expr_typed_constant } @hide:file @@ -196,16 +199,17 @@ Storage :: struct { extra arraylist.ArrayList(u32) spans arraylist.ArrayList(Span) sources arraylist.ArrayList(diagnostics.Source) + generated arraylist.ArrayList([]mut u8) } -#! owns node storage and source descriptors; borrows registered source paths and text. +#! owns node storage, source descriptors, and generated text; borrows registered source paths and text. Builder :: struct { storage Storage source []u8 source_id diagnostics.SourceId = diagnostics.SourceId(0) } -#! owns storage after finish, or borrows it after view; registered source paths and text remain borrowed. +#! owns storage after finish, or borrows it after view; source paths and non-generated text remain borrowed. Tree :: struct { storage Storage root NodeId @@ -238,6 +242,7 @@ ParamView :: struct { span Span name Span type NodeId + comptime bool = false } UnaryExprView :: struct { @@ -253,6 +258,11 @@ BinaryExprView :: struct { right NodeId } +TypedConstantView :: struct { + type NodeId + value NodeId +} + CatchExprView :: struct { span Span operand NodeId @@ -390,6 +400,7 @@ init_builder proc(allocator mem.Allocator, source []u8) Builder { extra = arraylist.init(allocator), spans = arraylist.init(allocator), sources = arraylist.init(allocator), + generated = arraylist.init(allocator), }, source = source, } @@ -432,8 +443,10 @@ source_files proc(tree @Tree) []diagnostics.Source { return tree.storage.sources.items } -#! releases owned AST and source-descriptor storage, without freeing source paths or text. +#! releases owned AST, generated text, and source descriptors, without freeing borrowed sources. deinit_builder proc(builder @mut Builder) void { + for (builder.storage.generated.items) |text| mem.free(builder.storage.generated.allocator, text) + arraylist.deinit(&builder.storage.generated) arraylist.deinit(&builder.storage.nodes) arraylist.deinit(&builder.storage.extra) arraylist.deinit(&builder.storage.spans) @@ -449,14 +462,17 @@ finish proc(builder @mut Builder, root NodeId) Tree { extra = arraylist.init(allocator), spans = arraylist.init(allocator), sources = arraylist.init(allocator), + generated = arraylist.init(allocator), } builder.source = &[] builder.source_id = diagnostics.SourceId(0) return tree } -#! releases owned AST and source-descriptor storage, without freeing source paths or text. +#! releases owned AST, generated text, and source descriptors, without freeing borrowed sources. deinit proc(tree @mut Tree) void { + for (tree.storage.generated.items) |text| mem.free(tree.storage.generated.allocator, text) + arraylist.deinit(&tree.storage.generated) arraylist.deinit(&tree.storage.nodes) arraylist.deinit(&tree.storage.extra) arraylist.deinit(&tree.storage.spans) @@ -513,6 +529,17 @@ add_binary proc( }, span) } +#! preserves a generated constant's declared type without introducing a runtime binding. +add_typed_constant proc( + builder @mut Builder, at Span, type, value NodeId, +) NodeId ! mem.AllocError { + return try add_node(builder, Node{ + kind = .expr_typed_constant, + data0 = NodeData{ node_id = type }, + data1 = NodeData{ node_id = value }, + }, at) +} + add_binary_type proc( builder @mut Builder, span Span, left, right NodeId, fallible bool, ) NodeId ! mem.AllocError { @@ -936,6 +963,7 @@ add_param proc( ) NodeId ! mem.AllocError { extra :: try add_extra(builder, name.start) _ = try add_extra(builder, name.end) + _ = try add_extra(builder, 0) return try add_node(builder, Node{ kind = .param_decl, main = NodeData{ extra = extra }, @@ -944,6 +972,13 @@ add_param proc( }, span) } +set_param_comptime proc(builder @mut Builder, id NodeId) void { + node :: builder.storage.nodes.items[usize(id)] + debug.assert(node.kind == .param_decl) + builder.storage.extra.items[usize(node.main.extra) + 2] = 1 +} + + add_function_decl proc( builder @mut Builder, span Span, @@ -980,6 +1015,15 @@ function_external proc(tree @Tree, id NodeId) bool { return tree.storage.extra.items[usize(node.data1.extra) - 1] != 0 } +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 + for 0..function_param_count(tree, id) |index| { + if (param(tree, function_param(tree, id, index)).comptime) return true + } + return false +} + add_import_decl proc( builder @mut Builder, span, path Span, @@ -1279,6 +1323,7 @@ param proc(tree @Tree, id NodeId) ParamView { span = get_span(&tree.storage, id), name = span_from_extra(&tree.storage, usize(node.main.extra), get_span(&tree.storage, id).source), type = node.data0.node_id, + comptime = tree.storage.extra.items[usize(node.main.extra) + 2] != 0, } } @@ -1339,6 +1384,7 @@ expression_kind proc(kind NodeKind) ExprKind { .expr_sum_type: .sum_type .expr_try: .try_expr .expr_catch: .catch_expr + .expr_typed_constant: .typed_constant else: unreachable } } @@ -1364,6 +1410,15 @@ binary_expr proc(tree @Tree, id NodeId) BinaryExprView { } } +typed_constant proc(tree @Tree, id NodeId) TypedConstantView { + node :: get_node(&tree.storage, id) + debug.assert(node.kind == .expr_typed_constant) + return TypedConstantView{ + type = node.data0.node_id, + value = node.data1.node_id, + } +} + catch_expr proc(tree @Tree, id NodeId) CatchExprView { node :: get_node(&tree.storage, id) debug.assert(node.kind == .expr_catch) @@ -1688,3 +1743,371 @@ maybe_node_id proc(value u32) ?NodeId { if (value == u32(no_node)) return null return NodeId(value) } + +NodeClass :: enum { + expression, statement, function, parameter, block, field, match_arm, package, import, invalid +} + +@hide:file +NodeLayout :: struct { + class NodeClass = .expression + direct_children u32 = 0 + extra_pointers u32 = 0 + extra_start usize = 0 + extra_count usize = 0 + extra_children u32 = 0 + list_start usize = 0 + list_end usize = 0 +} + +#! describes owned extra storage and child slots, excluding names, spans, and flags. +@hide:file +node_layout proc(node Node) NodeLayout { + layout := NodeLayout{} + match node.kind { + .literal_int, .literal_float, .literal_string, .literal_bool, .literal_null, + .expr_identifier, .invalid_expr: {} + .expr_unary: layout.direct_children = 2 + .expr_binary, .expr_range, .expr_index, .expr_selection, .expr_typed_constant, + .expr_fallible_type, .expr_sum_type, .expr_array_type, .expr_slice_type: + layout.direct_children = 6 + .expr_pointer_type: layout.direct_children = 4 + .expr_optional_type, .expr_distinct_type, .expr_alias, .expr_enum_literal, + .expr_try: layout.direct_children = 1 + .expr_call, .expr_builtin_call, .expr_function_type, .expr_array_literal, + .expr_record_type, .expr_record_literal, .expr_enum_type, .expr_match, + .block, .package: { + layout.extra_pointers = 6 + layout.extra_start = usize(node.data0.extra) + layout.extra_count = usize(node.data1.extra - node.data0.extra) + layout.list_end = layout.extra_count + if (node.kind == .block) layout.class = .block + else if (node.kind == .package) layout.class = .package + else if (node.kind != .expr_array_literal and node.kind != .expr_record_type) + layout.direct_children = 1 + } + .expr_union_type: { + layout.extra_pointers = 7 + layout.extra_start = usize(node.main.extra) + layout.extra_count = usize(node.data1.extra - node.main.extra) + layout.extra_children = 1 + layout.list_start = 2 + layout.list_end = layout.extra_count + } + .record_field: { + layout.class = .field + layout.direct_children = 7 + } + .match_arm: { + layout.class = .match_arm + layout.direct_children = 1 + layout.extra_pointers = 6 + layout.extra_start = usize(node.data0.extra) + layout.extra_count = usize(node.data1.extra - node.data0.extra) + layout.extra_children = 1 + layout.list_start = 3 + layout.list_end = layout.extra_count + } + .expr_catch: { + layout.direct_children = 6 + layout.extra_pointers = 1 + layout.extra_start = usize(node.main.extra) + layout.extra_count = 2 + layout.extra_children = 1 + } + .expr_slice: { + layout.direct_children = 1 + layout.extra_pointers = 2 + layout.extra_start = usize(node.data0.extra) + layout.extra_count = 3 + layout.extra_children = 7 + } + .stmt_expr, .stmt_return, .stmt_block, .stmt_defer, .stmt_errdefer: { + layout.class = .statement + layout.direct_children = 1 + if (node.kind == .stmt_defer or node.kind == .stmt_errdefer) + layout.direct_children = 3 + } + .stmt_break, .stmt_continue, .invalid_stmt: layout.class = .statement + .stmt_assignment: { + layout.class = .statement + layout.direct_children = 3 + } + .stmt_while: { + layout.class = .statement + layout.direct_children = 7 + } + .stmt_if: { + layout.class = .statement + layout.direct_children = 6 + layout.extra_pointers = 1 + layout.extra_start = usize(node.main.extra) + layout.extra_count = 2 + layout.extra_children = 3 + } + .stmt_for: { + layout.class = .statement + layout.direct_children = 3 + layout.extra_pointers = 4 + layout.extra_start = usize(node.data1.extra) + layout.extra_count = 4 + layout.extra_children = 3 + } + .stmt_decl: { + layout.class = .statement + layout.extra_pointers = 1 + layout.extra_start = usize(node.main.extra) + layout.extra_count = 4 + layout.extra_children = 12 + } + .func_decl: { + layout.class = .function + layout.direct_children = 2 + layout.extra_pointers = 5 + layout.extra_start = usize(node.main.extra) + layout.extra_count = usize(node.data1.extra - node.main.extra) + layout.extra_children = 4 + layout.list_start = 4 + layout.list_end = layout.extra_count - 1 + } + .param_decl: { + layout.class = .parameter + layout.direct_children = 2 + layout.extra_pointers = 1 + layout.extra_start = usize(node.main.extra) + layout.extra_count = 3 + } + .import_decl: { + layout.class = .import + layout.extra_pointers = 1 + layout.extra_start = usize(node.main.extra) + layout.extra_count = 2 + } + .invalid_decl: layout.class = .invalid + } + return layout +} + +@hide:file +has_slot proc(mask u32, index usize) bool { + return (mask & (u32(1) << u32(index))) != 0 +} + +@hide:file +node_word proc(node Node, index usize) NodeData { + return match index { + 0: node.main + 1: node.data0 + 2: node.data1 + else: unreachable + } +} + +@hide:file +set_node_word proc(node @mut Node, index usize, word NodeData) void { + match index { + 0: node.main = word + 1: node.data0 = word + 2: node.data1 = word + else: unreachable + } +} + +node_class proc(tree @Tree, id NodeId) NodeClass { + return node_layout(get_node(&tree.storage, id)).class +} + +#! counts actual AST children, excluding absent optional nodes and non-node metadata. +child_count proc(tree @Tree, id NodeId) usize { + node :: get_node(&tree.storage, id) + layout :: node_layout(node) + count := layout.list_end - layout.list_start + for (0..usize(3)) |i| { + if (has_slot(layout.direct_children, i) and node_word(node, i).node_id != no_node) + count += 1 + } + for (0..usize(4)) |i| { + if (!has_slot(layout.extra_children, i)) continue + if (NodeId(tree.storage.extra.items[layout.extra_start + i]) != no_node) count += 1 + } + return count +} + +#! returns a child in stable payload order; `index` must be less than child_count. +child proc(tree @Tree, id NodeId, index usize) NodeId { + node :: get_node(&tree.storage, id) + layout :: node_layout(node) + remaining := index + for (0..usize(3)) |i| { + if (!has_slot(layout.direct_children, i)) continue + value :: node_word(node, i).node_id + if (value == no_node) continue + if (remaining == 0) return value + remaining -= 1 + } + for (0..usize(4)) |i| { + if (!has_slot(layout.extra_children, i)) continue + value :: NodeId(tree.storage.extra.items[layout.extra_start + i]) + if (value == no_node) continue + if (remaining == 0) return value + remaining -= 1 + } + debug.assert(remaining < layout.list_end - layout.list_start) + return NodeId(tree.storage.extra.items[layout.extra_start + layout.list_start + remaining]) +} + +#! clones owned payload storage and preserves spans and interned names. +#! `mapping` covers original nodes, initially maxval!(NodeId), with substitutions prefilled. +#! nodes outside `mapping` are already concrete and returned unchanged. +clone_subtree proc( + builder @mut Builder, root NodeId, mapping []mut NodeId, +) NodeId ! mem.AllocError { + if (root == no_node or usize(root) >= mapping.len) return root + if (mapping[usize(root)] != no_node) return mapping[usize(root)] + original :: get_node(&builder.storage, root) + span :: get_span(&builder.storage, root) + layout :: node_layout(original) + cloned := original + start :: builder.storage.extra.items.len + for (0..layout.extra_count) |i| { + value :: builder.storage.extra.items[layout.extra_start + i] + _ = try add_extra(builder, value) + } + for (0..usize(3)) |i| { + if (has_slot(layout.extra_pointers, i)) { + offset :: usize(node_word(original, i).extra) - layout.extra_start + set_node_word(&cloned, i, NodeData{ extra = u32(start + offset) }) + } + if (has_slot(layout.direct_children, i)) { + value :: try clone_subtree(builder, node_word(original, i).node_id, mapping) + set_node_word(&cloned, i, NodeData{ node_id = value }) + } + } + for (0..layout.extra_count) |i| { + is_child :: ( + (i < 4 and has_slot(layout.extra_children, i)) + or (i >= layout.list_start and i < layout.list_end) + ) + if (!is_child) continue + original_child :: NodeId(builder.storage.extra.items[layout.extra_start + i]) + value :: try clone_subtree(builder, original_child, mapping) + builder.storage.extra.items[start + i] = u32(value) + } + result :: try add_node(builder, cloned, span) + mapping[usize(root)] = result + return result +} + +#! shallow-copies the source kind and payload, retaining the target's diagnostic span. +replace_node proc(builder @mut Builder, target, source NodeId) void { + builder.storage.nodes.items[usize(target)] = get_node(&builder.storage, source) +} + +#! shallow-copies kind, payload, and span; extra storage remains shared. +copy_node proc(builder @mut Builder, id NodeId) NodeId ! mem.AllocError { + node :: get_node(&builder.storage, id) + span :: get_span(&builder.storage, id) + return try add_node(builder, node, span) +} + +#! replaces package children without changing its node identity or source span. +set_package_declarations proc( + builder @mut Builder, root NodeId, declarations []NodeId, +) void ! mem.AllocError { + debug.assert(get_node(&builder.storage, root).kind == .package) + start :: builder.storage.extra.items.len + for (declarations) |declaration| _ = try add_extra(builder, u32(declaration)) + builder.storage.nodes.items[usize(root)].data0 = NodeData{ extra = u32(start) } + builder.storage.nodes.items[usize(root)].data1 = NodeData{ extra = u32(builder.storage.extra.items.len) } +} + +#! retains generated decimal text until deinit_builder or ownership transfer through finish. +#! generated literals retain `at` for diagnostics; read their spelling through literal_text. +add_integer proc(builder @mut Builder, at Span, magnitude u64) NodeId ! mem.AllocError { + digits [20]mut u8 := undefined + start := digits.len + remaining := magnitude + while true { + start -= 1 + digits[start] = '0' + u8(rem!(remaining, 10)) + remaining = divtrunc!(remaining, 10) + if (remaining == 0) break + } + return try add_generated_literal(builder, at, .int, digits[start..]) +} + +#! retains generated boolean text with the same ownership and provenance rules as add_integer. +add_boolean proc(builder @mut Builder, at Span, value bool) NodeId ! mem.AllocError { + text :: if (value) "true" else "false" + return try add_generated_literal(builder, at, .bool, text) +} + +@hide:file +add_generated_literal proc( + builder @mut Builder, at Span, kind LiteralKind, bytes []u8, +) NodeId ! mem.AllocError { + debug.assert(usize(at.source) < builder.storage.sources.items.len) + allocator :: builder.storage.generated.allocator + text :: try mem.alloc(u8, allocator, bytes.len) + errdefer mem.free(allocator, text) + memcopy!(text, bytes) + index :: builder.storage.generated.items.len + debug.assert(u64(index) < u64(maxval!(u32))) + try arraylist.append(&builder.storage.generated, text) + errdefer _ = arraylist.pop(&builder.storage.generated) + id :: try add_literal(builder, kind, at) + builder.storage.nodes.items[usize(id)].main = NodeData{ extra = u32(index) } + builder.storage.nodes.items[usize(id)].data0 = NodeData{ extra = 1 } + return id +} + +#! borrows a literal's spelling, independently of its diagnostic source span. +literal_text proc(tree @Tree, id NodeId) []u8 { + node :: get_node(&tree.storage, id) + if (node.data0.extra == 1) return tree.storage.generated.items[usize(node.main.extra)] + return source_text(tree, get_span(&tree.storage, id)) +} + +StringError :: enum { invalid_escape } + +#! decodes one byte from quoted literal text without allocation. +#! start `cursor` at one and call only while it is less than `text.len - 1`. +string_next proc(text []u8, cursor @mut usize) u8 ! StringError { + debug.assert(text.len >= 2 and cursor^ >= 1 and cursor^ < text.len - 1) + end :: text.len - 1 + byte := text[cursor^] + cursor^ += 1 + if (byte != '\\') return byte + if (cursor^ >= end) return .invalid_escape + escape :: text[cursor^] + cursor^ += 1 + match escape { + '\\': byte = '\\' + '"': byte = '"' + 'n': byte = '\n' + 'r': byte = '\r' + 't': byte = '\t' + '0': byte = 0 + 'x': { + if (end - cursor^ < 2) return .invalid_escape + high := u8(0) + low := u8(0) + if (hex_digit(text[cursor^])) |digit| high = digit + else return .invalid_escape + if (hex_digit(text[cursor^ + 1])) |digit| low = digit + else return .invalid_escape + byte = high * 16 + low + cursor^ += 2 + } + else: return .invalid_escape + } + return byte +} + +@hide:file +hex_digit proc(byte u8) ?u8 { + if (byte >= '0' and byte <= '9') return byte - '0' + if (byte >= 'a' and byte <= 'f') return byte - 'a' + 10 + if (byte >= 'A' and byte <= 'F') return byte - 'A' + 10 + return null +} diff --git a/source/ast/ast.test.hon b/source/ast/ast.test.hon index 67238de..114b4a5 100644 --- a/source/ast/ast.test.hon +++ b/source/ast/ast.test.hon @@ -40,6 +40,7 @@ exposes_typed_declaration_views test { try testing.expect_equal("value", source_text(&tree, statement.name)) } + exposes_array_type_literal_and_index_views test { strings := strpool.init(mem.c_allocator) defer strpool.deinit(&strings) @@ -112,3 +113,58 @@ removes_only_registered_source_without_allocating_builder test { replacement :: try set_source(&builder, "replacement.hon", "") try testing.expect_equal(diagnostics.SourceId(0), replacement) } + + +owns_generated_literal_text_across_finish test { + tree Tree := undefined + { + builder := init_builder(mem.c_allocator, "") + defer deinit_builder(&builder) + source :: try set_source(&builder, "generated.hon", "input") + at :: Span{ start = 0, end = 5, source = source } + zero :: try add_integer(&builder, at, 0) + maximum :: try add_integer(&builder, at, maxval!(u64)) + enabled :: try add_boolean(&builder, at, true) + disabled :: try add_boolean(&builder, at, false) + try testing.expect_equal(source, builder.source_id) + try testing.expect_equal("input", builder.source) + declarations [4]NodeId :: [zero, maximum, enabled, disabled] + root :: try add_package(&builder, at, declarations[..]) + tree = finish(&builder, root) + } + defer deinit(&tree) + zero :: package_decl(&tree, 0) + maximum :: package_decl(&tree, 1) + enabled :: package_decl(&tree, 2) + disabled :: package_decl(&tree, 3) + try testing.expect_equal(ExprKind.literal_int, expr_kind(&tree, zero)) + try testing.expect_equal("0", literal_text(&tree, zero)) + try testing.expect_equal("18446744073709551615", literal_text(&tree, maximum)) + try testing.expect_equal(ExprKind.literal_bool, expr_kind(&tree, enabled)) + try testing.expect_equal("true", literal_text(&tree, enabled)) + try testing.expect_equal("false", literal_text(&tree, disabled)) + try testing.expect_equal(1, source_files(&tree).len) + try testing.expect_equal(diagnostics.SourceId(0), node_span(&tree, zero).source) + try testing.expect_equal(0, node_span(&tree, zero).start) + try testing.expect_equal(5, node_span(&tree, zero).end) + try testing.expect_equal("input", source_text(&tree, node_span(&tree, zero))) + try testing.expect_equal("generated.hon", source_files(&tree)[usize(node_span(&tree, zero).source)].path) + try testing.expect_equal("generated.hon", source_files(&tree)[usize(node_span(&tree, maximum).source)].path) +} + +streams_decoded_string_literal_bytes test { + text :: "\"a\\\\\\\"\\n\\r\\t\\0\\x4a\\xFf\"" + expected [9]u8 :: ['a', '\\', '"', '\n', '\r', '\t', 0, 'J', 255] + cursor := usize(1) + for (expected) |byte| try testing.expect_equal(byte, try string_next(text, &cursor)) + try testing.expect_equal(text.len - 1, cursor) + malformed [6][]u8 :: ["\"\\q\"", "\"\\x\"", "\"\\x1\"", "\"\\xG0\"", "\"\\x0G\"", "\"\\\""] + for (malformed) |literal| { + cursor = 1 + _ = string_next(literal, &cursor) catch |error| { + try testing.expect_equal(StringError.invalid_escape, error) + continue + } + try testing.expect(false) + } +} diff --git a/source/compilation/compilation.hon b/source/compilation/compilation.hon index 75be131..ade7fd9 100644 --- a/source/compilation/compilation.hon +++ b/source/compilation/compilation.hon @@ -14,6 +14,7 @@ import "@source/lower" import "@source/loader" import "@source/ir" typecheck :: import "@source/sema/check" +import "@source/sema/specialize" #! identities are valid only within their owning compilation. SourceId :: alias diagnostics.SourceId @@ -350,7 +351,7 @@ compile proc( } imports := arraylist.init(sema.Import, compilation.allocator) defer arraylist.deinit(&imports) - tree :: ast.view(&compilation.builder, compilation.module_root?) + tree := ast.view(&compilation.builder, compilation.module_root?) for (compilation.imports.items) |link| { imported :: ast.import_decl(&tree, link.node) name := strpool.no_str @@ -392,7 +393,10 @@ compile proc( } resolution := try sema.resolve_packages(compilation.allocator, &tree, roots.items, imports.items, &compilation.strings, diagnostic_store) defer sema.deinit(&resolution) - checked := try typecheck.check(compilation.allocator, &tree, &resolution, diagnostic_store) + elaborated := try specialize.elaborate(compilation.allocator, &compilation.builder, compilation.module_root?, &resolution, diagnostic_store) + defer specialize.deinit(&elaborated) + tree = ast.view(&compilation.builder, compilation.module_root?) + checked := try typecheck.check_elaborated(compilation.allocator, &tree, &resolution, elaborated.constraints.items, diagnostic_store) defer typecheck.deinit(&checked) return try lower.lower_package(compilation.allocator, &tree, &resolution, &checked, roots.items[usize(root_package)], diagnostic_store) } diff --git a/source/compilation/compilation.test.hon b/source/compilation/compilation.test.hon index 9ce25a9..c89e0bc 100644 --- a/source/compilation/compilation.test.hon +++ b/source/compilation/compilation.test.hon @@ -68,6 +68,40 @@ keeps_package_declarations_distinct_and_ir_independent test { try testing.expect_equal(i64(6), right_result.value.data.integer) } +recompiles_specialized_calls_without_borrowing_previous_ir test { + diagnostic_store := diagnostics.init(mem.c_allocator) + defer diagnostics.deinit(&diagnostic_store) + modules [2]mut ir.Module := undefined + { + session := init(mem.c_allocator) + defer deinit(&session) + root :: try add_package(&session) + bytes := try owned_text( + `identity proc($T type, value T) T { return value } + `Box proc($T type) type { return struct { value T } } + `main proc() i16 { + ` box Box(i16) :: { value = identity(i16, 40) } + ` return identity(box.value) + 2 + `} + , + ) + defer arraylist.deinit(&bytes) + _ = try add_source(&session, root, "generic.hon", &bytes, &diagnostic_store) + modules[0] = try compile(&session, root, &diagnostic_store) + errdefer ir.deinit(&modules[0]) + modules[1] = try compile(&session, root, &diagnostic_store) + } + defer ir.deinit(&modules[0]) + defer ir.deinit(&modules[1]) + try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) + for (0..modules.len) |index| { + outcome := try interpreter.run(mem.c_allocator, &modules[index], &diagnostic_store) + defer interpreter.deinit(&outcome) + try testing.expect(outcome.trap == null) + try testing.expect_equal(i64(42), outcome.value.data.integer) + } +} + dependency_traps_keep_original_spans_and_remain_demand_driven test { mains [2][]u8 :: [ `dep :: import "../dependency" diff --git a/source/diagnostics/diagnostics.hon b/source/diagnostics/diagnostics.hon index eaafe0f..785e883 100644 --- a/source/diagnostics/diagnostics.hon +++ b/source/diagnostics/diagnostics.hon @@ -107,6 +107,10 @@ Code :: enum { catch_type_mismatch errdefer_outside_fallible cleanup_control_flow + comptime_argument_required + cannot_infer_generic + specialization_cycle + specialization_limit } Diagnostic :: struct { @@ -235,5 +239,9 @@ messages :: $enummap.init(Code, []u8, { catch_type_mismatch = "catch handler must produce the success type or terminate", errdefer_outside_fallible = "errdefer requires an enclosing fallible function", cleanup_control_flow = "cleanup cannot escape with return, break, continue, or try", + comptime_argument_required = "generic argument must be a compile-time value", + cannot_infer_generic = "cannot infer consistent compile-time arguments from runtime arguments", + specialization_cycle = "compile-time specialization depends on its unfinished result", + specialization_limit = "compile-time specialization exceeds its recursion or instance limit", }) diff --git a/source/lexer/scan.hon b/source/lexer/scan.hon index 4f4442c..5c16554 100644 --- a/source/lexer/scan.hon +++ b/source/lexer/scan.hon @@ -301,6 +301,7 @@ scan proc( ',': .comma '|': .pipe '@': .at + '$': .dollar '&': .ampersand '^': .caret '?': .question diff --git a/source/lexer/scan.test.hon b/source/lexer/scan.test.hon index 569a2ab..a70af34 100644 --- a/source/lexer/scan.test.hon +++ b/source/lexer/scan.test.hon @@ -11,7 +11,7 @@ reports_invalid_input_and_keeps_scanning test { state := init(mem.c_allocator) defer deinit(&state) - try scan(&state, "if name # comment\n$1. 2 3.5 \"hi\"", &pool, &diagnostic_store) + try scan(&state, "if name # comment\n~1. 2 3.5 \"hi\"", &pool, &diagnostic_store) try testing.expect_equal(9, state.tokens.items.len) try testing.expect_equal(TokenKind.if, state.tokens.items[0].kind) @@ -32,6 +32,27 @@ reports_invalid_input_and_keeps_scanning test { try testing.expect_equal(u32(7), state.tokens.items[1].span.end) } +scans_compile_time_parameter_markers test { + pool := strpool.init(mem.c_allocator) + defer strpool.deinit(&pool) + diagnostic_store := diagnostics.init(mem.c_allocator) + defer diagnostics.deinit(&diagnostic_store) + state := init(mem.c_allocator) + defer deinit(&state) + try scan(&state, "$T $K, $V type $N usize", &pool, &diagnostic_store) + expected [12]TokenKind :: [ + .dollar, .ident, .dollar, .ident, .comma, .dollar, .ident, .ident, + .dollar, .ident, .ident, .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) + try testing.expect_equal(u32(0), state.tokens.items[0].span.start) + try testing.expect_equal(u32(1), state.tokens.items[0].span.end) + try testing.expect_equal(u32(15), state.tokens.items[8].span.start) + try testing.expect_equal(u32(16), state.tokens.items[8].span.end) + try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) +} + reserves_boolean_keywords_and_scans_comparisons test { pool := strpool.init(mem.c_allocator) defer strpool.deinit(&pool) @@ -226,7 +247,7 @@ scans_import_keywords_and_preserves_source_in_tokens_and_diagnostics test { state := init(mem.c_allocator) defer deinit(&state) state.source_id = diagnostics.SourceId(7) - try scan(&state, "import test $\n1.\n\"unfinished", &pool, &diagnostic_store) + try scan(&state, "import test ~\n1.\n\"unfinished", &pool, &diagnostic_store) try testing.expect_equal(TokenKind.import, state.tokens.items[0].kind) try testing.expect_equal(TokenKind.test, state.tokens.items[1].kind) for (state.tokens.items) |token| try testing.expect_equal(state.source_id, token.span.source) diff --git a/source/lexer/token.hon b/source/lexer/token.hon index ba0aeb6..6c3d4c0 100644 --- a/source/lexer/token.hon +++ b/source/lexer/token.hon @@ -52,7 +52,7 @@ TokenKind :: enum { star, slash range, range_inclusive - at, ampersand, caret + at, ampersand, caret, dollar question, semicolon pipe diff --git a/source/lower/lower.hon b/source/lower/lower.hon index f80b130..9be11c4 100644 --- a/source/lower/lower.hon +++ b/source/lower/lower.hon @@ -1768,6 +1768,10 @@ lower_expression proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocEr } if (typecheck.is_union_variant(state.checked, id)) return try lower_union(state, id) match ast.expr_kind(tree, id) { + .typed_constant: { + if (trap_type(state, id)) return ir.no_value + return try lower_as(state, ast.typed_constant(tree, id).value, typecheck.type_of(state.checked, id)) + } .invalid: { invalid :: ast.invalid(tree, id) trap(state, invalid.diagnostic, invalid.span) @@ -1849,7 +1853,7 @@ lower_expression proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocEr if (trap_type(state, id)) return ir.no_value return try constant(state, ir.Value{ type = .bool, - data = ir.ValueData{ boolean = mem.eql(ast.source_text(tree, span), "true") }, + data = ir.ValueData{ boolean = mem.eql(ast.literal_text(tree, id), "true") }, }, span) } .literal_string: { @@ -1959,7 +1963,7 @@ lower_expression proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocEr }) } if (expression.operator == .negate and ir.integer_bits(type) != 0 and !ir.unsigned_integer(type) and ast.expr_kind(tree, expression.operand) == .literal_int) { - text :: ast.source_text(tree, ast.node_span(tree, expression.operand)) + text :: ast.literal_text(tree, expression.operand) magnitude_limit :: ir.integer_max(type) + 1 if (integer_magnitude(text, magnitude_limit)) |magnitude| { if (magnitude == magnitude_limit) { @@ -2099,7 +2103,7 @@ exact_float_integer proc(text []u8, value f64) bool { @hide:file lower_number proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) { span :: ast.node_span(state.tree, id) - text :: ast.source_text(state.tree, span) + text :: ast.literal_text(state.tree, id) type :: lower_type(state, typecheck.type_of(state.checked, id)) if (ir.integer_bits(type) != 0 and !ir.unsigned_integer(type)) { if (integer_magnitude(text, ir.integer_max(type))) |magnitude| { @@ -2129,14 +2133,6 @@ lower_number proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError return ir.no_value } -@hide:file -hex_digit proc(byte u8) ?u8 { - if (byte >= '0' and byte <= '9') return byte - '0' - if (byte >= 'a' and byte <= 'f') return byte - 'a' + 10 - if (byte >= 'A' and byte <= 'F') return byte - 'A' + 10 - return null -} - @hide:file lower_string proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) { span :: ast.node_span(state.tree, id) @@ -2147,57 +2143,14 @@ lower_string proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError length := usize(0) cursor := usize(1) end :: text.len - 1 - valid := true while (cursor < end) { - byte := text[cursor] - cursor += 1 - if (byte == '\\') { - if (cursor >= end) { - valid = false - break - } - escape :: text[cursor] - cursor += 1 - match escape { - '\\': byte = '\\' - '"': byte = '"' - 'n': byte = '\n' - 'r': byte = '\r' - 't': byte = '\t' - '0': byte = 0 - 'x': { - if (end - cursor < 2) { - valid = false - break - } - high := u8(0) - low := u8(0) - if (hex_digit(text[cursor])) |digit| high = digit - else { - valid = false - break - } - if (hex_digit(text[cursor + 1])) |digit| low = digit - else { - valid = false - break - } - byte = high * 16 + low - cursor += 2 - } - else: { - valid = false - break - } - } + byte :: ast.string_next(text, &cursor) catch |_| { + trap(state, try diagnostics.add(state.diagnostics, .invalid_escape, span), span) + return ir.no_value } owned[length] = byte length += 1 } - if (!valid) { - trap(state, try diagnostics.add(state.diagnostics, .invalid_escape, span), span) - return ir.no_value - } if (state.module.strings.items.len > usize(maxval!(ir.StringId))) return .out_of_memory diff --git a/source/parser/parse.hon b/source/parser/parse.hon index e6c7e08..7501dee 100644 --- a/source/parser/parse.hon +++ b/source/parser/parse.hon @@ -34,6 +34,13 @@ State :: struct { match_arm_body bool = false } +@hide:file +ParameterName :: struct { + token lexer.Token + start u32 + comptime bool +} + #! borrows `tokens` and `source` while parsing; the returned Tree borrows `source` until ast.deinit. #! `allocator` supplies returned node storage and temporary lists released before return. #! the caller owns the Tree and must release it with ast.deinit. @@ -178,13 +185,20 @@ parse_parameters proc( state @mut State, params @mut arraylist.ArrayList(ast.NodeId), ) void ! ParseError { skip_newlines(state) - names arraylist.ArrayList(lexer.Token) := arraylist.init(state.allocator) + names arraylist.ArrayList(ParameterName) := arraylist.init(state.allocator) defer arraylist.deinit(&names) while !next_is(state, .close_paren) { arraylist.clear(&names) while true { + start :: current(state).span.start + comptime :: next_is(state, .dollar) + if (comptime) state.next_token += 1 token :: try consume(state, .ident) - try arraylist.append(&names, token) + try arraylist.append(&names, ParameterName{ + token = token, + start = start, + comptime = comptime, + }) if (starts_type(current(state).kind)) break _ = try consume(state, .comma) } @@ -193,11 +207,12 @@ parse_parameters proc( for (names.items) |parameter_name| { param_id :: try ast.add_param( state.builder, - ast.Span{ start = parameter_name.span.start, end = type_end, source = parameter_name.span.source }, - parameter_name.span, - parameter_name.data.str_id, + ast.Span{ start = parameter_name.start, end = type_end, source = parameter_name.token.span.source }, + parameter_name.token.span, + parameter_name.token.data.str_id, type_id, ) + if (parameter_name.comptime) ast.set_param_comptime(state.builder, param_id) try arraylist.append(params, param_id) } skip_newlines(state) @@ -733,25 +748,7 @@ 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)) { - 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 = state.tokens[state.next_token - 1].span.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) - } + if (next_is(state, .proc)) return try parse_function_type(state, token.span, mutable or token.kind == .star) element :: try parse_type_atom(state) return try ast.add_pointer_type(state.builder, ast.Span{ start = token.span.start, @@ -799,13 +796,19 @@ 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, .enum) return try parse_enum_type(state) if next_is(state, .union) return try parse_union_type(state) if (next_is(state, .distinct) or next_is(state, .alias)) return try parse_type_wrapper(state) token :: try consume(state, .ident) type := try ast.add_identifier(state.builder, token.span, token.data.str_id) - while next_is(state, .dot) { + while true { + if (next_is(state, .open_paren)) { + type = try parse_call(state, type, token.span.start, false) + continue + } + if (!next_is(state, .dot)) break state.next_token += 1 member :: try consume_member(state, false) member_id :: try ast.add_identifier(state.builder, member.span, member.data.str_id) @@ -818,12 +821,33 @@ parse_type_atom proc(state @mut State) ast.NodeId ! ParseError { return type } +@hide:file +parse_function_type proc(state @mut State, start ast.Span, invalid bool) ast.NodeId ! ParseError { + _ = try consume(state, .proc) + _ = 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 = start.start, + end = state.tokens[state.next_token - 1].span.end, + source = start.source, + } + if (invalid) { + 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) +} + @hide:file starts_type proc(kind lexer.TokenKind) bool { return ( kind == .ident or kind == .open_bracket or kind == .struct or kind == .enum or kind == .union or kind == .distinct or kind == .alias - or kind == .at or kind == .star or kind == .question or kind == .open_paren + or kind == .at or kind == .star or kind == .question or kind == .open_paren or kind == .proc ) } @@ -1576,7 +1600,7 @@ 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: return try parse_type(state) + .at, .star, .question, .proc: return try parse_type(state) .struct: return try parse_record_type(state) .enum: return try parse_enum_type(state) .union: return try parse_union_type(state) diff --git a/source/parser/parse.test.hon b/source/parser/parse.test.hon index 9731e09..0bcffa4 100644 --- a/source/parser/parse.test.hon +++ b/source/parser/parse.test.hon @@ -107,10 +107,95 @@ parses_function_structure_through_tree_views test { try testing.expect_equal("U", strpool.get_str(&strings, ast.name_id(&tree, third.type))?) try testing.expect_equal(first.type, second.type) try testing.expect(first.type != third.type) + try testing.expect(!first.comptime) + try testing.expect(!second.comptime) + try testing.expect(!third.comptime) + try testing.expect(!ast.function_generic(&tree, declaration)) try testing.expect_equal(1, ast.block_statement_count(&tree, function.body)) try testing.expect_equal(0, diagnostics.count(&diagnostic_store)) } +parses_compile_time_parameter_groups_and_callbacks test { + strings := strpool.init(mem.c_allocator) + defer strpool.deinit(&strings) + diagnostic_store := diagnostics.init(mem.c_allocator) + defer diagnostics.deinit(&diagnostic_store) + source :: + `generic proc($T type, $K, $V type, $N usize, $first, second T, $build proc(value T) T, legacy @proc(value T) T) T {} + 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)) + declaration :: ast.package_decl(&tree, 0) + try testing.expect(ast.function_generic(&tree, declaration)) + expected [8]bool :: [true, true, true, true, true, false, true, false] + try testing.expect_equal(expected.len, ast.function_param_count(&tree, declaration)) + for (expected) |comptime, index| { + parameter :: ast.param(&tree, ast.function_param(&tree, declaration, index)) + try testing.expect_equal(comptime, parameter.comptime) + } + first :: ast.param(&tree, ast.function_param(&tree, declaration, 0)) + key :: ast.param(&tree, ast.function_param(&tree, declaration, 1)) + value :: ast.param(&tree, ast.function_param(&tree, declaration, 2)) + count :: ast.param(&tree, ast.function_param(&tree, declaration, 3)) + try testing.expect_equal("T", ast.source_text(&tree, first.name)) + try testing.expect_equal("$T type", ast.source_text(&tree, first.span)) + try testing.expect_equal("$K, $V type", ast.source_text(&tree, key.span)) + try testing.expect_equal("$V type", ast.source_text(&tree, value.span)) + try testing.expect_equal(key.type, value.type) + try testing.expect_equal("usize", ast.source_text(&tree, ast.node_span(&tree, count.type))) + callback :: ast.param(&tree, ast.function_param(&tree, declaration, 6)).type + legacy :: ast.param(&tree, ast.function_param(&tree, declaration, 7)).type + try testing.expect_equal(ast.ExprKind.function_type, ast.expr_kind(&tree, callback)) + try testing.expect_equal(ast.ExprKind.function_type, ast.expr_kind(&tree, legacy)) + try testing.expect_equal("proc(value T) T", ast.source_text(&tree, ast.node_span(&tree, callback))) + try testing.expect_equal("@proc(value T) T", ast.source_text(&tree, ast.node_span(&tree, legacy))) + try testing.expect_equal(1, ast.function_param_count(&tree, callback)) + try testing.expect(!ast.param(&tree, ast.function_param(&tree, callback, 0)).comptime) +} + +parses_dependent_type_calls_in_annotations_and_constructors test { + strings := strpool.init(mem.c_allocator) + defer strpool.deinit(&strings) + diagnostic_store := diagnostics.init(mem.c_allocator) + defer diagnostics.deinit(&diagnostic_store) + source :: + `example proc($T type, $N usize, input library.Buffer(T, N)) library.ArrayList(T) { + ` direct library.Buffer(T, N) :: library.Buffer(T, N){} + ` chained library.Factory(T).Buffer(N) :: library.Factory(T).Buffer(N){} + `} + 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)) + declaration :: ast.package_decl(&tree, 0) + function :: ast.function_decl(&tree, declaration) + input :: ast.param(&tree, ast.function_param(&tree, declaration, 2)) + try testing.expect_equal(ast.ExprKind.call, ast.expr_kind(&tree, input.type)) + try testing.expect_equal(2, ast.call_arg_count(&tree, input.type)) + try testing.expect_equal("library.Buffer", ast.source_text(&tree, ast.node_span(&tree, ast.call_expr(&tree, input.type).callee))) + try testing.expect_equal("T", ast.source_text(&tree, ast.node_span(&tree, ast.call_arg(&tree, input.type, 0)))) + try testing.expect_equal("N", ast.source_text(&tree, ast.node_span(&tree, ast.call_arg(&tree, input.type, 1)))) + try testing.expect_equal(ast.ExprKind.call, ast.expr_kind(&tree, function.return_type)) + try testing.expect_equal(1, ast.call_arg_count(&tree, function.return_type)) + direct :: ast.statement_decl(&tree, ast.block_statement(&tree, function.body, 0)) + try testing.expect_equal(ast.ExprKind.call, ast.expr_kind(&tree, direct.type?)) + constructor :: ast.record_literal(&tree, direct.initializer).type? + try testing.expect_equal(ast.ExprKind.call, ast.expr_kind(&tree, constructor)) + try testing.expect_equal("library.Buffer(T, N)", ast.source_text(&tree, ast.node_span(&tree, constructor))) + chained :: ast.statement_decl(&tree, ast.block_statement(&tree, function.body, 1)) + selected :: ast.selection_expr(&tree, ast.call_expr(&tree, chained.type?).callee) + try testing.expect_equal(ast.ExprKind.call, ast.expr_kind(&tree, selected.base)) + try testing.expect_equal("Buffer", ast.source_text(&tree, ast.node_span(&tree, selected.member))) + try testing.expect_equal(1, ast.call_arg_count(&tree, chained.type?)) +} + recovers_invalid_declaration_and_preserves_sibling test { strings := strpool.init(mem.c_allocator) defer strpool.deinit(&strings) @@ -1355,7 +1440,7 @@ preserves_array_error_ids_control_bodies_and_following_siblings test { defer diagnostics.deinit(&diagnostic_store) source :: `main proc() void { - ` bad [$]i64 :: [$, 2] + ` bad [~]i64 :: [~, 2] ` while (items[0) { break } ` for items[0 |i| { continue } ` unfinished :: [1, diff --git a/source/sema/check/check.hon b/source/sema/check/check.hon index 890a740..7b23602 100644 --- a/source/sema/check/check.hon +++ b/source/sema/check/check.hon @@ -234,6 +234,17 @@ check proc( tree @ast.Tree, resolution @sema.Resolution, diagnostic_store @mut diagnostics.Store, +) Result ! (mem.AllocError | diagnostics.CapacityError) { + return try check_elaborated(allocator, tree, resolution, &[], diagnostic_store) +} + +#! checks additional compile-time expressions retained when elaboration erased their calls. +check_elaborated proc( + allocator mem.Allocator, + tree @ast.Tree, + resolution @sema.Resolution, + constraints []ast.NodeId, + diagnostic_store @mut diagnostics.Store, ) Result ! (mem.AllocError | diagnostics.CapacityError) { count :: ast.node_count(tree) types :: try mem.alloc(Type, allocator, count) @@ -305,6 +316,12 @@ check proc( .import: {} } } + for (constraints) |constraint| { + if (ast.node_class(tree, constraint) == .statement) { + state.cells[usize(constraint)].local = true + try collect_declaration(&state, constraint) + } else try collect_expression(&state, constraint) + } # Preserve binding errors before RHS errors propagate. Indexed writability # waits for shapes; neither check poisons the target's type. for 0..count |i| { @@ -1127,6 +1144,14 @@ collect_expression proc(state @mut State, id ast.NodeId) void ! (mem.AllocError state.cells[usize(id)].in_cleanup = state.in_cleanup state.cells[usize(id)].narrowing_arm = state.narrowing_arm match ast.expr_kind(tree, id) { + .typed_constant: { + value :: ast.typed_constant(tree, id) + state.cells[usize(id)].fixed = true + state.cells[usize(id)].type = try check_annotation(state, value.type, false) + try collect_expression(state, value.value) + try depend(state, value.type, id) + try depend(state, value.value, id) + } .try_expr: { operand :: ast.type_operand(tree, id) try collect_expression(state, operand) @@ -1371,6 +1396,7 @@ builtin_kind proc(type sema.BuiltinType) TypeKind { .string: .string .void: .void .anyopaque: .anyopaque + .type: .type_value .c_int: .i32 .c_uint: .u32 .c_long, .c_longlong: .int @@ -1886,6 +1912,10 @@ validate proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnost } .expression: { match ast.expr_kind(tree, id) { + .typed_constant: { + value :: ast.typed_constant(tree, id) + try compatible(state, value.value, state.cells[usize(id)].type) + } .try_expr, .catch_expr: try validate_fallible_expression(state, id) .unary: { expression :: ast.unary_expr(tree, id) @@ -2114,8 +2144,9 @@ evaluate_integer proc(state @mut State, id ast.NodeId, allow_local bool) ?Consta state.cells[usize(id)].constant_visiting = true defer state.cells[usize(id)].constant_visiting = false match ast.expr_kind(state.tree, id) { + .typed_constant: return evaluate_integer(state, ast.typed_constant(state.tree, id).value, allow_local) .literal_int: { - text :: ast.source_text(state.tree, ast.node_span(state.tree, id)) + text :: ast.literal_text(state.tree, id) value := u64(0) for text |byte| { if (byte < '0' or byte > '9') return null @@ -2935,6 +2966,10 @@ apply_contexts proc(state @mut State, assignment_values, early bool) void ! (mem for 0..(state.cells.len) |i| { id :: ast.NodeId(i) if (state.cells[i].role == .expression and state.cells[i].assignment_value == assignment_values) { + if (ast.expr_kind(tree, id) == .typed_constant) { + value :: ast.typed_constant(tree, id) + try context_demand(state, value.value, state.cells[i].type, .type_mismatch, early) + } if (ast.expr_kind(tree, id) == .match_expr) _ = try infer_match(state, id, false) if (ast.expr_kind(tree, id) == .binary) { expression :: ast.binary_expr(tree, id) @@ -3945,7 +3980,7 @@ constant_order proc(left, right ConstantInteger) i32 { constant_real proc(state @mut State, id ast.NodeId) ?f64 ! mem.AllocError { missing ?f64 :: null if (ast.expr_kind(state.tree, id) == .literal_float) { - return strconv.parse_float(state.edges.allocator, ast.source_text(state.tree, ast.node_span(state.tree, id))) catch |err| { + return strconv.parse_float(state.edges.allocator, ast.literal_text(state.tree, id)) catch |err| { match err { .out_of_memory: return err .invalid_number, .out_of_range: return missing @@ -3994,7 +4029,7 @@ pattern_interval proc(state @mut State, id ast.NodeId, key Type) ?PatternInterva if (try constant_real(state, high)) |value| interval.real_high = value else return missing } else if (key.kind == .bool) { if (ast.expr_kind(state.tree, low) != .literal_bool or low != high) return missing - value :: mem.eql(ast.source_text(state.tree, ast.node_span(state.tree, low)), "true") + value :: mem.eql(ast.literal_text(state.tree, low), "true") interval.low.magnitude = if (value) 1 else 0 interval.high = interval.low } else { diff --git a/source/sema/resolve.hon b/source/sema/resolve.hon index 1f52162..5e52840 100644 --- a/source/sema/resolve.hon +++ b/source/sema/resolve.hon @@ -17,6 +17,7 @@ BuiltinType :: enum { anyopaque c_int, c_uint, c_long, c_ulong, c_longlong, c_ulonglong c_char, c_uchar, c_short, c_ushort + type } @hide:file @@ -55,6 +56,7 @@ builtins [_]Builtin :: [ { name = "c_uchar", type = .c_uchar }, { name = "c_short", type = .c_short }, { name = "c_ushort", type = .c_ushort }, + { name = "type", type = .type }, ] @hide:file @@ -275,6 +277,12 @@ binding proc(resolution @Resolution, id ast.NodeId) Binding { return resolution.storage.bindings[usize(id)] } +#! replaces owned binding storage after AST elaboration; takes ownership of `bindings`. +replace_bindings proc(resolution @mut Resolution, bindings []mut Binding) void { + mem.free(resolution.storage.allocator, resolution.storage.bindings) + resolution.storage.bindings = bindings +} + @hide:file lookup proc(state @State, scope ?@Scope, name strpool.StringId) ?Binding { if scope |current| { @@ -335,13 +343,17 @@ resolve_function proc( ) void ! (mem.AllocError | diagnostics.CapacityError) { tree :: state.tree function :: ast.function_decl(tree, id) - # signatures cannot see parameters or body-local declarations. - try resolve_expr(state, package_scope, function.return_type) + # compile-time parameters are in scope for subsequent dependent annotations. + signature := Scope{ names = hashmap.init(state.allocator), parent = package_scope } + defer hashmap.deinit(&signature.names) for (0..ast.function_param_count(tree, id)) |i| { parameter :: ast.function_param(tree, id, i) - try resolve_expr(state, package_scope, ast.param(tree, parameter).type) + try resolve_expr(state, &signature, ast.param(tree, parameter).type) + if (ast.param(tree, parameter).comptime) { + try predeclare(state, &signature, parameter, ast.param(tree, parameter).name) + } } - + try resolve_expr(state, &signature, function.return_type) scope := Scope{ names = hashmap.init(state.allocator), parent = package_scope, @@ -514,6 +526,11 @@ resolve_expr proc( if (state.bindings[usize(id)].kind != .none) return tree :: state.tree match ast.expr_kind(tree, id) { + .typed_constant: { + value :: ast.typed_constant(tree, id) + try resolve_expr(state, scope, value.type) + try resolve_expr(state, scope, value.value) + } .literal_int, .literal_float, .literal_bool, .literal_string, .literal_null: {} .enum_literal: {} .distinct_type, .alias_expr, .optional_type, .try_expr: try resolve_expr(state, scope, ast.type_operand(tree, id)) diff --git a/source/sema/resolve.test.hon b/source/sema/resolve.test.hon index ca8443d..80ce979 100644 --- a/source/sema/resolve.test.hon +++ b/source/sema/resolve.test.hon @@ -719,7 +719,7 @@ resolves_every_array_child_error_and_preserves_lexer_diagnostics test { ` return ` data [missing_count]MissingElement :: [absent, missing_element] ` (missing_base[missing_index])[missing_outer] = missing_rhs - ` malformed [$]i64 :: [$] + ` malformed [~]i64 :: [~] `} scan_state := lexer.init(mem.c_allocator) defer lexer.deinit(&scan_state) diff --git a/source/sema/specialize/specialize.hon b/source/sema/specialize/specialize.hon new file mode 100644 index 0000000..18be0ab --- /dev/null +++ b/source/sema/specialize/specialize.hon @@ -0,0 +1,1241 @@ +import "@std/mem" +import "@std/arraylist" +import "@std/hashmap" +import "@source/ast" +import "@source/diagnostics" +import "@source/strpool" +sema :: import "@source/sema" + +@hide:file no_node :: maxval!(ast.NodeId) +@hide:file no_index :: maxval!(usize) + +@hide:file +Error :: alias mem.AllocError | diagnostics.CapacityError + +@hide:file +ValueKind :: enum { + invalid + type + integer + boolean + string + function + null +} + +@hide:file +Value :: struct { + kind ValueKind = .invalid + node ast.NodeId = no_node + magnitude u64 = 0 + negative bool = false + boolean bool = false + annotation ast.NodeId = no_node +} + +@hide:file +Instance :: struct { + function ast.NodeId + arguments []mut Value + declaration ast.NodeId = no_node + type ast.NodeId = no_node + next usize = no_index + diagnostic diagnostics.DiagnosticId = diagnostics.no_diagnostic + signature ast.NodeId = no_node +} + +@hide:file +Rewrite :: struct { target ast.NodeId, saved ast.NodeId } + +#! owns an undo log and retained checking obligations; generated nodes belong to the caller's AST arena. +Result :: struct { + builder @mut ast.Builder + changes arraylist.ArrayList(Rewrite) + constraints arraylist.ArrayList(ast.NodeId) +} + +#! restores source nodes so the same compilation may be compiled again. +deinit proc(result @mut Result) void { + index := result.changes.items.len + while (index > 0) { + index -= 1 + change :: result.changes.items[index] + ast.replace_node(result.builder, change.target, change.saved) + } + arraylist.deinit(&result.changes) + arraylist.deinit(&result.constraints) +} + +@hide:file +State :: struct { + allocator mem.Allocator + builder @mut ast.Builder + tree ast.Tree + bindings arraylist.ArrayList(sema.Binding) + declarations arraylist.ArrayList(ast.NodeId) + instances arraylist.ArrayList(Instance) + cache hashmap.AutoHashMap(usize, usize) + expanded arraylist.ArrayList(u8) + diagnostics @mut diagnostics.Store + depth usize = 0 + original_count usize + rewrites @mut Result + mapping []mut ast.NodeId +} + +#! elaborates templates into cached concrete declarations in the caller-owned arena. +#! resolution is extended to cover generated nodes; no template reaches checking or lowering. +elaborate proc( + allocator mem.Allocator, + builder @mut ast.Builder, + root ast.NodeId, + resolution @mut sema.Resolution, + diagnostic_store @mut diagnostics.Store, +) Result ! Error { + result := Result{ builder = builder, changes = arraylist.init(allocator), constraints = arraylist.init(allocator) } + errdefer deinit(&result) + original :: ast.view(builder, root) + generic := false + for (0..ast.package_decl_count(&original)) |i| { + id :: ast.package_decl(&original, i) + if (ast.decl_kind(&original, id) != .function) continue + result_type :: sema.binding(resolution, ast.function_decl(&original, id).return_type) + if (ast.function_generic(&original, id) + or (result_type.kind == .builtin and result_type.data.builtin == .type)) { + generic = true + break + } + } + if (!generic) return result + mapping :: try mem.alloc(ast.NodeId, allocator, ast.node_count(&original)) + defer mem.free(allocator, mapping) + state := State{ + allocator = allocator, builder = builder, tree = ast.view(builder, root), + bindings = arraylist.init(allocator), declarations = arraylist.init(allocator), + instances = arraylist.init(allocator), cache = hashmap.init(allocator), + expanded = arraylist.init(allocator), diagnostics = diagnostic_store, + original_count = ast.node_count(&original), + rewrites = &result, + mapping = mapping, + } + defer arraylist.deinit(&state.bindings) + defer arraylist.deinit(&state.declarations) + defer arraylist.deinit(&state.expanded) + defer hashmap.deinit(&state.cache) + defer { + for (state.instances.items) |instance| mem.free(allocator, instance.arguments) + arraylist.deinit(&state.instances) + } + count :: ast.node_count(&state.tree) + for (0..count) |i| try arraylist.append(&state.bindings, sema.binding(resolution, ast.NodeId(i))) + for (0..ast.package_decl_count(&state.tree)) |i| { + id :: ast.package_decl(&state.tree, i) + if (ast.decl_kind(&state.tree, id) == .function and template(&state, id)) continue + try arraylist.append(&state.declarations, id) + } + index := 0 + while (index < state.declarations.items.len) { + try expand(&state, state.declarations.items[index]) + index += 1 + } + try remember(&state, root) + try ast.set_package_declarations(builder, root, state.declarations.items) + try refresh(&state) + owned :: try mem.alloc(sema.Binding, allocator, state.bindings.items.len) + memcopy!(owned, state.bindings.items) + sema.replace_bindings(resolution, owned) + return result +} + +@hide:file +refresh proc(state @mut State) void ! mem.AllocError { + state.tree = ast.view(state.builder, state.tree.root) + count :: ast.node_count(&state.tree) + while (state.bindings.items.len < count) try arraylist.append(&state.bindings, sema.Binding{}) + while (state.expanded.items.len < count) try arraylist.append(&state.expanded, u8(0)) +} + +@hide:file +binding proc(state @State, id ast.NodeId) sema.Binding { + if (usize(id) >= state.bindings.items.len) return sema.Binding{} + return state.bindings.items[usize(id)] +} + +@hide:file +reject proc(state @mut State, id ast.NodeId, code diagnostics.Code) void ! Error { + span :: ast.node_span(&state.tree, id) + diagnostic :: try diagnostics.add(state.diagnostics, code, span) + invalid :: try ast.add_invalid_expr(state.builder, span, diagnostic) + try replace(state, id, invalid) + try refresh(state) + state.bindings.items[usize(id)] = sema.Binding{ kind = .invalid, data = sema.BindingData{ diagnostic = diagnostic } } +} + +@hide:file +expand proc(state @mut State, id ast.NodeId) void ! Error { + try refresh(state) + if (state.expanded.items[usize(id)] != 0) return + state.expanded.items[usize(id)] = 1 + class :: ast.node_class(&state.tree, id) + if (class == .expression and ast.expr_kind(&state.tree, id) == .call) { + callee :: ast.call_expr(&state.tree, id).callee + try expand(state, callee) + resolved :: binding(state, callee) + if (resolved.kind == .declaration + and ast.node_class(&state.tree, resolved.data.declaration) == .function + and template(state, resolved.data.declaration)) { + try specialize_call(state, id, resolved.data.declaration) + return + } + } + count :: ast.child_count(&state.tree, id) + for (0..count) |i| { + child :: ast.child(&state.tree, id, i) + try expand(state, child) + } + if (usize(id) >= state.original_count and class == .expression and ast.expr_kind(&state.tree, id) == .array_type) { + view :: ast.array_type(&state.tree, id) + if (view.count) |count_node| { + try arraylist.append(&state.rewrites.constraints, count_node) + value :: try evaluate(state, count_node) + if (value.kind == .integer) { + folded :: try materialize(state, value) + replacement :: try ast.add_array_type(state.builder, ast.node_span(&state.tree, id), + folded.node, view.element, view.mutable) + try replace(state, id, replacement) + try refresh(state) + } + } + } +} + +@hide:file +builtin proc(state @mut State, at ast.NodeId, kind sema.BuiltinType) ast.NodeId ! Error { + id :: try ast.add_identifier(state.builder, ast.node_span(&state.tree, at), strpool.no_str) + try refresh(state) + state.bindings.items[usize(id)] = sema.Binding{ kind = .builtin, data = sema.BindingData{ builtin = kind } } + return id +} + +@hide:file +reference proc(state @mut State, at, target ast.NodeId) ast.NodeId ! Error { + id :: try ast.add_identifier(state.builder, ast.node_span(&state.tree, at), strpool.no_str) + try refresh(state) + state.bindings.items[usize(id)] = sema.Binding{ kind = .declaration, data = sema.BindingData{ declaration = target } } + return id +} + +@hide:file +is_type proc(state @State, id ast.NodeId) bool { + value :: binding(state, id) + return value.kind == .builtin and value.data.builtin == .type +} + +@hide:file +specialize_call proc(state @mut State, call, function ast.NodeId) void ! Error { + if (state.depth >= 128 or state.instances.items.len >= 4096) { + try reject(state, call, .specialization_limit) + return + } + state.depth += 1 + defer state.depth -= 1 + count :: ast.function_param_count(&state.tree, function) + runtime := usize(0) + for (0..count) |i| { + if (!ast.param(&state.tree, ast.function_param(&state.tree, function, i)).comptime) runtime += 1 + } + if (runtime != 0 and is_type(state, ast.function_decl(&state.tree, function).return_type)) { + try reject(state, call, .comptime_argument_required) + return + } + supplied :: ast.call_arg_count(&state.tree, call) + explicit :: supplied == count + if (!explicit and supplied != runtime) { + try reject(state, call, .argument_count_mismatch) + return + } + arguments :: try mem.alloc(Value, state.allocator, count) + defer mem.free(state.allocator, arguments) + mem.set(arguments, Value{}) + runtime_arguments := arraylist.init(ast.NodeId, state.allocator) + defer arraylist.deinit(&runtime_arguments) + position := 0 + for (0..count) |i| { + parameter :: ast.function_param(&state.tree, function, i) + view :: ast.param(&state.tree, parameter) + if (view.comptime and !explicit) continue + argument :: ast.call_arg(&state.tree, call, position) + position += 1 + try expand(state, argument) + if (view.comptime) { + arguments[i] = try evaluate(state, argument) + if (arguments[i].kind == .invalid) { + try reject(state, call, .comptime_argument_required) + return + } + if (is_type(state, view.type) != (arguments[i].kind == .type)) { + try reject(state, call, .type_mismatch) + return + } + } else { + try arraylist.append(&runtime_arguments, argument) + } + } + if (!explicit) { + for (0..usize(2)) |phase| { + position = 0 + for (0..count) |i| { + parameter :: ast.function_param(&state.tree, function, i) + view :: ast.param(&state.tree, parameter) + if (view.comptime) continue + argument :: runtime_arguments.items[position] + position += 1 + contextual :: flexible(state, argument) + if (contextual != (phase == 1)) continue + actual :: try expression_type(state, argument, 0) + if (actual == no_node or !try infer(state, function, view.type, actual, arguments, contextual)) { + try reject(state, call, .cannot_infer_generic) + return + } + } + } + } + for (0..count) |i| { + parameter :: ast.function_param(&state.tree, function, i) + if (ast.param(&state.tree, parameter).comptime and arguments[i].kind == .invalid) { + try reject(state, call, .cannot_infer_generic) + return + } + } + key := usize(function) + for arguments |argument| { + if (argument.kind != .invalid) key = mix(key, try value_hash(state, argument)) + } + head := no_index + if (hashmap.get(&state.cache, key)) |found| head = found + current := head + instance := no_index + while (current != no_index) { + entry :: state.instances.items[current] + equal := entry.function == function and entry.arguments.len == count + if (equal) { + for (0..count) |i| { + if (!try value_equal(state, entry.arguments[i], arguments[i])) { + equal = false + break + } + } + } + if (equal) { + instance = current + break + } + current = entry.next + } + if (instance == no_index) { + for (0..count) |i| { + if (arguments[i].kind != .invalid) arguments[i] = try materialize(state, arguments[i]) + } + values :: try mem.alloc(Value, state.allocator, count) + memcopy!(values, arguments) + instance = state.instances.items.len + arraylist.append(&state.instances, Instance{ function = function, arguments = values, next = head }) catch |err| { + mem.free(state.allocator, values) + return err + } + # cache heads are replaced without reallocating a second entry for the same hash. + if (head != no_index) { + for (state.cache.entries) |@entry| { + if (entry.hash != 0 and entry.key == key) { + entry.value = instance + break + } + } + } else { + hashmap.put(&state.cache, key, instance) catch |err| { + match err { + .key_exists: unreachable + else: return err + } + } + } + try instantiate(state, instance, call) + if (binding(state, call).kind == .invalid) { + state.instances.items[instance].diagnostic = binding(state, call).data.diagnostic + return + } + } + diagnostic :: state.instances.items[instance].diagnostic + if (diagnostic != diagnostics.no_diagnostic) { + invalid :: try ast.add_invalid_expr(state.builder, ast.node_span(&state.tree, call), diagnostic) + try replace(state, call, invalid) + try refresh(state) + state.bindings.items[usize(call)] = sema.Binding{ kind = .invalid, data = sema.BindingData{ diagnostic = diagnostic } } + return + } + target :: state.instances.items[instance].declaration + if (target == no_node) { + try reject(state, call, .specialization_cycle) + return + } + if (explicit) { + signature :: state.instances.items[instance].signature + for (0..count) |i| { + parameter :: ast.function_param(&state.tree, signature, i) + view :: ast.param(&state.tree, parameter) + if (!view.comptime or is_type(state, view.type)) continue + original :: ast.call_arg(&state.tree, call, i) + constraint :: try ast.add_typed_constant(state.builder, ast.node_span(&state.tree, original), view.type, original) + try refresh(state) + try arraylist.append(&state.rewrites.constraints, constraint) + } + } + callee :: try reference(state, call, target) + replacement := no_node + if (state.instances.items[instance].type != no_node) { + replacement = try ast.add_type_operand(state.builder, ast.node_span(&state.tree, call), callee, true) + } else { + replacement = try ast.add_call(state.builder, ast.node_span(&state.tree, call), callee, runtime_arguments.items) + } + try replace(state, call, replacement) + try refresh(state) +} + +@hide:file +instantiate proc(state @mut State, index usize, call ast.NodeId) void ! Error { + function :: state.instances.items[index].function + count :: state.original_count + clone_start :: ast.node_count(&state.tree) + mapping :: state.mapping + mem.set(mapping, no_node) + parameters :: ast.function_param_count(&state.tree, function) + for (0..parameters) |i| { + parameter :: ast.function_param(&state.tree, function, i) + if (!ast.param(&state.tree, parameter).comptime) continue + value :: state.instances.items[index].arguments[i] + replacement := value.node + annotation :: ast.param(&state.tree, parameter).type + if (!is_type(state, annotation)) { + concrete_type :: try ast.clone_subtree(state.builder, annotation, mapping) + replacement = try ast.add_typed_constant(state.builder, ast.node_span(&state.tree, parameter), concrete_type, value.node) + try refresh(state) + } + for (0..count) |n| { + resolved :: binding(state, ast.NodeId(n)) + if (resolved.kind == .declaration and resolved.data.declaration == parameter + and ast.node_class(&state.tree, ast.NodeId(n)) == .expression) mapping[n] = replacement + } + } + clone :: try ast.clone_subtree(state.builder, function, mapping) + try refresh(state) + # Copy original resolution, remapping references to cloned locals and runtime parameters. + for (0..count) |i| { + target :: mapping[i] + if (target == no_node or usize(target) < clone_start) continue + if (ast.node_class(&state.tree, target) == .expression and ast.expr_kind(&state.tree, target) == .typed_constant) continue + value := state.bindings.items[i] + if (value.kind == .declaration) { + original :: value.data.declaration + if (original != function and usize(original) < count and mapping[usize(original)] != no_node) + value.data.declaration = mapping[usize(original)] + } + state.bindings.items[usize(target)] = value + } + state.instances.items[index].signature = try ast.copy_node(state.builder, clone) + try refresh(state) + view :: ast.function_decl(&state.tree, clone) + for (0..parameters) |i| { + parameter :: ast.function_param(&state.tree, clone, i) + annotation :: ast.param(&state.tree, parameter).type + if (ast.param(&state.tree, parameter).comptime and !is_type(state, annotation)) { + if (!try valid_value(state, annotation, state.instances.items[index].arguments[i])) { + value :: state.instances.items[index].arguments[i] + code :: if (value.kind == .integer and try integer_type(state, annotation)) + diagnostics.Code.literal_out_of_range else diagnostics.Code.type_mismatch + try reject(state, call, code) + return + } + } + } + if (is_type(state, view.return_type)) { + returned :: try return_type_value(state, view.body) + if (returned == no_node) { + try reject(state, call, .comptime_argument_required) + return + } + result := returned + kind :: ast.expr_kind(&state.tree, returned) + if (kind != .record_type and kind != .union_type and kind != .enum_type and kind != .distinct_type) { + result = try ast.add_type_operand(state.builder, ast.node_span(&state.tree, returned), returned, true) + try refresh(state) + } + declaration :: try ast.add_statement_decl(state.builder, view.span, view.name, + ast.name_id(&state.tree, function), null, .constant, result) + try refresh(state) + state.instances.items[index].declaration = declaration + state.instances.items[index].type = returned + state.bindings.items[usize(declaration)] = sema.Binding{ kind = .declaration, data = sema.BindingData{ declaration = declaration } } + try arraylist.append(&state.declarations, declaration) + try expand(state, declaration) + } else { + runtime := arraylist.init(ast.NodeId, state.allocator) + defer arraylist.deinit(&runtime) + for (0..parameters) |i| { + parameter :: ast.function_param(&state.tree, clone, i) + if (!ast.param(&state.tree, parameter).comptime) try arraylist.append(&runtime, parameter) + } + concrete :: try ast.add_function_decl(state.builder, view.span, view.name, + ast.name_id(&state.tree, function), view.return_type, runtime.items, view.body) + ast.replace_node(state.builder, clone, concrete) + try refresh(state) + state.instances.items[index].declaration = clone + state.bindings.items[usize(clone)] = sema.Binding{ kind = .declaration, data = sema.BindingData{ declaration = clone } } + try arraylist.append(&state.declarations, clone) + try expand(state, clone) + } +} + +@hide:file +return_type_value proc(state @mut State, block ast.NodeId) ast.NodeId ! Error { + count :: ast.block_statement_count(&state.tree, block) + for (0..count) |i| { + statement :: ast.block_statement(&state.tree, block, i) + match ast.stmt_kind(&state.tree, statement) { + .return_stmt: { + if (ast.return_stmt(&state.tree, statement).value) |value| return value + return no_node + } + .declaration: { + declaration :: ast.statement_decl(&state.tree, statement) + if (declaration.mutability != .constant) return no_node + value :: try evaluate(state, declaration.initializer) + if (value.kind == .invalid) return no_node + try arraylist.append(&state.rewrites.constraints, statement) + } + .if_stmt: { + conditional :: ast.if_stmt(&state.tree, statement) + boolean :: try builtin(state, conditional.condition, .bool) + constraint :: try ast.add_typed_constant(state.builder, ast.node_span(&state.tree, conditional.condition), + boolean, conditional.condition) + try refresh(state) + try arraylist.append(&state.rewrites.constraints, constraint) + value :: try evaluate(state, conditional.condition) + if (value.kind != .boolean) return no_node + branch := no_node + if (value.boolean) branch = conditional.then_block + else if (conditional.else_block) |other| branch = other + if (branch != no_node) { + result :: try return_type_value(state, branch) + if (result != no_node) return result + } + } + .block_stmt: { + result :: try return_type_value(state, ast.statement_block(&state.tree, statement)) + if (result != no_node) return result + } + else: return no_node + } + } + return no_node +} + +@hide:file +mix proc(left, right usize) usize { + return (left << 7) xor (left >> (sizeof!(usize) * 8 - 7)) xor right +} + +@hide:file +evaluate proc(state @mut State, id ast.NodeId) Value ! Error { + if (state.depth >= 128) return Value{} + state.depth += 1 + defer state.depth -= 1 + try expand(state, id) + kind :: ast.expr_kind(&state.tree, id) + resolved :: binding(state, id) + if (resolved.kind == .builtin and resolved.data.builtin != .type) return Value{ kind = .type, node = id } + if (resolved.kind == .declaration) { + target :: resolved.data.declaration + class :: ast.node_class(&state.tree, target) + if (class == .function) { + if (template(state, target)) return Value{} + return Value{ kind = .function, node = id } + } + if (class == .statement) { + declaration :: ast.statement_decl(&state.tree, target) + if (declaration.mutability != .constant) return Value{} + value := try evaluate(state, declaration.initializer) + if (declaration.type) |annotation| value.annotation = annotation + return value + } + return Value{} + } + match kind { + .typed_constant: { + view :: ast.typed_constant(&state.tree, id) + value := try evaluate(state, view.value) + value.annotation = view.type + return value + } + .literal_int: { + value := u64(0) + for ast.literal_text(&state.tree, id) |byte| { + if (byte < '0' or byte > '9') return Value{} + digit :: u64(byte - '0') + if (value > divtrunc!(maxval!(u64) - digit, u64(10))) return Value{} + value = value * 10 + digit + } + return Value{ kind = .integer, node = id, magnitude = value } + } + .literal_bool: return Value{ kind = .boolean, node = id, + boolean = mem.eql(ast.literal_text(&state.tree, id), "true") } + .literal_string: { + text :: ast.literal_text(&state.tree, id) + cursor := usize(1) + while (cursor < text.len - 1) { + _ = ast.string_next(text, &cursor) catch { + try reject(state, id, .invalid_escape) + return Value{} + } + } + return Value{ kind = .string, node = id } + } + .literal_null: return Value{ kind = .null, node = id } + .record_type, .array_type, .slice_type, .pointer_type, .function_type, .optional_type, + .enum_type, .union_type, .distinct_type, .fallible_type, .sum_type: + return Value{ kind = .type, node = id } + .alias_expr: return try evaluate(state, ast.type_operand(&state.tree, id)) + .unary: { + view :: ast.unary_expr(&state.tree, id) + value := try evaluate(state, view.operand) + if (view.operator == .negate and value.kind == .integer) { + value.negative = value.magnitude != 0 and !value.negative + } else if (view.operator == .logical_not and value.kind == .boolean) { + value.boolean = !value.boolean + } else return Value{} + value.node = id + return value + } + .binary: return try evaluate_binary(state, id) + else: return Value{} + } +} + +@hide:file +evaluate_binary proc(state @mut State, id ast.NodeId) Value ! Error { + view :: ast.binary_expr(&state.tree, id) + left :: try evaluate(state, view.left) + if (left.kind == .boolean) { + if (view.operator == .logical_and and !left.boolean) return Value{ kind = .boolean, node = id, boolean = false } + if (view.operator == .logical_or and left.boolean) return Value{ kind = .boolean, node = id, boolean = true } + } + right := try evaluate(state, view.right) + if (left.kind == .invalid or right.kind == .invalid) return Value{} + if (left.annotation != no_node and right.annotation != no_node + and left.kind == .integer and right.kind == .integer + and !try same_type(state, left.annotation, right.annotation)) return Value{} + if (view.operator == .equal or view.operator == .not_equal) { + if (left.kind != right.kind) return Value{} + equal :: try value_equal(state, left, right) + return Value{ kind = .boolean, node = id, boolean = equal == (view.operator == .equal) } + } + if (left.kind == .boolean and right.kind == .boolean) { + if (view.operator == .logical_and) return Value{ kind = .boolean, node = id, boolean = left.boolean and right.boolean } + if (view.operator == .logical_or) return Value{ kind = .boolean, node = id, boolean = left.boolean or right.boolean } + return Value{} + } + if (left.kind != .integer or right.kind != .integer) return Value{} + annotation :: if (left.annotation != no_node) left.annotation else right.annotation + less :: if (left.negative != right.negative) left.negative + else if (left.negative) left.magnitude > right.magnitude else left.magnitude < right.magnitude + equal :: left.magnitude == right.magnitude and left.negative == right.negative + match view.operator { + .less: return Value{ kind = .boolean, node = id, boolean = less } + .less_equal: return Value{ kind = .boolean, node = id, boolean = less or equal } + .greater: return Value{ kind = .boolean, node = id, boolean = !less and !equal } + .greater_equal: return Value{ kind = .boolean, node = id, boolean = !less } + .multiply: { + if (right.magnitude != 0 and left.magnitude > divtrunc!(maxval!(u64), right.magnitude)) return Value{} + magnitude :: left.magnitude * right.magnitude + return try integer_value(state, id, magnitude, magnitude != 0 and left.negative != right.negative, annotation) + } + .add, .subtract: { + if (view.operator == .subtract and right.magnitude != 0) right.negative = !right.negative + if (left.negative == right.negative) { + if (left.magnitude > maxval!(u64) - right.magnitude) return Value{} + return try integer_value(state, id, left.magnitude + right.magnitude, left.negative, annotation) + } + if (left.magnitude < right.magnitude) { + return try integer_value(state, id, right.magnitude - left.magnitude, right.negative, annotation) + } + return try integer_value(state, id, left.magnitude - right.magnitude, + left.magnitude != right.magnitude and left.negative, annotation) + } + else: return Value{} + } +} + +@hide:file +canonical_type proc(state @mut State, id ast.NodeId) ast.NodeId ! Error { + value :: try evaluate(state, id) + return if (value.kind == .type) value.node else no_node +} + +@hide:file +same_type proc(state @mut State, left, right ast.NodeId) bool ! Error { + a :: try canonical_type(state, left) + b :: try canonical_type(state, right) + if (a == no_node or b == no_node) return false + if (a == b) return true + x :: binding(state, a) + y :: binding(state, b) + if (x.kind == .builtin or y.kind == .builtin) { + return x.kind == .builtin and y.kind == .builtin and builtin_identity(x.data.builtin) == builtin_identity(y.data.builtin) + } + kind :: ast.expr_kind(&state.tree, a) + if (kind != ast.expr_kind(&state.tree, b)) return false + match kind { + .pointer_type: { + p :: ast.pointer_type(&state.tree, a) + q :: ast.pointer_type(&state.tree, b) + return p.mutable == q.mutable and p.many == q.many and try same_type(state, p.element, q.element) + } + .array_type: { + p :: ast.array_type(&state.tree, a) + q :: ast.array_type(&state.tree, b) + if (p.mutable != q.mutable or p.count == null or q.count == null) return false + pv :: try evaluate(state, p.count?) + qv :: try evaluate(state, q.count?) + return try value_equal(state, pv, qv) and try same_type(state, p.element, q.element) + } + .slice_type: { + p :: ast.slice_type(&state.tree, a) + q :: ast.slice_type(&state.tree, b) + if (p.mutable != q.mutable or (p.sentinel == null) != (q.sentinel == null)) return false + if (p.sentinel) |sentinel| { + if (!try value_equal(state, try evaluate(state, sentinel), try evaluate(state, q.sentinel?))) return false + } + return try same_type(state, p.element, q.element) + } + .optional_type: return try same_type(state, ast.type_operand(&state.tree, a), ast.type_operand(&state.tree, b)) + .fallible_type, .sum_type: { + p :: ast.binary_expr(&state.tree, a) + q :: ast.binary_expr(&state.tree, b) + return try same_type(state, p.left, q.left) and try same_type(state, p.right, q.right) + } + .function_type: { + count :: ast.function_param_count(&state.tree, a) + if (count != ast.function_param_count(&state.tree, b)) return false + if (!try same_type(state, ast.callable_result(&state.tree, a), ast.callable_result(&state.tree, b))) return false + for (0..count) |i| { + p :: ast.param(&state.tree, ast.function_param(&state.tree, a, i)).type + q :: ast.param(&state.tree, ast.function_param(&state.tree, b, i)).type + if (!try same_type(state, p, q)) return false + } + return true + } + else: return false + } +} + +@hide:file +value_equal proc(state @mut State, left, right Value) bool ! Error { + if (left.kind != right.kind) return false + return match left.kind { + .invalid, .null: true + .type: try same_type(state, left.node, right.node) + .integer: left.magnitude == right.magnitude and left.negative == right.negative + .boolean: left.boolean == right.boolean + .function: binding(state, left.node).data.declaration == binding(state, right.node).data.declaration + .string: try same_string(state, left.node, right.node) + } +} + +@hide:file +value_hash proc(state @mut State, value Value) usize ! Error { + hash :: enum_index(value.kind) + negative :: if (value.negative) usize(1) else usize(0) + boolean :: if (value.boolean) usize(1) else usize(0) + return match value.kind { + .type: mix(hash, try type_hash(state, value.node)) + .integer: mix(mix(hash, usize(value.magnitude)), negative) + .boolean: mix(hash, boolean) + .function: mix(hash, usize(binding(state, value.node).data.declaration)) + .string: string_hash(hash, ast.literal_text(&state.tree, value.node)) + .invalid, .null: hash + } +} + +@hide:file +string_hash proc(seed usize, text []u8) usize { + result := seed + cursor := usize(1) + while (cursor < text.len - 1) { + byte :: ast.string_next(text, &cursor) catch { return 0 } + result = mix(result, usize(byte)) + } + return result +} + +@hide:file +type_hash proc(state @mut State, id ast.NodeId) usize ! Error { + node :: try canonical_type(state, id) + if (node == no_node) return 0 + resolved :: binding(state, node) + if (resolved.kind == .builtin) return mix(1, enum_index(builtin_identity(resolved.data.builtin))) + kind :: ast.expr_kind(&state.tree, node) + hash := enum_index(kind) + match kind { + .pointer_type: { + view :: ast.pointer_type(&state.tree, node) + mutable :: if (view.mutable) usize(1) else usize(0) + many :: if (view.many) usize(1) else usize(0) + hash = mix(hash, mutable) + hash = mix(hash, many) + return mix(hash, try type_hash(state, view.element)) + } + .array_type: { + view :: ast.array_type(&state.tree, node) + mutable :: if (view.mutable) usize(1) else usize(0) + hash = mix(hash, mutable) + if (view.count) |count| hash = mix(hash, try value_hash(state, try evaluate(state, count))) + return mix(hash, try type_hash(state, view.element)) + } + .slice_type: { + view :: ast.slice_type(&state.tree, node) + mutable :: if (view.mutable) usize(1) else usize(0) + hash = mix(hash, mutable) + if (view.sentinel) |sentinel| hash = mix(hash, try value_hash(state, try evaluate(state, sentinel))) + return mix(hash, try type_hash(state, view.element)) + } + .optional_type: return mix(hash, try type_hash(state, ast.type_operand(&state.tree, node))) + .fallible_type, .sum_type: { + view :: ast.binary_expr(&state.tree, node) + return mix(mix(hash, try type_hash(state, view.left)), try type_hash(state, view.right)) + } + .function_type: { + hash = mix(hash, try type_hash(state, ast.callable_result(&state.tree, node))) + for (0..ast.function_param_count(&state.tree, node)) |i| { + parameter :: ast.function_param(&state.tree, node, i) + hash = mix(hash, try type_hash(state, ast.param(&state.tree, parameter).type)) + } + return hash + } + else: return mix(hash, usize(node)) + } +} + +@hide:file +expression_type proc(state @mut State, id ast.NodeId, depth usize) ast.NodeId ! Error { + if (depth >= 128) return no_node + try expand(state, id) + resolved :: binding(state, id) + if (resolved.kind == .declaration) { + target :: resolved.data.declaration + class :: ast.node_class(&state.tree, target) + if (class == .parameter) { + annotation :: ast.param(&state.tree, target).type + return try canonical_type(state, annotation) + } + if (class == .statement) { + declaration :: ast.statement_decl(&state.tree, target) + if (declaration.type) |annotation| return try canonical_type(state, annotation) + return try expression_type(state, declaration.initializer, depth + 1) + } + if (class == .function) { + params := arraylist.init(ast.NodeId, state.allocator) + defer arraylist.deinit(¶ms) + for (0..ast.function_param_count(&state.tree, target)) |i| + try arraylist.append(¶ms, ast.function_param(&state.tree, target, i)) + result :: try ast.add_function_type(state.builder, ast.node_span(&state.tree, id), + ast.function_decl(&state.tree, target).return_type, params.items) + try refresh(state) + return result + } + return no_node + } + kind :: ast.expr_kind(&state.tree, id) + match kind { + .typed_constant: return try canonical_type(state, ast.typed_constant(&state.tree, id).type) + .literal_int: return try builtin(state, id, .i64) + .literal_float: return try builtin(state, id, .f64) + .literal_bool: return try builtin(state, id, .bool) + .literal_string: return try builtin(state, id, .string) + .record_literal: { + view :: ast.record_literal(&state.tree, id) + if (view.type) |annotation| return try canonical_type(state, annotation) + fields := arraylist.init(ast.NodeId, state.allocator) + defer arraylist.deinit(&fields) + for (0..ast.record_field_count(&state.tree, id)) |i| { + field :: ast.field(&state.tree, ast.record_field(&state.tree, id, i)) + if (field.value == null) return no_node + type :: try expression_type(state, field.value?, depth + 1) + if (type == no_node) return no_node + generated :: try ast.add_field(state.builder, field.span, field.name, type, null) + try refresh(state) + try arraylist.append(&fields, generated) + } + result :: try ast.add_record_type(state.builder, ast.node_span(&state.tree, id), fields.items) + try refresh(state) + return result + } + .array_literal: { + count :: ast.array_element_count(&state.tree, id) + if (count == 0) return no_node + element :: try expression_type(state, ast.array_element(&state.tree, id, 0), depth + 1) + if (element == no_node) return no_node + length :: try ast.add_integer(state.builder, ast.node_span(&state.tree, id), u64(count)) + try refresh(state) + result :: try ast.add_array_type(state.builder, ast.node_span(&state.tree, id), length, element, false) + try refresh(state) + return result + } + .call: { + callee :: ast.call_expr(&state.tree, id).callee + destination :: binding(state, callee) + if (destination.kind == .builtin) return callee + if (destination.kind == .declaration and ast.node_class(&state.tree, destination.data.declaration) == .function) { + return try canonical_type(state, ast.function_decl(&state.tree, destination.data.declaration).return_type) + } + signature :: try expression_type(state, callee, depth + 1) + if (signature != no_node and ast.expr_kind(&state.tree, signature) == .function_type) + return try canonical_type(state, ast.callable_result(&state.tree, signature)) + return no_node + } + .unary: { + view :: ast.unary_expr(&state.tree, id) + operand :: try expression_type(state, view.operand, depth + 1) + if (operand == no_node) return no_node + if (view.operator == .address_of) { + mutable := false + target :: binding(state, view.operand) + if (target.kind == .declaration and ast.node_class(&state.tree, target.data.declaration) == .statement) + mutable = ast.statement_decl(&state.tree, target.data.declaration).mutability == .variable + result :: try ast.add_pointer_type(state.builder, view.span, operand, mutable, false) + try refresh(state) + return result + } + if (view.operator == .dereference and ast.expr_kind(&state.tree, operand) == .pointer_type) + return try canonical_type(state, ast.pointer_type(&state.tree, operand).element) + if (view.operator == .unwrap and ast.expr_kind(&state.tree, operand) == .optional_type) + return try canonical_type(state, ast.type_operand(&state.tree, operand)) + return operand + } + .binary: { + view :: ast.binary_expr(&state.tree, id) + if (view.operator == .equal or view.operator == .not_equal or view.operator == .less + or view.operator == .less_equal or view.operator == .greater or view.operator == .greater_equal + or view.operator == .logical_and or view.operator == .logical_or) return try builtin(state, id, .bool) + return try expression_type(state, view.left, depth + 1) + } + .selection: { + view :: ast.selection_expr(&state.tree, id) + base := try expression_type(state, view.base, depth + 1) + if (base == no_node) return no_node + if (ast.expr_kind(&state.tree, base) == .pointer_type) base = try canonical_type(state, ast.pointer_type(&state.tree, base).element) + if (base == no_node) return no_node + member :: ast.source_text(&state.tree, ast.node_span(&state.tree, view.member)) + base_kind :: ast.expr_kind(&state.tree, base) + if (mem.eql(member, "len") and (base_kind == .array_type or base_kind == .slice_type + or (binding(state, base).kind == .builtin and binding(state, base).data.builtin == .string))) + return try builtin(state, id, .usize) + if (base_kind != .record_type and base_kind != .union_type) return no_node + for (0..ast.record_field_count(&state.tree, base)) |i| { + field :: ast.field(&state.tree, ast.record_field(&state.tree, base, i)) + if (field.name) |name| { + if (ast.name_id(&state.tree, name) == ast.name_id(&state.tree, view.member)) + return try canonical_type(state, field.type?) + } + } + return no_node + } + .index: { + view :: ast.index_expr(&state.tree, id) + base :: try expression_type(state, view.base, depth + 1) + if (base == no_node) return no_node + base_kind :: ast.expr_kind(&state.tree, base) + if (base_kind == .array_type) return try canonical_type(state, ast.array_type(&state.tree, base).element) + if (base_kind == .slice_type) return try canonical_type(state, ast.slice_type(&state.tree, base).element) + if (base_kind == .pointer_type) return try canonical_type(state, ast.pointer_type(&state.tree, base).element) + if (binding(state, base).kind == .builtin and binding(state, base).data.builtin == .string) return try builtin(state, id, .u8) + return no_node + } + .slice: { + view :: ast.slice_expr(&state.tree, id) + base :: try expression_type(state, view.base, depth + 1) + if (base == no_node) return no_node + base_kind :: ast.expr_kind(&state.tree, base) + 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 == .pointer_type) { + element = ast.pointer_type(&state.tree, base).element + mutable = ast.pointer_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 (element == no_node) return no_node + result :: try ast.add_slice_type(state.builder, ast.node_span(&state.tree, id), element, mutable, null) + try refresh(state) + return result + } + else: return no_node + } +} + +@hide:file +parameter_index proc(state @State, function, id ast.NodeId) usize { + for (0..ast.function_param_count(&state.tree, function)) |i| { + if (ast.function_param(&state.tree, function, i) == id and ast.param(&state.tree, id).comptime) return i + } + return no_index +} + +@hide:file +infer_value proc(state @mut State, function, pattern ast.NodeId, actual Value, arguments []mut Value, contextual bool) bool ! Error { + resolved :: binding(state, pattern) + if (resolved.kind == .declaration) { + index :: parameter_index(state, function, resolved.data.declaration) + if (index != no_index) { + if (arguments[index].kind == .invalid) { + arguments[index] = actual + return true + } + if (contextual and arguments[index].kind == .type and actual.kind == .type) return true + return try value_equal(state, arguments[index], actual) + } + } + return try value_equal(state, try evaluate(state, pattern), actual) +} + +@hide:file +infer proc(state @mut State, function, pattern, actual ast.NodeId, arguments []mut Value, contextual bool) bool ! Error { + 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) + kind :: ast.expr_kind(&state.tree, pattern) + actual_kind :: ast.expr_kind(&state.tree, actual) + 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) + } + .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 (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) + 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 + return try infer(state, function, ast.type_operand(&state.tree, pattern), element, arguments, contextual) + } + .call: { + callee :: binding(state, ast.call_expr(&state.tree, pattern).callee) + if (callee.kind != .declaration) return false + 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 + count :: ast.call_arg_count(&state.tree, pattern) + if (count != instance.arguments.len) return false + for (0..count) |j| { + if (!try infer_value(state, function, ast.call_arg(&state.tree, pattern, j), instance.arguments[j], arguments, contextual)) return false + } + return true + } + return false + } + else: return true + } +} + +@hide:file +materialize proc(state @mut State, value Value) Value ! Error { + result := value + span :: ast.node_span(&state.tree, value.node) + if (value.kind == .integer) { + result.node = try ast.add_integer(state.builder, span, value.magnitude) + if (value.negative) result.node = try ast.add_unary(state.builder, span, .negate, result.node) + } else if (value.kind == .boolean) { + result.node = try ast.add_boolean(state.builder, span, value.boolean) + } + try refresh(state) + return result +} + +@hide:file +valid_value proc(state @mut State, annotation ast.NodeId, value Value) bool ! Error { + type :: try canonical_type(state, annotation) + if (type == no_node) return false + resolved :: binding(state, type) + if (resolved.kind == .builtin) { + kind :: resolved.data.builtin + if (kind == .bool) return value.kind == .boolean + if (kind == .string) return value.kind == .string + if (value.kind != .integer) return false + bits := u32(64) + signed := false + match kind { + .i8, .c_char: { + bits = 8 + signed = true + } + .i16, .c_short: { + bits = 16 + signed = true + } + .i32, .c_int: { + bits = 32 + signed = true + } + .int, .i64, .isize, .c_long, .c_longlong: signed = true + .u8, .c_uchar: bits = 8 + .u16, .c_ushort: bits = 16 + .u32, .c_uint: bits = 32 + .u64, .uint, .usize, .c_ulong, .c_ulonglong: {} + else: return false + } + limit := maxval!(u64) + if (signed) { + limit = u64(1) << (bits - 1) + if (!value.negative) limit -= 1 + } else { + if (value.negative) return false + if (bits != 64) limit = (u64(1) << bits) - 1 + } + return value.magnitude <= limit + } + kind :: ast.expr_kind(&state.tree, type) + if (kind == .optional_type) return value.kind == .null or try valid_value(state, ast.type_operand(&state.tree, type), value) + if (kind == .slice_type and value.kind == .string) { + view :: ast.slice_type(&state.tree, type) + child :: try canonical_type(state, view.element) + return ( + !view.mutable and child != no_node and binding(state, child).kind == .builtin + and binding(state, child).data.builtin == .u8 + ) + } + if (kind == .function_type and value.kind == .function) { + actual :: try expression_type(state, value.node, 0) + return actual != no_node and try same_type(state, type, actual) + } + if (kind == .distinct_type) return try valid_value(state, ast.type_operand(&state.tree, type), value) + return false +} + +@hide:file +enum_index proc($T type, value T) usize { + match typeinfo!(T) { + .enum |info|: { + inline for (info.fields) |field| { + if (value == field!(T, field.name)) return field.index + } + unreachable + } + else: unreachable + } +} + +@hide:file +remember proc(state @mut State, target ast.NodeId) void ! mem.AllocError { + if (usize(target) >= state.original_count) return + saved :: try ast.copy_node(state.builder, target) + try arraylist.append(&state.rewrites.changes, Rewrite{ target = target, saved = saved }) +} + +@hide:file +replace proc(state @mut State, target, source ast.NodeId) void ! mem.AllocError { + try remember(state, target) + ast.replace_node(state.builder, target, source) +} + +@hide:file +builtin_identity proc(kind sema.BuiltinType) sema.BuiltinType { + return match kind { + .i64, .c_long, .c_longlong: .int + .f64: .float + .uint, .c_ulong, .c_ulonglong: .u64 + .c_char: .i8 + .c_uchar: .u8 + .c_short: .i16 + .c_ushort: .u16 + .c_int: .i32 + .c_uint: .u32 + else: kind + } +} + +@hide:file +integer_type proc(state @mut State, annotation ast.NodeId) bool ! Error { + type :: try canonical_type(state, annotation) + if (type == no_node) return false + resolved :: binding(state, type) + if (resolved.kind == .builtin) { + return match builtin_identity(resolved.data.builtin) { + .int, .i8, .i16, .i32, .isize, .u8, .u16, .u32, .u64, .usize: true + else: false + } + } + if (ast.expr_kind(&state.tree, type) == .distinct_type) return try integer_type(state, ast.type_operand(&state.tree, type)) + return false +} + +@hide:file +integer_value proc(state @mut State, id ast.NodeId, magnitude u64, negative bool, annotation ast.NodeId) Value ! Error { + result :: Value{ kind = .integer, node = id, magnitude = magnitude, negative = negative, annotation = annotation } + if (annotation != no_node and !try valid_value(state, annotation, result)) return Value{} + return result +} + +@hide:file +same_string proc(state @State, left, right ast.NodeId) bool ! Error { + a :: ast.literal_text(&state.tree, left) + b :: ast.literal_text(&state.tree, right) + ac := usize(1) + bc := usize(1) + while (ac < a.len - 1 and bc < b.len - 1) { + av :: ast.string_next(a, &ac) catch { return false } + bv :: ast.string_next(b, &bc) catch { return false } + if (av != bv) return false + } + return ac == a.len - 1 and bc == b.len - 1 +} + +@hide:file +flexible proc(state @State, id ast.NodeId) bool { + kind :: ast.expr_kind(&state.tree, id) + if (kind == .literal_int or kind == .literal_float) return true + if (kind == .unary) { + view :: ast.unary_expr(&state.tree, id) + return view.operator == .negate and flexible(state, view.operand) + } + if (kind == .binary) { + view :: ast.binary_expr(&state.tree, id) + return ( + (view.operator == .add or view.operator == .subtract or view.operator == .multiply or view.operator == .divide) + and flexible(state, view.left) and flexible(state, view.right) + ) + } + return false +} + +@hide:file +template proc(state @State, id ast.NodeId) bool { + return ast.function_generic(&state.tree, id) or is_type(state, ast.function_decl(&state.tree, id).return_type) +} diff --git a/tests/native.py b/tests/native.py index 3651488..e26a42d 100644 --- a/tests/native.py +++ b/tests/native.py @@ -11,6 +11,432 @@ HONEY = ROOT / "build/honey" # Explicit expectations guard against frontend bugs shared by both execution paths. CASES = { + "specialization_parameterless_type_producer_is_cached": ( + """ + Token proc() type { return struct { value i16 } } + main proc() i16 { + first Token() :: { value = 42 } + second Token() :: first + return second.value + } + """, b"42\n", None, + ), + "specialization_explicit_identity_independent_widths": ( + """ + identity proc($T type, value T) T { return value } + main proc() i16 { + narrow u8 :: identity(u8, 40) + wide i16 :: identity(i16, 2) + return i16(narrow) + wide + } + """, b"42\n", None, + ), + "specialization_inferred_identity_independent_widths": ( + """ + identity proc($T type, value T) T { return value } + main proc() i16 { + narrow u8 :: 40 + wide i16 :: 2 + first u8 :: identity(narrow) + second i16 :: identity(wide) + return i16(first) + second + } + """, b"42\n", None, + ), + "specialization_cached_nominal_type_through_aliases": ( + """ + Box proc($T type) type { return struct { value T } } + Element :: alias i16 + First :: Box(Element) + Second :: alias Box(i16) + read proc(value Box(i16)) i16 { return value.value } + main proc() i16 { + first First :: { value = 40 } + second Second :: first + repeated Box(i16) :: second + return read(repeated) + 2 + } + """, b"42\n", None, + ), + "specialization_different_type_arguments_remain_nominally_distinct": ( + """ + Box proc($T type) type { return struct { value T } } + main proc() i16 { + signed Box(i16) :: { value = 42 } + unsigned Box(u16) :: signed + return i16(unsigned.value) + } + """, b"", b"incompatible value type", + ), + "specialization_generated_record_and_mutable_array_storage": ( + """ + Fixed proc($T type, $N usize) type { + return struct { values [N]mut T } + } + main proc() i16 { + fixed Fixed(i16, 2) := { values = [40, 0] } + fixed.values[1] = 2 + copy Fixed(i16, 2) :: fixed + return copy.values[0] + copy.values[1] + } + """, b"42\n", None, + ), + "specialization_infers_generated_record_type_and_length": ( + """ + Fixed proc($T type, $N usize) type { + return struct { values [N]mut T } + } + first proc($T type, $N usize, value @Fixed(T, N)) T { + return value.values[0] + } + length proc($T type, $N usize, value @Fixed(T, N)) usize { + return N + } + main proc() i16 { + fixed Fixed(i16, 2) :: { values = [40, 1] } + return first(&fixed) + i16(length(&fixed)) + } + """, b"42\n", None, + ), + "specialization_infers_pointer_array_element_and_length": ( + """ + endpoints proc($T type, $N usize, values @[N]T) T { + return values[0] + values[N - 1] + } + main proc() i16 { + values [2]i16 :: [40, 2] + return endpoints(&values) + } + """, b"42\n", None, + ), + "specialization_nested_runtime_calls": ( + """ + identity proc($T type, value T) T { return value } + forward proc($T type, value T) T { return identity(value) } + main proc() i16 { return forward(i16, 42) } + """, b"42\n", None, + ), + "specialization_nested_generated_types": ( + """ + Box proc($T type) type { return struct { value T } } + Wrapped proc($T type) type { return struct { box Box(T) } } + main proc() i16 { + wrapped Wrapped(i16) :: { box = Box(i16){ value = 42 } } + return wrapped.box.value + } + """, b"42\n", None, + ), + "specialization_recursive_runtime_call_reuses_instance": ( + """ + repeat proc($T type, value T, remaining usize) T { + if (remaining == 0) return value + return repeat(value, remaining - 1) + } + main proc() i16 { return repeat(i16, 42, 3) } + """, b"42\n", None, + ), + "specialization_immutable_scalar_arguments_and_expressions": ( + """ + base usize :: 1 + Fixed proc($T type, $N usize) type { + return struct { values [N]mut T } + } + main proc() i16 { + count usize :: base + 1 + fixed Fixed(i16, count) :: { values = [40, 2] } + same Fixed(i16, 2) :: fixed + return same.values[0] + same.values[1] + } + """, b"42\n", None, + ), + "specialization_rejects_mutable_compile_time_argument": ( + """ + value proc($N usize, result i16) i16 { return result } + main proc() i16 { + count usize := 2 + return value(count, 42) + } + """, b"", b"generic argument must be a compile-time value", + ), + "specialization_rejects_runtime_compile_time_argument": ( + """ + value proc($N usize, result i16) i16 { return result } + runtime proc(count usize) i16 { return value(count, 42) } + main proc() i16 { return runtime(2) } + """, b"", b"generic argument must be a compile-time value", + ), + "specialization_boolean_value_parameters": ( + """ + choose proc($enabled bool, value i16) i16 { + if (enabled) return value + return value + 2 + } + main proc() i16 { + enabled :: true + if (choose(enabled, 40) != 40) return 0 + return choose(false, 40) + } + """, b"42\n", None, + ), + "specialization_string_value_parameters": ( + """ + offset proc($text string, value i16) i16 { + return value + i16(text.len) + } + main proc() i16 { return offset("ab", 40) } + """, b"42\n", None, + ), + "specialization_callback_value_parameters": ( + """ + increment proc(value i16) i16 { return value + 2 } + apply proc($T type, $callback proc(value T) T, value T) T { + return callback(value) + } + main proc() i16 { return apply(i16, increment, 40) } + """, b"42\n", None, + ), + "specialization_type_producer_conditional_returns": ( + """ + Choose proc($wide bool) type { + if (wide) return struct { value i64 } + return struct { value i16 } + } + main proc() i16 { + narrow Choose(false) :: { value = 40 } + wide Choose(true) :: { value = 2 } + return narrow.value + i16(wide.value) + } + """, b"42\n", None, + ), + "specialization_type_producer_local_computed_storage_count": ( + """ + Fixed proc($T type, $N usize) type { + count usize :: N + 1 + return struct { values [count]mut T } + } + main proc() i16 { + fixed Fixed(i16, 1) := { values = [40, 0] } + fixed.values[1] = 2 + return fixed.values[0] + fixed.values[1] + } + """, b"42\n", None, + ), + "specialization_rejects_out_of_range_unused_value_argument": ( + """ + ignore proc($N u8) i16 { return 42 } + main proc() i16 { return ignore(256) } + """, b"", b"numeric literal is outside", + ), + "specialization_rejects_inconsistent_inferred_type": ( + """ + first proc($T type, left T, right T) T { return left } + main proc() i16 { + left i16 :: 40 + right u8 :: 2 + return first(left, right) + } + """, b"", b"cannot infer consistent compile-time arguments", + ), + "specialization_recursive_generated_record_pointer": ( + """ + Node proc($T type) type { + return struct { value T, next ?@Node(T) } + } + main proc() i16 { + tail Node(i16) :: { value = 2, next = null } + head Node(i16) :: { value = 40, next = &tail } + if head.next |next| return head.value + next.value + return 0 + } + """, b"42\n", None, + ), + "specialization_phantom_type_arguments_remain_nominally_distinct": ( + """ + Phantom proc($T type) type { return struct { value i16 } } + main proc() i16 { + first Phantom(i16) :: { value = 42 } + second Phantom(u8) :: first + return second.value + } + """, b"", b"incompatible value type", + ), + "specialization_phantom_value_arguments_remain_nominally_distinct": ( + """ + Tagged proc($N usize) type { return struct { value i16 } } + main proc() i16 { + first Tagged(1) :: { value = 42 } + second Tagged(2) :: first + return second.value + } + """, b"", b"incompatible value type", + ), + "specialization_direct_array_type_return": ( + """ + Array proc($T type, $N usize) type { return [N]T } + Row :: alias Array(i16, 2) + main proc() i16 { + row Row :: [40, 2] + same Array(i16, 2) :: row + return same[0] + same[1] + } + """, b"42\n", None, + ), + "specialization_value_parameter_preserves_narrow_arithmetic": ( + """ + add proc($N u8) i64 { return i64(N + 1) } + main proc() i64 { return add(255) } + """, b"", b"integer arithmetic overflow", + ), + "specialization_optional_value_null_capture_is_safe": ( + """ + read proc($value ?i16) i16 { + if value |present| return present + return 42 + } + main proc() i16 { return read(null) } + """, b"42\n", None, + ), + "specialization_optional_value_null_unwrap_traps": ( + """ + read proc($value ?i16) i16 { return value? } + main proc() i16 { return read(null) } + """, b"", b"null", + ), + "specialization_optional_value_present_literal_capture": ( + """ + read proc($value ?i16) i16 { + if value |present| return present + return 0 + } + main proc() i16 { return read(42) } + """, b"42\n", None, + ), + "specialization_optional_callback_unwrap_and_call": ( + """ + increment proc(value i16) i16 { return value + 2 } + invoke proc($callback ?proc(value i16) i16, value i16) i16 { + return callback?(value) + } + main proc() i16 { return invoke(increment, 40) } + """, b"42\n", None, + ), + "specialization_optional_callback_capture_and_null_comparison": ( + """ + increment proc(value i16) i16 { return value + 2 } + invoke proc($callback ?proc(value i16) i16, value i16) i16 { + if callback |present| return present(value) + if (callback == null) return value + 2 + return 0 + } + main proc() i16 { + if (invoke(increment, 40) != 42) return 0 + return invoke(null, 40) + } + """, b"42\n", None, + ), + "specialization_string_argument_preserves_slice_annotation": ( + """ + length proc($text []u8) usize { return text.len } + main proc() i16 { return 40 + i16(length("ab")) } + """, b"42\n", None, + ), + "specialization_cached_phantom_type_canonical_builtin_alias": ( + """ + Phantom proc($T type) type { return struct { value i16 } } + Element :: alias i64 + main proc() i16 { + first Phantom(int) :: { value = 42 } + second Phantom(i64) :: first + third Phantom(Element) :: second + return third.value + } + """, b"42\n", None, + ), + "specialization_infers_type_through_type_producer_wrapper": ( + """ + Box proc($T type) type { return struct { value T } } + Wrapped proc($T type) type { return Box(T) } + read proc($T type, value @Wrapped(T)) T { return value.value } + main proc() i16 { + value Wrapped(i16) :: { value = 42 } + return read(&value) + } + """, b"42\n", None, + ), + "specialization_string_cache_uses_decoded_value": ( + """ + Tag proc($text string) type { return struct { value i16 } } + main proc() i16 { + first Tag("a") :: { value = 42 } + second Tag("\\x61") :: first + return second.value + } + """, b"42\n", None, + ), + "specialization_type_producer_rejects_runtime_parameters": ( + """ + Box proc($T type, ignored i16) type { return struct { value T } } + main proc() i16 { + value Box(i16, true) :: { value = 42 } + return value.value + } + """, b"", b"generic argument must be a compile-time value", + ), + "specialization_rejects_invalid_compile_time_comparison": ( + """ + ignore proc($flag bool) i16 { return 42 } + main proc() i16 { return ignore(1 == true) } + """, b"", b"generic argument must be a compile-time value", + ), + "specialization_contextual_literal_after_typed_argument": ( + """ + first proc($T type, left T, right T) T { return left } + main proc() i16 { + value i16 :: 42 + result i16 :: first(value, 0) + return result + } + """, b"42\n", None, + ), + "specialization_contextual_literal_before_typed_argument": ( + """ + first proc($T type, left T, right T) T { return left } + main proc() i16 { + value i16 :: 42 + result i16 :: first(0, value) + return result + 42 + } + """, b"42\n", None, + ), + "specialization_contextual_nested_integer_literal_expressions": ( + """ + first proc($T type, left T, right T) T { return left } + main proc() i16 { + value i16 :: 40 + left i16 :: first(-(1 + 1), value) + right i16 :: first(value, (3 * 4) + 2) + return left + right + 4 + } + """, b"42\n", None, + ), + "specialization_contextual_literal_right_out_of_range": ( + """ + first proc($T type, left T, right T) T { return left } + main proc() u8 { + value u8 :: 42 + return first(value, 256) + } + """, b"", b"numeric literal is outside", + ), + "specialization_contextual_literal_left_out_of_range": ( + """ + first proc($T type, left T, right T) T { return left } + main proc() u8 { + value u8 :: 42 + return first(256, value) + } + """, b"", b"numeric literal is outside", + ), "fallible_success_and_catch_short_circuit": ( """ Error :: enum { failed } @@ -2128,6 +2554,49 @@ def check_data_model_packages(temporary, executable): return 3 +def check_specialization_packages(temporary, executable): + project = temporary / "specialization" + app, library = project / "app", project / "library" + app.mkdir(parents=True) + library.mkdir() + (library / "types.hon").write_text(textwrap.dedent(""" + Box proc($T type) type { return struct { value T } } + Fixed proc($T type, $N usize) type { + return struct { values [N]mut T } + } + """).strip() + "\n") + (library / "functions.hon").write_text(textwrap.dedent(""" + identity proc($T type, value T) T { return value } + first proc($T type, $N usize, value @Fixed(T, N)) T { + return value.values[0] + } + """).strip() + "\n") + source = app / "main.hon" + source.write_text(textwrap.dedent(""" + lib :: import "../library" + Element :: alias i16 + ImportedBox :: alias lib.Box(Element) + main proc() i16 { + imported ImportedBox :: { value = 40 } + copy lib.Box(i16) :: imported + fixed lib.Fixed(i16, 2) :: { values = [2, 0] } + return copy.value + lib.identity(lib.first(&fixed)) + } + """).strip() + "\n") + check(app, executable, (b"42\n", None)) + source.write_text(textwrap.dedent(""" + lib :: import "../library" + Box proc($T type) type { return struct { value T } } + main proc() i16 { + imported lib.Box(i16) :: { value = 42 } + local Box(i16) :: imported + return local.value + } + """).strip() + "\n") + check(app, executable, (b"", b"incompatible value type")) + return 2 + + def check_dependency_provenance(temporary, executable): project = temporary / "provenance" app = project / "app" @@ -2368,6 +2837,7 @@ def main(): count += check_namespace_packages(temporary, executable) count += check_record_packages(temporary, executable) count += check_data_model_packages(temporary, executable) + count += check_specialization_packages(temporary, executable) count += check_fallible_packages_and_entry(temporary, executable) count += check_dependency_provenance(temporary, executable) source = ROOT / "programs/factorial.hon"