diff --git a/README.md b/README.md index 412ae84..cc9a698 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,19 @@ indices require `usize`; integer literals and flexible inferred values receive t data pointers use `@T` for one value and `*T` for many values, with `mut` after the pointer marker for writable pointees. `&value` borrows addressable initialized storage; postfix `^` reads or writes the pointee. pointer mutability is independent of binding mutability: an immutable pointer binding or parameter can mutate its `mut` pointee. mutable pointers weaken to readonly pointers with the same child type. address-taking also accepts an expected many-pointer type, such as `pointer *mut i16 :: &values[0]`; arbitrary temporaries are not addressable. -pointer-to-record field selection dereferences implicitly. pointers to arrays support bounded indexing, `.len`, and `.ptr`; `.ptr` returns a many-pointer and retains both pointer and array-element mutability restrictions. many-pointer indices require `usize` and caller-provided valid storage. many-pointer `pointer + offset` requires a `usize` offset, advances by the pointee's storage size and preserves its type/mutability; single-pointer addition, reversed addition and pointer subtraction are invalid, matching Bro. operands evaluate left to right once; one-past pointers can be formed but not dereferenced. pointers preserve alias identity through calls, returns, globals and aggregate copies; dereferenced aggregates still copy by value. `==` and `!=` compare locations. recursive records may contain pointers to themselves without recursive by-value layout. CLI results render pointer leaves as ``. pointee lifetimes remain caller-managed; there is no borrow checker. slices, optionals, layout/cast builtins and allocator libc calls remain unimplemented. +pointer-to-record field selection dereferences implicitly. pointers to arrays support bounded indexing, `.len`, and `.ptr`; `.ptr` returns a many-pointer and retains both pointer and array-element mutability restrictions. many-pointer indices require `usize` and caller-provided valid storage. many-pointer `pointer + offset` requires a `usize` offset, advances by the pointee's storage size and preserves its type/mutability; single-pointer addition, reversed addition and pointer subtraction are invalid, matching Bro. operands evaluate left to right once; one-past pointers can be formed but not dereferenced. pointers preserve alias identity through calls, returns, globals and aggregate copies; dereferenced aggregates still copy by value. `==` and `!=` compare locations. recursive records may contain pointers to themselves without recursive by-value layout. CLI results render pointer leaves as ``. pointee lifetimes remain caller-managed; there is no borrow checker. + +slices use `[]T` or `[]mut T` and borrow storage rather than copying elements. `values[start..end]` checks ordered, upper-exclusive `usize` bounds; either bound may be omitted for arrays, pointers to arrays and slices. many-pointer slices require an explicit end. empty and one-past slices are valid, but indexing at `.len` traps. `.ptr` returns a many-pointer and `.len` returns `usize`; copies, calls and aggregate storage preserve aliases. mutable slices can write through immutable bindings and parameters, while readonly views cannot strengthen implicitly. contextual `&[]` creates an empty mutable slice; borrowed backing storage must outlive every view. + +sentinel slices use `[;sentinel]T` and checked construction such as `pointer[..length;0]`. the sentinel is a representable compile-time integer stored immediately after the logical elements, outside `.len`; construction checks both accessible bounds and its value. sentinel views can weaken to plain slices, not the reverse. string values expose readonly byte indexing, slicing, `.ptr` and `.len`, and convert to `[]u8` or `[;0]u8`; their module-owned backing includes an implicit NUL without changing the logical length. + +optionals use `?T`; values of `T` and `null` initialize optional storage, parameters, results and aggregate fields. `== null` and `!= null` test presence. postfix `value?` unwraps or traps, while `if value |payload|` and `while value |payload|` bind a payload only inside the taken body; loop guards are re-evaluated and `continue` returns to the guard. optional data/function pointers use a nullable pointer representation; other optionals retain a separate presence flag and by-value payload. optional pointer weakening preserves aliases and readonly restrictions. + +`sizeof!(T)` and `alignof!(T)` report the supported target's native layout, including aggregate padding, slice descriptors and nullable pointers. `ptrcast!(value)` takes its full result pointer type from context; `ptrcast!(T, value)` states it explicitly. casts preserve the storage location and optional presence, permit byte reinterpretation, and cannot remove readonly qualification. `constcast!` changes pointer or slice mutability without changing the pointee type; it does not make genuinely readonly storage writable or extend its lifetime. `anyopaque` is a pointer-only pointee, not a storable or dereferenceable value. + +`memcopy!(destination, source)` copies matching typed slices or pointers to arrays, checking equal lengths and rejecting nonempty overlap. `memset!(destination, value)` repeats complete typed values, including aggregate fields, rather than repeating a byte. destinations require writable elements; empty and zero-sized copies/sets do not access storage. interpreter byte views synchronize with the original typed storage so casts and memory operations preserve aliases. + +native external declarations use `malloc c_func(size c_ulong) ?*mut anyopaque`, without a body. supported signatures contain concrete integer/float/boolean scalars, data pointers, optional data pointers and `void` results; aggregate C ABI and variadics remain outside this gate. Darwin `c_char`/`c_uchar`, `c_short`/`c_ushort`, `c_int`/`c_uint`, `c_long`/`c_ulong` and `c_longlong`/`c_ulonglong` map to the corresponding 8/16/32/64-bit integer representations. executable checks cover `malloc`, `realloc`, `free` and `posix_memalign` through allocator-style callbacks. foreign calls trap only when reached in the interpreter; arbitrary interpreter FFI is not supported. compiling the existing generic/fallible standard library remains a later gate. records use `Point :: struct { x i16, y i16 = 2 }`, with comma- or newline-separated fields. named record declarations have nominal identity; two separately declared records are not interchangeable. construct them with `Point { x = 40 }` or a contextual keyed literal, such as `point Point := { x = 40 }`. omitted fields use their declared defaults; missing required fields, duplicate/unknown fields, incompatible values and recursive by-value layouts are diagnosed. explicit initializers run in source order, followed by omitted defaults in declaration order. an explicit value overrides its field's default without evaluating that default. @@ -131,7 +143,7 @@ resolver scopes use `@std/hashmap.AutoHashMap(K, V)`, which shares `init`, `put` `sema.resolve` returns an owned binding table indexed by AST `NodeId`, without modifying the AST. `sema.binding` identifies a declaration, package namespace, builtin type, or invalid binding carrying its diagnostic ID; nodes with no binding return `.none`. `sema.resolve_packages` additionally borrows package roots and import links, predeclares every package before resolving qualified uses, and keeps import aliases in their originating source file. selected declarations have the same binding identity as unqualified declarations. release the result with `sema.deinit`. unknown names and duplicate declarations add source-spanned diagnostics; invalid syntax and failed imports retain their existing diagnostic identity. same-scope duplicates retain the first declaration for recovery, while forbidden inner declarations invalidate that name's uses rather than silently binding them to an outer declaration. -type checking (`@source/sema/check`) runs after name resolution. `check.check` borrows the AST and binding table and returns owned per-node types, pointer/array/record/enum descriptors and construction mappings; query with `check.type_of`, `check.pointer_type`, `check.array_type`, `check.record_type`, `check.enum_type`, `check.field_index` and `check.initializer_field`, and release with `check.deinit`. declarations infer their initializer types, including forward references and function aliases; function types identify an AST declaration or explicit signature node supplying the callable signature. annotations support builtin types, data pointers, callback signatures, fixed arrays and local/imported records, enums, unions and distinct/alias types; type names are not ordinary runtime values. `void` is valid as a function result or tagged-union payload, not as a parameter or standalone stored value. +type checking (`@source/sema/check`) runs after name resolution. `check.check` borrows the AST and binding table and returns owned per-node types, pointer/container/record/enum descriptors and construction mappings; query with `check.type_of`, `check.pointer_type`, `check.array_type`, `check.record_type`, `check.enum_type`, `check.field_index` and `check.initializer_field`, and release with `check.deinit`. `ArrayType.kind` distinguishes fixed arrays, slices and optionals; its element records the slice element or optional payload, with mutability and sentinel metadata for slices. declarations infer their initializer types, including forward references and function aliases; function types identify an AST declaration or explicit signature node supplying the callable signature. annotations support builtin types, data pointers, callback signatures, fixed arrays, slices, optionals and local/imported records, enums, unions and distinct/alias types; type names are not ordinary runtime values. `void` is valid as a function result or tagged-union payload, not as a parameter or standalone stored value. function fallthrough obligations are separate from body types. `check.fallthrough_diagnostic(&result, function)` accepts a function declaration's `NodeId` and returns its original missing-return diagnostic ID, or `null` when no obligation exists. nested body errors retain their original identities. lowering consumes this fact without interpreting diagnostic codes and places its trap only on a surviving fallthrough path; `check.deinit` releases the obligation storage. @@ -141,7 +153,7 @@ numeric types include `i8`, `i16`, `i32`, `i64`, `u8`, `u16`, `u32`, `u64`, `isi the conversion policy is value-preserving by default: any future implicit widening must preserve every value of the source type, not merely the value observed at one use. narrowing, potentially inexact integer-to-float conversion, float-to-integer conversion, and signedness changes that cannot preserve the full source range require an explicit operation. integer arithmetic remains checked rather than wrapping; floating arithmetic retains IEEE rounding. -explicit numeric conversion uses `T(value)`, such as `u8(index)`, `usize(byte)` or `f64(count)`, with exactly one numeric argument. integer conversions trap with `integer arithmetic overflow` if the value is outside the destination range; float-to-integer conversion truncates toward zero, then checks bounds, rejecting NaN and infinity. integer-to-float conversion permits rounding, including `f64(9007199254740993)`. operands retain their independent types rather than being narrowed by the result context; a positive constant integer conversion operand above `i64`'s maximum defaults to `u64`. boolean conversions, arbitrary-width integers, `f32`, C scalar types and implicit typed-value widening are not implemented. native storage uses LLVM `i8`/`i16`/`i32`/`i64`, including array elements and function parameters/results. +explicit numeric conversion uses `T(value)`, such as `u8(index)`, `usize(byte)` or `f64(count)`, with exactly one numeric argument. integer conversions trap with `integer arithmetic overflow` if the value is outside the destination range; float-to-integer conversion truncates toward zero, then checks bounds, rejecting NaN and infinity. integer-to-float conversion permits rounding, including `f64(9007199254740993)`. operands retain their independent types rather than being narrowed by the result context; a positive constant integer conversion operand above `i64`'s maximum defaults to `u64`. boolean conversions, arbitrary-width integers, `f32` and implicit typed-value widening are not implemented. native storage uses LLVM `i8`/`i16`/`i32`/`i64`, including array elements and function parameters/results. `@std/strconv.parse_float(allocator, text)` parses a complete borrowed `[]u8` decimal into `f64`. it accepts an optional sign, decimal point, and exponent; whitespace, embedded NUL bytes, trailing input, hexadecimal, infinities, and NaNs return `invalid_number`. finite subnormals and signed zero are preserved; overflow and nonzero values rounded to zero return `out_of_range`. allocation failures propagate as `out_of_memory`, and temporary storage is always released. the Darwin backend uses libc's C-locale conversion without changing the process locale; rounding follows the caller's floating-point rounding mode. numeric lowering uses this interface, keeping exact integer-to-float validation compiler-specific. diff --git a/TODO.md b/TODO.md index 2d52eaa..92a9f8c 100644 --- a/TODO.md +++ b/TODO.md @@ -10,9 +10,12 @@ integers, checked numeric conversions, records/tuples, construction/defaults, enums, distinct/alias types, overlapping untagged/tagged unions, field reads/writes and exhaustive statement/value matching work through both execution paths. source data pointers/references support aliasing, mutation, calls and aggregate -storage; explicit callback signatures and many-pointer addition work through both -execution paths. the driver retains frontend storage -and source provenance through `@source/compilation`, with compilation-wide AST node identities. +storage; explicit callback signatures, many-pointer addition, mutable/sentinel +slices, optionals and layout/cast/memory builtins work through both execution paths. +native scalar/pointer `c_func` declarations support allocator-required libc calls; +executed foreign calls remain diagnostic traps in the interpreter. +the driver retains frontend storage and source provenance through +`@source/compilation`, with compilation-wide AST node identities. file commands retain single-file behavior; directory commands compile the package's production files and load its dependencies in separate scopes. @@ -41,10 +44,10 @@ test discovery and FFI remain outside these completed slices. - [x] native integer widths (`i8`–`i64`, `u8`–`u64`, `isize`/`usize`) and checked explicit numeric conversions, including float truncation/range checks and integer-to-float rounding. preserve exact types through arrays, calls and range captures; retain strict implicit conversion rules. - [x] records/tuples, typed/contextual/anonymous construction, field defaults and reads/writes, nested aggregate value copies, imported record types and constructors through interpreter/native execution. - [x] nominal enums and distinct types, transparent declaration/type aliases, overlapping untagged/tagged unions, contextual payload construction, active-field checks and exhaustive scalar/enum/tagged-union matching through interpreter/native execution. pointer captures, inline specialization, sum composition and yielding value blocks remain with their respective later gates. -- [ ] memory representation: source pointers/references, mutable and sentinel slices, optionals, layout/cast builtins and allocator-required libc calls. internal IR array addresses are not source pointers. examples: `std/mem/mem.hon`, `std/arraylist/arraylist.hon`. +- [x] memory representation: source pointers/references, mutable and sentinel slices, optionals, layout/cast builtins and allocator-required libc calls. internal IR array addresses are not source pointers. examples: `std/mem/mem.hon`, `std/arraylist/arraylist.hon`. - [x] source data pointers/references: `@T`/`*T`, `mut`, address-taking and dereference, pointer-aware fields/indexing, array `.len`/`.ptr`, readonly weakening, location equality, recursive pointer-linked records and shared pointees across interpreter/native calls and aggregate copies. - [x] function-pointer signatures: explicit `@proc(...) Result`, structural signature checking, callbacks in parameters/results/records/arrays, vtable calls and imported signature aliases; many-pointer `+ usize` arithmetic preserves pointee type, mutability, stride and location identity through interpreter/native execution. - - [ ] mutable/sentinel slices, optionals, layout/cast builtins and allocator-required libc calls. + - [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/fallible standard-library compilation and native startup/IO remain with later gates. - [ ] fallible results and cleanup: error-set composition, `try`/`catch`, `defer`/`errdefer`; infrastructure errors remain distinct from diagnostic traps. examples: `source/main.hon:41`, `source/sema/check/check.hon:117`. - [ ] generic specialization and compile-time execution/reflection sufficient for existing `ArrayList`, hash maps, `enummap`, `meta` and `io.print`; include type/value parameters and compile-time generated storage. - [ ] remaining source constructs: collection/index iteration, expression-valued control flow and `yield`, compound updates and multiline strings. examples: `source/parser/parse.hon`, `source/llvm/runtime.hon`. diff --git a/source/ast/ast.hon b/source/ast/ast.hon index b0828e7..312fd26 100644 --- a/source/ast/ast.hon +++ b/source/ast/ast.hon @@ -1,17 +1,17 @@ #! grammar: -#! * type ..... -> identifier {'.' member} | ('@' | '*') ['mut'] type | '@proc' '(' parameters ')' type | '[' (expr | '_') ']' ['mut'] type | 'struct' '{' fields '}' | 'enum' ['(' type ')'] '{' members '}' | 'union' ['(' (type | 'enum') ')'] '{' fields '}' | ('distinct' | 'alias') type +#! * type ..... -> identifier {'.' member} | '?' type | ('@' | '*') ['mut'] type | '@proc' '(' parameters ')' type | '[' (expr | '_') ']' ['mut'] type | '[' [';' expr] ']' ['mut'] type | 'struct' '{' fields '}' | 'enum' ['(' type ')'] '{' members '}' | 'union' ['(' (type | 'enum') ')'] '{' fields '}' | ('distinct' | 'alias') type #! * binding ... -> :: | := -#! * literal .. -> int | float | string | 'true' | 'false' +#! * literal .. -> int | float | string | 'true' | 'false' | 'null' #! * op ....... -> '+' | '-' | '*' | '/' | '==' | '!=' | '<' | '<=' | '>' | '>=' | 'and' | 'or' -#! * expr ..... -> identifier | literal | type | '.' member | '[' [expr {',' expr} [',']] ']' | [type] '{' [initializer {',' initializer} [',']] '}' | '(' expr ')' | ('-' | '!' | '&') expr | expr '^' | expr op expr | expr '(' [expr {',' expr} [',']] ')' | expr '[' expr ']' | expr '.' (member | int) | expr ('..' | '..=') expr | 'match' expr '{' match_arms '}' -#! * decl ..... -> identifier [type] binding expr | ['test'] [identifier '::'] 'import' string +#! * expr ..... -> identifier | literal | type | '.' member | '[' [expr {',' expr} [',']] ']' | [type] '{' [initializer {',' initializer} [',']] '}' | '(' expr ')' | ('-' | '!' | '&') 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 '}' +#! * decl ..... -> identifier [type] binding expr | identifier 'c_func' '(' parameters ')' type | ['test'] [identifier '::'] 'import' string #! * block .... -> '{' {stmt} '}' #! * arm ...... -> block | stmt (single statements become canonical blocks) #! * match_arm -> (expr {',' expr} ['|' identifier '|'] | 'else') ':' (block | stmt | expr); arms are comma/newline separated. #! enum members and tagged-union variants accept keyword names; member names do not enter lexical scopes. -#! * stmt ..... -> decl | expr | expr '=' expr | 'return' [expr] | 'break' | 'continue' | 'if' expr arm ['else' arm] | 'while' expr arm | 'for' expr '|' ['@'] identifier [',' identifier] '|' arm -#! braceless guards/iterables require parentheses unless they are calls; else binds to the nearest if. -#! precedence (tightest first): calls/indices/selections/dereference, unary -/!/& higher than multiplication/division, addition/subtraction, relational comparisons, equality, and, or, ranges. compound range endpoints require their own parentheses. +#! * stmt ..... -> decl | expr | expr '=' expr | 'return' [expr] | 'break' | 'continue' | 'if' expr ['|' identifier '|'] arm ['else' arm] | 'while' expr ['|' identifier '|'] arm | 'for' expr '|' ['@'] identifier [',' identifier] '|' arm +#! braceless guards/iterables require parentheses unless they are calls or optional captures; else binds to the nearest if. +#! precedence (tightest first): calls/indices/selections/dereference/unwrap, unary -/!/& higher than multiplication/division, addition/subtraction, relational comparisons, equality, and, or, ranges. compound range endpoints require their own parentheses. #! binary operators associate left; unary operators associate right. import "@std/debug" @@ -30,7 +30,7 @@ NodeId :: distinct u32 no_node :: maxval!(NodeId) LiteralKind :: enum { - int, float, string, bool + int, float, string, bool, null } ExprKind :: enum { @@ -58,6 +58,11 @@ ExprKind :: enum { index selection invalid + slice_type + optional_type + slice + builtin_call + literal_null } DeclKind :: enum { @@ -89,6 +94,7 @@ UnaryOperator :: enum { logical_not address_of dereference + unwrap } BinaryOperator :: enum { @@ -144,6 +150,11 @@ NodeKind :: enum { invalid_expr invalid_stmt invalid_decl + expr_slice_type + expr_optional_type + expr_slice + expr_builtin_call + literal_null } @hide:file @@ -246,6 +257,21 @@ PointerTypeView :: struct { many bool } +SliceTypeView :: struct { + element NodeId + mutable bool + sentinel ?NodeId +} + +SliceView :: struct { + base NodeId + start ?NodeId + end ?NodeId + sentinel ?NodeId +} + +BuiltinCallView :: struct { name NodeId } + RecordLiteralView :: struct { span Span type ?NodeId @@ -290,6 +316,7 @@ IfView :: struct { condition NodeId then_block NodeId else_block ?NodeId + capture ?NodeId = null } AssignmentView :: struct { @@ -302,6 +329,7 @@ WhileView :: struct { span Span condition NodeId body NodeId + capture ?NodeId = null } RangeView :: struct { @@ -413,6 +441,7 @@ add_literal proc(builder @mut Builder, kind LiteralKind, span Span) NodeId ! mem .float: .literal_float .string: .literal_string .bool: .literal_bool + .null: .literal_null } return try add_node(builder, Node{ kind = node_kind }, span) } @@ -484,6 +513,39 @@ add_call proc( }, span) } +add_builtin_call proc(builder @mut Builder, span Span, name NodeId, arguments []NodeId) NodeId ! mem.AllocError { + id :: try add_call(builder, span, name, arguments) + builder.storage.nodes.items[usize(id)].kind = .expr_builtin_call + return id +} + +add_optional_type proc(builder @mut Builder, span Span, element NodeId) NodeId ! mem.AllocError { + return try add_node(builder, Node{ kind = .expr_optional_type, main = NodeData{ node_id = element } }, span) +} + +add_slice_type proc(builder @mut Builder, span Span, element NodeId, mutable bool, sentinel ?NodeId) NodeId ! mem.AllocError { + flag u32 :: if (mutable) 1 else 0 + raw_sentinel :: if (sentinel) |id| id else no_node + return try add_node(builder, Node{ + kind = .expr_slice_type, + main = NodeData{ extra = flag }, + data0 = NodeData{ node_id = raw_sentinel }, + data1 = NodeData{ node_id = element }, + }, span) +} + +add_slice proc(builder @mut Builder, span Span, base NodeId, start, end, sentinel ?NodeId) NodeId ! mem.AllocError { + raw_start :: if (start) |id| u32(id) else u32(no_node) + raw_end :: if (end) |id| u32(id) else u32(no_node) + raw_sentinel :: if (sentinel) |id| u32(id) else u32(no_node) + extra :: try add_extra(builder, raw_start) + _ = try add_extra(builder, raw_end) + _ = try add_extra(builder, raw_sentinel) + return try add_node(builder, Node{ + kind = .expr_slice, main = NodeData{ node_id = base }, data0 = NodeData{ extra = extra }, + }, span) +} + add_array_type proc( builder @mut Builder, span Span, count ?NodeId, element NodeId, mutable bool, ) NodeId ! mem.AllocError { @@ -697,9 +759,11 @@ add_if_stmt proc( else_block ?NodeId, ) NodeId ! mem.AllocError { alternative :: if (else_block) |id| id else no_node + extra :: try add_extra(builder, u32(condition)) + _ = try add_extra(builder, u32(no_node)) return try add_node(builder, Node{ kind = .stmt_if, - main = NodeData{ node_id = condition }, + main = NodeData{ extra = extra }, data0 = NodeData{ node_id = then_block }, data1 = NodeData{ node_id = alternative }, }, span) @@ -731,6 +795,15 @@ add_while_stmt proc( }, span) } +set_capture proc(builder @mut Builder, id NodeId, capture NodeId) void { + node :: &builder.storage.nodes.items[usize(id)] + if (node.kind == .stmt_if) builder.storage.extra.items[usize(node.main.extra) + 1] = u32(capture) + else { + debug.assert(node.kind == .stmt_while) + node.data1.node_id = capture + } +} + add_for_stmt proc( builder @mut Builder, span Span, iterable NodeId, capture, index ?NodeId, pointer_capture, index_capture bool, body NodeId, @@ -804,6 +877,7 @@ add_function_decl proc( _ = try add_extra(builder, u32(return_type)) _ = try add_extra(builder, u32(interned_name)) for (params) |parameter| _ = try add_extra(builder, u32(parameter)) + _ = try add_extra(builder, 0) return try add_node(builder, Node{ kind = .func_decl, main = NodeData{ extra = u32(start) }, @@ -812,6 +886,18 @@ add_function_decl proc( }, span) } +set_function_external proc(builder @mut Builder, id NodeId) void { + node :: builder.storage.nodes.items[usize(id)] + debug.assert(node.kind == .func_decl) + builder.storage.extra.items[usize(node.data1.extra) - 1] = 1 +} + +function_external proc(tree @Tree, id NodeId) bool { + node :: get_node(&tree.storage, id) + if (node.kind != .func_decl) return false + return tree.storage.extra.items[usize(node.data1.extra) - 1] != 0 +} + add_import_decl proc( builder @mut Builder, span, path Span, @@ -980,9 +1066,10 @@ if_stmt proc(tree @Tree, id NodeId) IfView { debug.assert(node.kind == .stmt_if) return IfView{ span = get_span(&tree.storage, id), - condition = node.main.node_id, + condition = NodeId(tree.storage.extra.items[usize(node.main.extra)]), then_block = node.data0.node_id, else_block = maybe_node_id(u32(node.data1.node_id)), + capture = maybe_node_id(tree.storage.extra.items[usize(node.main.extra) + 1]), } } @@ -1003,6 +1090,7 @@ while_stmt proc(tree @Tree, id NodeId) WhileView { span = get_span(&tree.storage, id), condition = node.main.node_id, body = node.data0.node_id, + capture = maybe_node_id(u32(node.data1.node_id)), } } @@ -1071,14 +1159,15 @@ function_param_count proc(tree @Tree, id NodeId) usize { node :: get_node(&tree.storage, id) debug.assert(node.kind == .func_decl or node.kind == .expr_function_type) start :: if (node.kind == .func_decl) node.main.extra + 4 else node.data0.extra - return usize(node.data1.extra - start) + suffix :: if (node.kind == .func_decl) u32(1) else u32(0) + return usize(node.data1.extra - start - suffix) } function_param proc(tree @Tree, id NodeId, index usize) NodeId { node :: get_node(&tree.storage, id) debug.assert(node.kind == .func_decl or node.kind == .expr_function_type) start :: if (node.kind == .func_decl) node.main.extra + 4 else node.data0.extra - debug.assert(index < usize(node.data1.extra - start)) + debug.assert(index < function_param_count(tree, id)) return NodeId(tree.storage.extra.items[usize(start) + index]) } @@ -1140,6 +1229,11 @@ expression_kind proc(kind NodeKind) ExprKind { .expr_index: .index .expr_selection: .selection .invalid_expr: .invalid + .expr_slice_type: .slice_type + .expr_optional_type: .optional_type + .expr_slice: .slice + .expr_builtin_call: .builtin_call + .literal_null: .literal_null else: unreachable } } @@ -1187,13 +1281,13 @@ call_expr proc(tree @Tree, id NodeId) CallExprView { call_arg_count proc(tree @Tree, id NodeId) usize { node :: get_node(&tree.storage, id) - debug.assert(node.kind == .expr_call) + debug.assert(node.kind == .expr_call or node.kind == .expr_builtin_call) return usize(node.data1.extra - node.data0.extra) } call_arg proc(tree @Tree, id NodeId, index usize) NodeId { node :: get_node(&tree.storage, id) - debug.assert(node.kind == .expr_call) + debug.assert(node.kind == .expr_call or node.kind == .expr_builtin_call) debug.assert(index < usize(node.data1.extra - node.data0.extra)) return NodeId(tree.storage.extra.items[usize(node.data0.extra) + index]) } @@ -1219,6 +1313,33 @@ pointer_type proc(tree @Tree, id NodeId) PointerTypeView { } } +slice_type proc(tree @Tree, id NodeId) SliceTypeView { + node :: get_node(&tree.storage, id) + debug.assert(node.kind == .expr_slice_type) + return SliceTypeView{ + element = node.data1.node_id, mutable = node.main.extra != 0, + sentinel = maybe_node_id(u32(node.data0.node_id)), + } +} + +slice_expr proc(tree @Tree, id NodeId) SliceView { + node :: get_node(&tree.storage, id) + debug.assert(node.kind == .expr_slice) + extra :: usize(node.data0.extra) + return SliceView{ + base = node.main.node_id, + start = maybe_node_id(tree.storage.extra.items[extra]), + end = maybe_node_id(tree.storage.extra.items[extra + 1]), + sentinel = maybe_node_id(tree.storage.extra.items[extra + 2]), + } +} + +builtin_call proc(tree @Tree, id NodeId) BuiltinCallView { + node :: get_node(&tree.storage, id) + debug.assert(node.kind == .expr_builtin_call) + return BuiltinCallView{ name = node.main.node_id } +} + array_element_count proc(tree @Tree, id NodeId) usize { node :: get_node(&tree.storage, id) debug.assert(node.kind == .expr_array_literal) @@ -1282,7 +1403,7 @@ union_type proc(tree @Tree, id NodeId) UnionTypeView { type_operand proc(tree @Tree, id NodeId) NodeId { node :: get_node(&tree.storage, id) - debug.assert(node.kind == .expr_distinct_type or node.kind == .expr_alias) + debug.assert(node.kind == .expr_distinct_type or node.kind == .expr_alias or node.kind == .expr_optional_type) return node.main.node_id } diff --git a/source/diagnostics/diagnostics.hon b/source/diagnostics/diagnostics.hon index b3dde9f..11a6b0d 100644 --- a/source/diagnostics/diagnostics.hon +++ b/source/diagnostics/diagnostics.hon @@ -94,6 +94,11 @@ Code :: enum { invalid_dereference invalid_pointer_assignment invalid_pointer_access + null_unwrap + invalid_slice_bounds + sentinel_mismatch + overlapping_memory_copy + unsupported_foreign_call } Diagnostic :: struct { @@ -209,5 +214,10 @@ messages :: $enummap.init(Code, []u8, { invalid_dereference = "dereference requires a pointer", invalid_pointer_assignment = "pointer assignment requires a mutable pointee", invalid_pointer_access = "pointer does not refer to live accessible storage", + null_unwrap = "cannot unwrap a null optional", + invalid_slice_bounds = "slice bounds are invalid", + sentinel_mismatch = "slice sentinel does not match the expected value", + overlapping_memory_copy = "memory copy source and destination overlap", + unsupported_foreign_call = "foreign function calls are unsupported by the interpreter", }) diff --git a/source/interpreter/interpreter.hon b/source/interpreter/interpreter.hon index 7cf8f7c..2dbce84 100644 --- a/source/interpreter/interpreter.hon +++ b/source/interpreter/interpreter.hon @@ -42,6 +42,9 @@ Address :: struct { scalar ?@mut ir.Value = null initialized ?@mut bool = null raw bool = false + byte_view bool = false + readonly bool = false + bytes []mut ir.Value = &[] # source handles retain root identity and native offsets, never frame-local address IDs. root ?@mut ir.Value = null root_type ir.Type = .invalid @@ -55,6 +58,14 @@ Pointer :: struct { root_type ir.Type root_array ir.ArrayId byte_offset usize + bytes []mut ir.Value = &[] + readonly bool = false +} + +@hide:file +StringView :: struct { + root ir.Value = ir.Value{ type = .u8 } + cells []mut ir.Value } @hide:file @@ -103,6 +114,32 @@ run proc( globals[i].value = aggregate_view(global.type, global.array_type, aggregate_globals[global_offset..global_offset + cells]) global_offset += cells } + # byte reinterpretation reuses run-wide scratch; loads are copied before the next instruction. + scratch_size usize := 16 + for (module.arrays.items) |shape| { + if (shape.byte_size > scratch_size) scratch_size = shape.byte_size + } + scratch :: try mem.alloc(ir.Value, allocator, scratch_size) + defer mem.free(allocator, scratch) + source_scratch :: try mem.alloc(ir.Value, allocator, scratch_size) + defer mem.free(allocator, source_scratch) + string_size usize := 0 + for (module.strings.items) |string| { + if (string.len == maxval!(usize) or string.len + 1 > maxval!(usize) - string_size) return .storage_limit_reached + string_size += string.len + 1 + } + string_cells :: try mem.alloc(ir.Value, allocator, string_size) + defer mem.free(allocator, string_cells) + strings :: try mem.alloc(StringView, allocator, module.strings.items.len) + defer mem.free(allocator, strings) + string_offset usize := 0 + for (module.strings.items) |string, index| { + cells :: string_cells[string_offset..string_offset + string.len + 1] + for (string.allocation[..string.len]) |byte, i| cells[i] = ir.Value{ type = .u8, data = ir.ValueData{ unsigned = u64(byte) } } + cells[string.len] = ir.Value{ type = .u8, data = ir.ValueData{ unsigned = 0 } } + strings[index] = StringView{ cells = cells } + string_offset += cells.len + } pointers := arraylist.init(Pointer, allocator) defer arraylist.deinit(&pointers) frames := arraylist.init(Frame, allocator) @@ -110,6 +147,9 @@ run proc( for (frames.items) |frame| free_frame(allocator, frame) arraylist.deinit(&frames) } + if (module.functions.items[usize(entry)].external_name.len != 0) { + return try runtime_trap(diagnostic_store, .unsupported_foreign_call, module.functions.items[usize(entry)].span) + } try push_frame(allocator, module, &frames, entry, ir.no_value, null) while frames.items.len != 0 { @@ -210,7 +250,14 @@ run proc( } } .call: { - callee :: frames.items[frame_index].values[usize(instruction.left)].data.function + callable :: frames.items[frame_index].values[usize(instruction.left)] + if (callable.data.function == maxval!(ir.FunctionId)) { + return try runtime_trap(diagnostic_store, .null_unwrap, instruction.span) + } + callee :: callable.data.function + if (module.functions.items[usize(callee)].external_name.len != 0) { + return try runtime_trap(diagnostic_store, .unsupported_foreign_call, instruction.span) + } try push_frame(allocator, module, &frames, callee, instruction.destination, null) # frame indices survive stack growth; pointers into frames.items do not. for (0..instruction.arguments_count) |argument_index| { @@ -251,6 +298,95 @@ run proc( continue } .union_tag: value = ir.union_tag(module, frames.items[frame_index].values[usize(instruction.left)]) + .string_length: { + string :: frames.items[frame_index].values[usize(instruction.left)].data.string + value = ir.Value{ type = .usize, data = ir.ValueData{ unsigned = u64(ir.string_bytes(module, string).len) } } + } + .string_pointer: { + string :: frames.items[frame_index].values[usize(instruction.left)].data.string + view :: &strings[usize(string)] + value = try pointer_value(&pointers, Pointer{ + root = &view.root, root_type = .u8, root_array = ir.no_array, + byte_offset = 0, bytes = view.cells, readonly = true, + }) + } + .slice_check: { + start :: frames.items[frame_index].values[usize(instruction.left)].data.unsigned + end :: frames.items[frame_index].values[usize(instruction.right)].data.unsigned + if (start > end) return try runtime_trap(diagnostic_store, .invalid_slice_bounds, instruction.span) + if (instruction.extra != ir.no_value and end > frames.items[frame_index].values[usize(instruction.extra)].data.unsigned) { + return try runtime_trap(diagnostic_store, .invalid_slice_bounds, instruction.span) + } + continue + } + .optional_check: { + operand :: frames.items[frame_index].values[usize(instruction.left)] + absent := operand.data.unsigned == maxval!(u64) + if (operand.type == .bool) absent = !operand.data.boolean + else if (operand.type == .function) absent = operand.data.function == maxval!(ir.FunctionId) + if (absent) return try runtime_trap(diagnostic_store, .null_unwrap, instruction.span) + continue + } + .pointer_cast: { + value = frames.items[frame_index].values[usize(instruction.left)] + if (value.type == .function) { + bits :: if (value.data.function == maxval!(ir.FunctionId)) maxval!(u64) else u64(value.data.function) + value.data = ir.ValueData{ unsigned = bits } + } + value.type = instruction.type + } + .sentinel_check: { + handle :: frames.items[frame_index].values[usize(instruction.left)].data.unsigned + if (handle >= u64(pointers.items.len)) return try runtime_trap(diagnostic_store, .invalid_pointer_access, instruction.span) + address :: pointer_address(module, pointers.items[usize(handle)], instruction.pointee, instruction.array_type) + if (address == null) return try runtime_trap(diagnostic_store, .invalid_pointer_access, instruction.span) + actual :: read_address(module, address?, scratch) + expected :: frames.items[frame_index].values[usize(instruction.right)] + if (!equal_value(module, actual, expected)) return try runtime_trap(diagnostic_store, .sentinel_mismatch, instruction.span) + continue + } + .memory_copy, .memory_set: { + count :: frames.items[frame_index].values[usize(instruction.extra)].data.unsigned + width :: ir.storage_size(module, instruction.pointee, instruction.array_type) + if (count == 0 or width == 0) continue + if (count > u64(divtrunc!(maxval!(usize), width))) return try runtime_trap(diagnostic_store, .invalid_pointer_access, instruction.span) + size :: usize(count) * width + destination_handle :: frames.items[frame_index].values[usize(instruction.left)].data.unsigned + if (destination_handle >= u64(pointers.items.len)) return try runtime_trap(diagnostic_store, .invalid_pointer_access, instruction.span) + destination := pointers.items[usize(destination_handle)] + if (destination.readonly or !pointer_range(module, destination, size)) return try runtime_trap(diagnostic_store, .invalid_pointer_access, instruction.span) + source := destination + if (instruction.op == .memory_copy) { + source_handle :: frames.items[frame_index].values[usize(instruction.right)].data.unsigned + if (source_handle >= u64(pointers.items.len)) return try runtime_trap(diagnostic_store, .invalid_pointer_access, instruction.span) + source = pointers.items[usize(source_handle)] + if (!pointer_range(module, source, size)) return try runtime_trap(diagnostic_store, .invalid_pointer_access, instruction.span) + if (ptrcast!(@usize, &source.root)^ == ptrcast!(@usize, &destination.root)^) { + distance :: if (source.byte_offset > destination.byte_offset) source.byte_offset - destination.byte_offset else destination.byte_offset - source.byte_offset + if (distance < size) return try runtime_trap(diagnostic_store, .overlapping_memory_copy, instruction.span) + } + } + destination_size :: pointer_size(module, destination) + destination_bytes :: scratch[..destination_size] + mem.set(destination_bytes, ir.Value{ type = .u8, data = ir.ValueData{ unsigned = 0 } }) + ir.write_storage(module, destination_bytes, destination.root^) + target :: destination_bytes[destination.byte_offset..destination.byte_offset + size] + if (instruction.op == .memory_copy) { + source_bytes := source.bytes + if (source_bytes.len == 0) { + source_bytes = source_scratch[..pointer_size(module, source)] + mem.set(source_bytes, ir.Value{ type = .u8, data = ir.ValueData{ unsigned = 0 } }) + ir.write_storage(module, source_bytes, source.root^) + } + memcopy!(target, source_bytes[source.byte_offset..source.byte_offset + size]) + } else { + element :: frames.items[frame_index].values[usize(instruction.right)] + for (0..usize(count)) |index| ir.write_storage(module, target[index * width..], element) + } + restored :: ir.read_storage(module, destination.root_type, destination.root_array, destination_bytes, true) + copy_value(module, destination.root, restored) + continue + } .index: { base :: frames.items[frame_index].values[usize(instruction.left)] index :: frames.items[frame_index].values[usize(instruction.right)].data.unsigned @@ -324,15 +460,15 @@ run proc( if (index >= u64(shape.len)) { return try runtime_trap(diagnostic_store, .index_out_of_bounds, instruction.span) } - width :: if (base.raw) ir.storage_size(module, shape.element, shape.element_array) else element_cells(module, shape) + width :: if (base.raw or base.byte_view) ir.storage_size(module, shape.element, shape.element_array) else element_cells(module, shape) start :: usize(index) * width address := base address.type = instruction.pointee address.array_type = instruction.array_type - address.cells = base.cells[start..start + width] + if (!base.byte_view) address.cells = base.cells[start..start + width] address.scalar = null address.byte_offset += usize(index) * ir.storage_size(module, shape.element, shape.element_array) - if (!address.raw and !ir.aggregate(address.type)) address.scalar = &address.cells[0] + if (!address.raw and !address.byte_view and !ir.aggregate(address.type)) address.scalar = &address.cells[0] frames.items[frame_index].addresses[usize(instruction.destination)] = address value = ir.Value{ type = .address, data = ir.ValueData{ address = u32(instruction.destination) } } } @@ -345,21 +481,21 @@ run proc( if base.initialized |initialized| { if (!initialized^) return try runtime_trap(diagnostic_store, .read_before_initialization, instruction.span) } - if (!ir.union_active(module, aggregate_view(.record, base.array_type, base.cells), usize(instruction.slot))) { + if (!ir.union_active(module, read_address(module, base, scratch), usize(instruction.slot))) { return try runtime_trap(diagnostic_store, .inactive_union_field, instruction.span) } } raw :: base.raw or shape.is_union - width :: if (raw) ir.storage_size(module, field.type, field.array_type) else if (ir.aggregate(field.type)) module.arrays.items[usize(field.array_type)].cells else 1 + width :: if (raw or base.byte_view) ir.storage_size(module, field.type, field.array_type) else if (ir.aggregate(field.type)) module.arrays.items[usize(field.array_type)].cells else 1 start :: if (raw) field.byte_offset else field.offset address := base address.type = instruction.pointee address.array_type = instruction.array_type - address.cells = base.cells[start..start + width] + if (!base.byte_view) address.cells = base.cells[start..start + width] address.scalar = null address.raw = raw address.byte_offset += field.byte_offset - if (!raw and !ir.aggregate(address.type)) address.scalar = &address.cells[0] + if (!raw and !address.byte_view and !ir.aggregate(address.type)) address.scalar = &address.cells[0] frames.items[frame_index].addresses[usize(instruction.destination)] = address value = ir.Value{ type = .address, data = ir.ValueData{ address = u32(instruction.destination) } } } @@ -374,6 +510,8 @@ run proc( root_type = address.root_type, root_array = address.root_array, byte_offset = address.byte_offset, + readonly = address.readonly, + bytes = address.bytes, }) } .pointer_address: { @@ -382,17 +520,7 @@ run proc( return try runtime_trap(diagnostic_store, .invalid_pointer_access, instruction.span) } location :: pointers.items[usize(handle)] - root := Address{ - type = location.root_type, - array_type = location.root_array, - cells = &[], - root = location.root, - root_type = location.root_type, - root_array = location.root_array, - } - if (ir.aggregate(root.type)) root.cells = root.root?^.data.elements - else root.scalar = root.root - if (project_pointer(module, root, location.byte_offset, instruction.pointee, instruction.array_type)) |address| { + if (pointer_address(module, location, instruction.pointee, instruction.array_type)) |address| { frames.items[frame_index].addresses[usize(instruction.destination)] = address value = ir.Value{ type = .address, data = ir.ValueData{ address = u32(instruction.destination) } } } else { @@ -411,7 +539,7 @@ run proc( return try runtime_trap(diagnostic_store, .invalid_pointer_access, instruction.span) } address.byte_offset += usize(index) * stride - if (address.byte_offset > ir.storage_size(module, address.root_type, address.root_array)) { + if (address.byte_offset > pointer_size(module, address)) { return try runtime_trap(diagnostic_store, .invalid_pointer_access, instruction.span) } value = try pointer_value(&pointers, address) @@ -426,13 +554,11 @@ run proc( } } if (instruction.op == .load_address) { - if (address.raw) value = ir.read_storage(module, address.type, address.array_type, address.cells, true) - else value = if (ir.aggregate(address.type)) aggregate_view(address.type, address.array_type, address.cells) else address.scalar?^ + value = read_address(module, address, scratch) } else { source :: frames.items[frame_index].values[usize(instruction.right)] - if (address.raw) ir.write_storage(module, address.cells, source) - else if (ir.aggregate(address.type)) ir.copy_aggregate(module, aggregate_view(address.type, address.array_type, address.cells), source) - else address.scalar?^ = source + if (address.readonly) return try runtime_trap(diagnostic_store, .invalid_pointer_access, instruction.span) + write_address(module, address, source, scratch) continue } } @@ -526,6 +652,11 @@ run proc( .not_equal: left.data.boolean != right.data.boolean else: unreachable } + .function: result = match instruction.op { + .equal: left.data.function == right.data.function + .not_equal: left.data.function != right.data.function + else: unreachable + } .pointer: result = match instruction.op { .equal: left.data.unsigned == right.data.unsigned .not_equal: left.data.unsigned != right.data.unsigned @@ -543,6 +674,102 @@ run proc( unreachable } +@hide:file +pointer_size proc(module @ir.Module, pointer Pointer) usize { + if (pointer.bytes.len != 0) return pointer.bytes.len + return ir.storage_size(module, pointer.root_type, pointer.root_array) +} + +@hide:file +pointer_range proc(module @ir.Module, pointer Pointer, size usize) bool { + bound :: pointer_size(module, pointer) + return pointer.byte_offset <= bound and size <= bound - pointer.byte_offset +} + +@hide:file +pointer_address proc(module @ir.Module, pointer Pointer, type ir.Type, shape ir.ArrayId) ?Address { + size :: ir.storage_size(module, type, shape) + if (!pointer_range(module, pointer, size)) return null + base := Address{ + type = pointer.root_type, array_type = pointer.root_array, cells = &[], + root = pointer.root, root_type = pointer.root_type, root_array = pointer.root_array, + readonly = pointer.readonly, bytes = pointer.bytes, + } + if (pointer.bytes.len != 0) { + base.type = type + base.array_type = shape + base.cells = pointer.bytes[pointer.byte_offset..pointer.byte_offset + size] + base.raw = true + base.byte_offset = pointer.byte_offset + return base + } + if (ir.aggregate(base.type)) base.cells = pointer.root^.data.elements + else base.scalar = pointer.root + if (project_pointer(module, base, pointer.byte_offset, type, shape)) |address| return address + # differently typed projections retain the original root and native byte offset. + base.type = type + base.array_type = shape + base.byte_offset = pointer.byte_offset + base.byte_view = true + base.scalar = null + return base +} + +@hide:file +read_address proc(module @ir.Module, address Address, scratch []mut ir.Value) ir.Value { + if (address.byte_view) { + size :: ir.storage_size(module, address.root_type, address.root_array) + cells :: scratch[..size] + mem.set(cells, ir.Value{ type = .u8, data = ir.ValueData{ unsigned = 0 } }) + ir.write_storage(module, cells, address.root?^) + return ir.read_storage(module, address.type, address.array_type, cells[address.byte_offset..], true) + } + if (address.raw) return ir.read_storage(module, address.type, address.array_type, address.cells, true) + if (ir.aggregate(address.type)) return aggregate_view(address.type, address.array_type, address.cells) + return address.scalar?^ +} + +@hide:file +write_address proc(module @ir.Module, address Address, source ir.Value, scratch []mut ir.Value) void { + if (address.byte_view) { + size :: ir.storage_size(module, address.root_type, address.root_array) + cells :: scratch[..size] + mem.set(cells, ir.Value{ type = .u8, data = ir.ValueData{ unsigned = 0 } }) + ir.write_storage(module, cells, address.root?^) + ir.write_storage(module, cells[address.byte_offset..], source) + restored :: ir.read_storage(module, address.root_type, address.root_array, cells, true) + copy_value(module, address.root?, restored) + } else if (address.raw) ir.write_storage(module, address.cells, source) + else if (ir.aggregate(address.type)) ir.copy_aggregate(module, aggregate_view(address.type, address.array_type, address.cells), source) + else address.scalar?^ = source +} + +@hide:file +equal_value proc(module @ir.Module, left, right ir.Value) bool { + if (ir.aggregate(left.type)) { + shape :: module.arrays.items[usize(left.array_type)] + if (shape.is_union) { + for (left.data.elements) |byte, index| { + if (byte.data.unsigned != right.data.elements[index].data.unsigned) return false + } + return true + } + count :: if (shape.kind == .record) shape.fields.len else shape.len + for (0..count) |index| { + if (!equal_value(module, ir.aggregate_item(module, left, index), ir.aggregate_item(module, right, index))) return false + } + return true + } + return match left.type { + .float: left.data.real == right.data.real + .bool: left.data.boolean == right.data.boolean + .function: left.data.function == right.data.function + .string: left.data.string == right.data.string + .void: true + else: left.data.unsigned == right.data.unsigned + } +} + @hide:file pointer_value proc(pointers @mut arraylist.ArrayList(Pointer), address Pointer) ir.Value ! mem.AllocError { # ponytail: linear location interning; use a location-keyed map if distinct-address workloads grow. diff --git a/source/ir/ir.hon b/source/ir/ir.hon index e6ac183..d39bfd9 100644 --- a/source/ir/ir.hon +++ b/source/ir/ir.hon @@ -146,6 +146,14 @@ Op :: enum { pointer_from_address pointer_address pointer_offset + pointer_cast + string_pointer + string_length + slice_check + sentinel_check + optional_check + memory_copy + memory_set negate convert logical_not @@ -172,6 +180,7 @@ Instruction :: struct { destination ValueId = no_value left ValueId = no_value right ValueId = no_value + extra ValueId = no_value slot u32 = 0 constant Value = Value{} arguments_start usize = 0 @@ -205,6 +214,8 @@ Block :: struct { #! producers use add_local, add_parameter, and emit; derived metadata is read-only to callers. Function :: struct { parameters arraylist.ArrayList(Type) + # owned by the module allocator; empty for interpreted/native-defined functions. + external_name []u8 = "" locals arraylist.ArrayList(Type) result Type = .void result_array_type ArrayId = no_array @@ -435,7 +446,7 @@ read_storage proc(module @Module, type Type, shape ArrayId, cells []mut Value, r .float: data = ValueData{ real = ptrcast!(@f64, &bits)^ } .bool: data = ValueData{ boolean = (bits & 1) != 0 } .string: data = ValueData{ string = StringId(bits) } - .function: data = ValueData{ function = FunctionId(bits) } + .function: data = ValueData{ unsigned = bits } .void: return Value{} else: {} } @@ -462,7 +473,7 @@ write_storage proc(module @Module, cells []mut Value, value Value) void { .float: bits = ptrcast!(@u64, &value.data.real)^ .bool: bits = if (value.data.boolean) 1 else 0 .string: bits = u64(value.data.string) - .function: bits = u64(value.data.function) + .function: bits = if (value.data.function == maxval!(FunctionId)) maxval!(u64) else u64(value.data.function) .pointer: bits = value.data.unsigned .void: return else: bits = ptrcast!(@u64, &value.data.integer)^ @@ -568,6 +579,11 @@ emit proc( and operation.op != .assign_local and operation.op != .reset_local and operation.op != .store_address + and operation.op != .slice_check + and operation.op != .sentinel_check + and operation.op != .optional_check + and operation.op != .memory_copy + and operation.op != .memory_set ) count := body.value_count if (produces_value) { @@ -618,6 +634,7 @@ deinit proc(module @mut Module) void { arraylist.deinit(&function.locals) arraylist.deinit(&function.blocks) arraylist.deinit(&function.array_storage) + if (function.external_name.len != 0) mem.free(module.allocator, function.external_name) } for (module.strings.items) |string| mem.free(module.allocator, string.allocation) diff --git a/source/lexer/scan.hon b/source/lexer/scan.hon index bf5a515..a66678e 100644 --- a/source/lexer/scan.hon +++ b/source/lexer/scan.hon @@ -21,6 +21,8 @@ SourceError :: enum { source_too_large } @hide:file keywords strmap.StaticStringMap(TokenKind) :: strmap.init([ { "proc", .proc }, + { "c_func", .c_func }, + { "null", .null }, { "return", .return }, { "break", .break }, { "continue", .continue }, @@ -297,6 +299,8 @@ scan proc( '@': .at '&': .ampersand '^': .caret + '?': .question + ';': .semicolon '(': .open_paren ')': .close_paren '[': .open_bracket diff --git a/source/lexer/token.hon b/source/lexer/token.hon index 8ef0f64..c62d331 100644 --- a/source/lexer/token.hon +++ b/source/lexer/token.hon @@ -29,6 +29,8 @@ TokenKind :: enum { distinct, alias import, test mut + c_func + null # literals ident, string @@ -49,6 +51,7 @@ TokenKind :: enum { range, range_inclusive at, ampersand, caret + question, semicolon pipe # logical diff --git a/source/llvm/instructions.hon b/source/llvm/instructions.hon index e112a98..59460ca 100644 --- a/source/llvm/instructions.hon +++ b/source/llvm/instructions.hon @@ -178,6 +178,9 @@ emit_instruction proc(state @mut State, block @ir.Block, instruction ir.Instruct } .pointer_address: { pointer :: try load(state, instruction.left) + missing :: fresh(state) + try io.print(" %r{} = icmp eq ptr %r{}, null\n", output, {missing, pointer}) + try check_failure(state, missing, .invalid_pointer_access, instruction.span) address :: fresh(state) try io.print(" %r{} = insertvalue %address poison, ptr %r{}, 0\n", output, {address, pointer}) try io.print(" %r{} = insertvalue %address %r{}, ptr null, 1\n", output, {result, address}) @@ -189,6 +192,56 @@ emit_instruction proc(state @mut State, block @ir.Block, instruction ir.Instruct try write_type(output, state.module, Type{ kind = instruction.pointee, shape = instruction.array_type }) try io.print(", ptr %r{}, i64 %r{}\n", output, {pointer, index}) } + .pointer_cast: result = try load(state, instruction.left) + .string_pointer, .string_length: { + string :: try load(state, instruction.left) + field usize :: if (instruction.op == .string_pointer) 0 else 1 + try io.print(" %r{} = extractvalue %string %r{}, {}\n", output, {result, string, field}) + } + .optional_check: { + operand :: try load(state, instruction.left) + failed :: fresh(state) + if (state.values[usize(instruction.left)].kind == .bool) { + try io.print(" %r{} = xor i1 %r{}, true\n", output, {failed, operand}) + } else try io.print(" %r{} = icmp eq ptr %r{}, null\n", output, {failed, operand}) + try check_failure(state, failed, .null_unwrap, instruction.span) + return + } + .slice_check: { + start :: try load(state, instruction.left) + end :: try load(state, instruction.right) + reversed :: fresh(state) + try io.print(" %r{} = icmp ugt i64 %r{}, %r{}\n", output, {reversed, start, end}) + failed := reversed + if (instruction.extra != ir.no_value) { + bound :: try load(state, instruction.extra) + outside :: fresh(state) + failed = fresh(state) + try io.print(" %r{} = icmp ugt i64 %r{}, %r{}\n", output, {outside, end, bound}) + try io.print(" %r{} = or i1 %r{}, %r{}\n", output, {failed, reversed, outside}) + } + try check_failure(state, failed, .invalid_slice_bounds, instruction.span) + return + } + .sentinel_check: { + pointer :: try load(state, instruction.left) + expected :: try load(state, instruction.right) + missing :: fresh(state) + try io.print(" %r{} = icmp eq ptr %r{}, null\n", output, {missing, pointer}) + try check_failure(state, missing, .invalid_pointer_access, instruction.span) + actual :: fresh(state) + element :: Type{ kind = instruction.pointee, shape = instruction.array_type } + try io.print(" %r{} = load ", output, {actual}) + try write_type(output, state.module, element) + try io.print(", ptr %r{}, align 1\n", output, {pointer}) + failed :: try sentinel_difference(state, element, actual, expected) + try check_failure(state, failed, .sentinel_mismatch, instruction.span) + return + } + .memory_copy, .memory_set: { + try emit_memory(state, instruction) + return + } .load_address, .store_address: { address :: try load(state, instruction.left) pointer :: fresh(state) @@ -204,7 +257,7 @@ emit_instruction proc(state @mut State, block @ir.Block, instruction ir.Instruct } try io.print(" %r{} = load ", output, {result}) try write_type(output, state.module, type) - try io.print(", ptr %r{}\n", output, {pointer}) + try io.print(", ptr %r{}, align 1\n", output, {pointer}) } .call: { callee :: try load(state, instruction.left) @@ -216,12 +269,12 @@ emit_instruction proc(state @mut State, block @ir.Block, instruction ir.Instruct } if (instruction.type != .void) try io.print(" %r{} = call ", output, {result}) else try io.write_all(output, " call ") - try write_type(output, state.module, type) + try write_abi_result(output, state.module, type) try io.print(" %r{}(", output, {callee}) for (0..instruction.arguments_count) |index| { if (index != 0) try io.write_all(output, ", ") operand :: block.arguments.items[instruction.arguments_start + index] - try write_type(output, state.module, state.values[usize(operand)]) + try write_abi_parameter(output, state.module, state.values[usize(operand)]) try io.print(" %r{}", output, {first_argument + index}) } try io.write_all(output, ")\n") @@ -314,6 +367,130 @@ emit_instruction proc(state @mut State, block @ir.Block, instruction ir.Instruct try store(state, instruction.destination, result) } +#! compares the complete sentinel value, including nested aggregate fields. +@hide:file +sentinel_difference proc(state @mut State, type Type, actual, expected usize) usize ! io.WriteError { + output :: state.output + if (ir.aggregate(type.kind)) { + shape :: state.module.arrays.items[usize(type.shape)] + count usize :: if (shape.is_union) divtrunc!(shape.byte_size, 8) else if (shape.kind == .record) shape.fields.len else shape.len + result := fresh(state) + try io.print(" %r{} = icmp ne i1 false, false\n", output, {result}) + for (0..count) |index| { + child :: if (shape.is_union) Type{ kind = .u64 } else if (shape.kind == .record) Type{ kind = shape.fields[index].type, shape = shape.fields[index].array_type } else Type{ kind = shape.element, shape = shape.element_array } + left :: fresh(state) + right :: fresh(state) + try io.print(" %r{} = extractvalue ", output, {left}) + try write_type(output, state.module, type) + try io.print(" %r{}, ", output, {actual}) + if (shape.is_union) try io.write_all(output, "0, ") + try io.print("{}\n %r{} = extractvalue ", output, {index, right}) + try write_type(output, state.module, type) + try io.print(" %r{}, ", output, {expected}) + if (shape.is_union) try io.write_all(output, "0, ") + try io.print("{}\n", output, {index}) + different :: try sentinel_difference(state, child, left, right) + combined :: fresh(state) + try io.print(" %r{} = or i1 %r{}, %r{}\n", output, {combined, result, different}) + result = combined + } + return result + } + if (type.kind == .string) { + left_pointer :: fresh(state) + right_pointer :: fresh(state) + left_length :: fresh(state) + right_length :: fresh(state) + try io.print(" %r{} = extractvalue %string %r{}, 0\n", output, {left_pointer, actual}) + try io.print(" %r{} = extractvalue %string %r{}, 0\n", output, {right_pointer, expected}) + try io.print(" %r{} = extractvalue %string %r{}, 1\n", output, {left_length, actual}) + try io.print(" %r{} = extractvalue %string %r{}, 1\n", output, {right_length, expected}) + pointer_difference :: try sentinel_difference(state, Type{ kind = .pointer }, left_pointer, right_pointer) + length_difference :: try sentinel_difference(state, Type{ kind = .usize }, left_length, right_length) + result :: fresh(state) + try io.print(" %r{} = or i1 %r{}, %r{}\n", output, {result, pointer_difference, length_difference}) + return result + } + result :: fresh(state) + if (type.kind == .void) { + try io.print(" %r{} = icmp ne i1 false, false\n", output, {result}) + return result + } + opcode []u8 :: if (type.kind == .float) "fcmp une" else "icmp ne" + try io.print(" %r{} = {s} ", output, {result, opcode}) + try write_type(output, state.module, type) + try io.print(" %r{}, %r{}\n", output, {actual, expected}) + return result +} + +@hide:file +emit_memory proc(state @mut State, instruction ir.Instruction) void ! io.WriteError { + output :: state.output + element :: Type{ kind = instruction.pointee, shape = instruction.array_type } + width :: ir.storage_size(state.module, element.kind, element.shape) + if (width == 0) return + destination :: try load(state, instruction.left) + operand :: try load(state, instruction.right) + count :: try load(state, instruction.extra) + size := count + if (width != 1) { + pair :: fresh(state) + overflow :: fresh(state) + size = fresh(state) + try io.print(" %r{} = call {{ i64, i1 }} @llvm.umul.with.overflow.i64(i64 %r{}, i64 {})\n", output, {pair, count, width}) + try io.print(" %r{} = extractvalue {{ i64, i1 }} %r{}, 1\n", output, {overflow, pair}) + try check_failure(state, overflow, .integer_overflow, instruction.span) + try io.print(" %r{} = extractvalue {{ i64, i1 }} %r{}, 0\n", output, {size, pair}) + } + nonempty :: fresh(state) + destination_null :: fresh(state) + try io.print(" %r{} = icmp ne i64 %r{}, 0\n", output, {nonempty, count}) + try io.print(" %r{} = icmp eq ptr %r{}, null\n", output, {destination_null, destination}) + missing := destination_null + if (instruction.op == .memory_copy) { + source_null :: fresh(state) + missing = fresh(state) + try io.print(" %r{} = icmp eq ptr %r{}, null\n", output, {source_null, operand}) + try io.print(" %r{} = or i1 %r{}, %r{}\n", output, {missing, destination_null, source_null}) + } + invalid :: fresh(state) + try io.print(" %r{} = and i1 %r{}, %r{}\n", output, {invalid, nonempty, missing}) + try check_failure(state, invalid, .invalid_pointer_access, instruction.span) + if (instruction.op == .memory_copy) { + destination_bits :: fresh(state) + source_bits :: fresh(state) + ordered :: fresh(state) + forward :: fresh(state) + backward :: fresh(state) + distance :: fresh(state) + overlap :: fresh(state) + try io.print(" %r{} = ptrtoint ptr %r{} to i64\n", output, {destination_bits, destination}) + try io.print(" %r{} = ptrtoint ptr %r{} to i64\n", output, {source_bits, operand}) + try io.print(" %r{} = icmp uge i64 %r{}, %r{}\n", output, {ordered, destination_bits, source_bits}) + try io.print(" %r{} = sub i64 %r{}, %r{}\n", output, {forward, destination_bits, source_bits}) + try io.print(" %r{} = sub i64 %r{}, %r{}\n", output, {backward, source_bits, destination_bits}) + try io.print(" %r{} = select i1 %r{}, i64 %r{}, i64 %r{}\n", output, {distance, ordered, forward, backward}) + try io.print(" %r{} = icmp ult i64 %r{}, %r{}\n", output, {overlap, distance, size}) + try check_failure(state, overlap, .overlapping_memory_copy, instruction.span) + try io.print(" call void @llvm.memcpy.p0.p0.i64(ptr %r{}, ptr %r{}, i64 %r{}, i1 false)\n", output, {destination, operand, size}) + return + } + label :: fresh(state) + index :: fresh(state) + next :: fresh(state) + active :: fresh(state) + pointer :: fresh(state) + try io.print(" br label %set_start{}\nset_start{}:\n br label %set_test{}\nset_test{}:\n", output, {label, label, label, label}) + try io.print(" %r{} = phi i64 [ 0, %set_start{} ], [ %r{}, %set_body{} ]\n", output, {index, label, next, label}) + try io.print(" %r{} = icmp ult i64 %r{}, %r{}\n", output, {active, index, count}) + try io.print(" br i1 %r{}, label %set_body{}, label %set_end{}\nset_body{}:\n", output, {active, label, label, label}) + try io.print(" %r{} = getelementptr ", output, {pointer}) + try write_type(output, state.module, element) + try io.print(", ptr %r{}, i64 %r{}\n", output, {destination, index}) + try store_payload(state, element, operand, pointer) + try io.print(" %r{} = add i64 %r{}, 1\n br label %set_test{}\nset_end{}:\n", output, {next, index, label, label}) +} + @hide:file check_union proc(state @mut State, pointer usize, shape ir.ArrayType, field usize, span diagnostics.Span) void ! io.WriteError { if (shape.tag_type == .void) return @@ -351,7 +528,7 @@ store_payload proc(state @mut State, type Type, value, pointer usize) void ! io. } try io.write_all(state.output, " store ") try write_type(state.output, state.module, type) - try io.print(" %r{}, ptr %r{}\n", state.output, {value, pointer}) + try io.print(" %r{}, ptr %r{}, align 1\n", state.output, {value, pointer}) } @hide:file diff --git a/source/llvm/llvm.hon b/source/llvm/llvm.hon index 6eae6aa..8a235f1 100644 --- a/source/llvm/llvm.hon +++ b/source/llvm/llvm.hon @@ -46,7 +46,7 @@ emit proc( try io.write_all(output, runtime) for (sources) |source, index| { try io.print("\n@p{} = private constant ", output, {index}) - try bytes_constant(output, source.path) + try bytes_constant(output, source.path, false) } try io.print("\n@paths = private constant [{} x %string] [", output, {sources.len}) for (sources) |source, index| { @@ -54,14 +54,23 @@ emit proc( try io.print("%string {{ ptr @p{}, i64 {} }}", output, {index, source.path.len}) } try io.write_all(output, "]\n") - for (module.strings.items) |_, index| { + for (module.strings.items) |string, index| { try io.print("@s{} = private constant ", output, {index}) - try bytes_constant(output, ir.string_bytes(module, ir.StringId(index))) + try bytes_constant(output, string.allocation[..string.len], true) try io.write_all(output, "\n") } for (diagnostic_store.list.items) |diagnostic, index| { try io.print("@d{} = private constant ", output, {index}) - try bytes_constant(output, enummap.get(diagnostics.Code, []u8, &diagnostics.messages, diagnostic.code)?) + try bytes_constant(output, enummap.get(diagnostics.Code, []u8, &diagnostics.messages, diagnostic.code)?, false) + try io.write_all(output, "\n") + } + memory_codes [6]diagnostics.Code :: [ + .null_unwrap, .invalid_slice_bounds, .sentinel_mismatch, + .overlapping_memory_copy, .unsupported_foreign_call, .invalid_pointer_access, + ] + for (memory_codes) |code| { + try io.print("@memory_{s} = private constant ", output, {memory_symbol(code)}) + try bytes_constant(output, enummap.get(diagnostics.Code, []u8, &diagnostics.messages, code)?, false) try io.write_all(output, "\n") } try emit_reporting(output, diagnostic_store, line_starts.items, sources, primary) @@ -77,6 +86,14 @@ emit proc( else try emit_array_printer(output, module, ir.ArrayId(index)) } for (module.functions.items) |function, index| { + if (function.external_name.len != 0) { + duplicate := false + for (module.functions.items[..index]) |previous| { + if (mem.eql(previous.external_name, function.external_name)) duplicate = true + } + if (duplicate or mem.eql(function.external_name, "write") or mem.eql(function.external_name, "__error") + or mem.eql(function.external_name, "snprintf") or mem.eql(function.external_name, "exit")) continue + } values :: try mem.alloc(Type, allocator, function.value_count) defer mem.free(allocator, values) mem.set(values, Type{}) @@ -97,8 +114,10 @@ emit proc( if (result.kind != .void) try io.write_all(output, " %r0 = ") else try io.write_all(output, " ") try io.write_all(output, "call ") - try write_type(output, module, result) - try io.print(" @f{}()\n", output, {u32(entry)}) + try write_abi_result(output, module, result) + try io.write_all(output, " ") + try function_symbol(output, module, entry) + try io.write_all(output, "()\n") if (result.kind != .void and result.kind != .invalid) { try print_call(output, module, result, 0) try io.write_all(output, " call void @out(i32 1, ptr @newline, i64 1)\n") @@ -113,13 +132,15 @@ emit proc( } @hide:file -bytes_constant proc(output io.Writer, bytes []u8) void ! io.WriteError { - try io.print("[{} x i8] c\"", output, {bytes.len}) +bytes_constant proc(output io.Writer, bytes []u8, terminated bool) void ! io.WriteError { + size usize :: if (terminated) bytes.len + 1 else bytes.len + try io.print("[{} x i8] c\"", output, {size}) digits :: "0123456789ABCDEF" for (bytes) |byte| { escaped [3]u8 :: ['\\', digits[usize(divtrunc!(byte, 16))], digits[usize(rem!(byte, 16))]] try io.write_all(output, (&escaped)[..]) } + if (terminated) try io.write_all(output, "\\00") try io.write_all(output, "\"") } @@ -161,6 +182,45 @@ write_type proc(output io.Writer, module @ir.Module, type Type) void ! io.WriteE } } +@hide:file +abi_extension proc(kind ir.Type) []u8 { + return match kind { + .i8, .i16: "signext" + .u8, .u16, .bool: "zeroext" + else: "" + } +} + +@hide +write_abi_result proc(output io.Writer, module @ir.Module, type Type) void ! io.WriteError { + extension :: abi_extension(type.kind) + if (extension.len != 0) try io.print("{s} ", output, {extension}) + try write_type(output, module, type) +} + +@hide +write_abi_parameter proc(output io.Writer, module @ir.Module, type Type) void ! io.WriteError { + try write_type(output, module, type) + extension :: abi_extension(type.kind) + if (extension.len != 0) try io.print(" {s}", output, {extension}) +} + +@hide +function_symbol proc(output io.Writer, module @ir.Module, id ir.FunctionId) void ! io.WriteError { + name :: module.functions.items[usize(id)].external_name + if (name.len == 0) { + try io.print("@f{}", output, {u32(id)}) + return + } + try io.write_all(output, "@\"") + digits :: "0123456789ABCDEF" + for (name) |byte| { + escaped [3]u8 :: ['\\', digits[usize(divtrunc!(byte, 16))], digits[usize(rem!(byte, 16))]] + try io.write_all(output, (&escaped)[..]) + } + try io.write_all(output, "\"") +} + @hide:file local_type proc(function @ir.Function, index usize) Type { type := Type{ kind = function.locals.items[index] } @@ -252,7 +312,7 @@ emit_global proc(output io.Writer, module @ir.Module, global ir.Global, index us try io.print(" call void @f{}()\n", output, {u32(global.initializer)}) } else { try io.write_all(output, " %value = call ") - try write_type(output, module, type) + try write_abi_result(output, module, type) try io.print(" @f{}()\n store ", output, {u32(global.initializer)}) try write_type(output, module, type) try io.print(" %value, ptr @g{}\n", output, {index}) @@ -406,6 +466,19 @@ check_local proc(state @State, slot u32, read bool, span diagnostics.Span) void try io.print(" call void @check_init(ptr %i{}, i1 {}, i64 {}, i64 {}, i64 {})\n", state.output, {slot, read, usize(span.source), position.line, position.column}) } +@hide:file +memory_symbol proc(code diagnostics.Code) []u8 { + return match code { + .null_unwrap: "null_unwrap" + .invalid_slice_bounds: "invalid_slice_bounds" + .sentinel_mismatch: "sentinel_mismatch" + .overlapping_memory_copy: "overlapping_memory_copy" + .unsupported_foreign_call: "unsupported_foreign_call" + .invalid_pointer_access: "invalid_pointer_access" + else: unreachable + } +} + @hide:file trap proc(state @State, code diagnostics.Code, span diagnostics.Span) void ! io.WriteError { position :: location(state.line_starts, span) @@ -413,11 +486,14 @@ trap proc(state @State, code diagnostics.Code, span diagnostics.Span) void ! io. .integer_overflow: "overflow" .index_out_of_bounds: "bounds" .inactive_union_field: "inactive_union" - else: unreachable + else: "" } message :: enummap.get(diagnostics.Code, []u8, &diagnostics.messages, code)? length :: message.len - try io.print(" call void @trap(i64 -1, i64 {}, i64 {}, i64 {}, ptr @{s}, i64 {})\n unreachable\n", state.output, {usize(span.source), position.line, position.column, symbol, length}) + try io.print(" call void @trap(i64 -1, i64 {}, i64 {}, i64 {}, ptr @", state.output, {usize(span.source), position.line, position.column}) + if (symbol.len != 0) try io.write_all(state.output, symbol) + else try io.print("memory_{s}", state.output, {memory_symbol(code)}) + try io.print(", i64 {})\n unreachable\n", state.output, {length}) } @hide @@ -439,7 +515,14 @@ write_constant proc(output io.Writer, module @ir.Module, value ir.Value) void ! } .bool: try io.print("{}", output, {value.data.boolean}) .string: try io.print("{{ ptr @s{}, i64 {} }}", output, {u32(value.data.string), ir.string_bytes(module, value.data.string).len}) - .function: try io.print("@f{}", output, {u32(value.data.function)}) + .pointer: { + if (value.data.unsigned == maxval!(u64)) try io.write_all(output, "null") + else unreachable + } + .function: { + if (value.data.function == maxval!(ir.FunctionId)) try io.write_all(output, "null") + else try function_symbol(output, module, value.data.function) + } .array: { shape :: module.arrays.items[usize(value.array_type)] try io.write_all(output, "[") @@ -482,14 +565,21 @@ write_constant proc(output io.Writer, module @ir.Module, value ir.Value) void ! @hide:file emit_function proc(state @mut State, function @ir.Function, id usize) void ! io.WriteError { output :: state.output - try io.write_all(output, "define internal ") - try write_type(output, state.module, Type{ kind = function.result, shape = function.result_array_type }) - try io.print(" @f{}(", output, {id}) + if (function.external_name.len != 0) try io.write_all(output, "declare ") + else try io.write_all(output, "define internal ") + try write_abi_result(output, state.module, Type{ kind = function.result, shape = function.result_array_type }) + try io.write_all(output, " ") + try function_symbol(output, state.module, ir.FunctionId(id)) + try io.write_all(output, "(") for (function.parameters.items) |_, index| { if (index != 0) try io.write_all(output, ", ") - try write_type(output, state.module, local_type(function, index)) + try write_abi_parameter(output, state.module, local_type(function, index)) try io.print(" %p{}", output, {index}) } + if (function.external_name.len != 0) { + try io.write_all(output, ")\n") + return + } try io.write_all(output, ") {\nentry:\n") for (function.locals.items) |_, index| { type :: local_type(function, index) diff --git a/source/llvm/runtime.hon b/source/llvm/runtime.hon index 334c265..c144407 100644 --- a/source/llvm/runtime.hon +++ b/source/llvm/runtime.hon @@ -8,6 +8,7 @@ runtime :: `declare i32 @snprintf(ptr, i64, ptr, ...) `declare void @exit(i32) noreturn `declare double @llvm.trunc.f64(double) + `declare void @llvm.memcpy.p0.p0.i64(ptr, ptr, i64, i1 immarg) `declare { i8, i1 } @llvm.sadd.with.overflow.i8(i8, i8) `declare { i8, i1 } @llvm.ssub.with.overflow.i8(i8, i8) `declare { i8, i1 } @llvm.smul.with.overflow.i8(i8, i8) diff --git a/source/lower/lower.hon b/source/lower/lower.hon index f23b54b..11bfbe0 100644 --- a/source/lower/lower.hon +++ b/source/lower/lower.hon @@ -38,6 +38,7 @@ State :: struct { block ir.BlockId = ir.BlockId(0) terminated bool = false loop ?LoopTargets = null + return_type typecheck.Type = typecheck.Type{} } #! lowers checked source into caller-owned executable storage; release with ir.deinit. @@ -136,6 +137,8 @@ lower_type proc(state @State, type typecheck.Type) ir.Type { if (type.kind == .enumeration) { return lower_type(state, typecheck.Type{ kind = typecheck.enum_type(state.checked, type.enum_type).backing }) } + if (type.kind == .slice) return .record + if (type.kind == .optional) return lower_optional_type(state, typecheck.array_type(state.checked, type.array_type).element) return match type.kind { .int: .int .usize: .usize @@ -156,13 +159,21 @@ lower_type proc(state @State, type typecheck.Type) ir.Type { .void: .void .function: .function .enumeration: unreachable - .none, .invalid, .range, .type_value: .invalid + .none, .invalid, .range, .type_value, .null, .anyopaque: .invalid + .slice, .optional: unreachable } } +@hide:file +lower_optional_type proc(state @State, child typecheck.Type) ir.Type { + if (child.kind == .pointer or child.kind == .function) return lower_type(state, child) + return .record +} + @hide:file array_shape proc(state @mut State, type typecheck.Type) ir.ArrayId ! (mem.AllocError | diagnostics.CapacityError) { if (type.kind == .record) return try prepare_record(state, type.record_type) + if (type.kind == .slice or type.kind == .optional) return try prepare_container(state, type) if (type.kind != .array) return ir.no_array # checker history includes unresolved shapes; materialize only final used types. return try prepare_array(state, type.array_type) @@ -173,7 +184,7 @@ prepare_array proc(state @mut State, id typecheck.ArrayId) ir.ArrayId ! (mem.All if (state.arrays[usize(id)] != ir.no_array) return state.arrays[usize(id)] descriptor :: typecheck.array_type(state.checked, id) child := ir.no_array - if (descriptor.element.kind == .array or descriptor.element.kind == .record) child = try array_shape(state, descriptor.element) + child = try array_shape(state, descriptor.element) result :: try ir.add_array( state.module, lower_type(state, descriptor.element), @@ -185,6 +196,22 @@ prepare_array proc(state @mut State, id typecheck.ArrayId) ir.ArrayId ! (mem.All return result } +@hide:file +prepare_container proc(state @mut State, type typecheck.Type) ir.ArrayId ! (mem.AllocError | diagnostics.CapacityError) { + descriptor :: typecheck.array_type(state.checked, type.array_type) + if (type.kind == .optional and (descriptor.element.kind == .pointer or descriptor.element.kind == .function)) return ir.no_array + index :: usize(type.array_type) + if (state.arrays[index] != ir.no_array) return state.arrays[index] + fields [2]mut ir.RecordField := [{ type = .pointer }, { type = .usize }] + if (type.kind == .optional) { + fields[0] = ir.RecordField{ type = .bool } + fields[1] = ir.RecordField{ type = lower_type(state, descriptor.element), array_type = try array_shape(state, descriptor.element) } + } + shape :: try ir.add_record(state.module, fields[..]) + state.arrays[index] = shape + return shape +} + @hide:file prepare_record proc(state @mut State, id typecheck.RecordId) ir.ArrayId ! (mem.AllocError | diagnostics.CapacityError) { if (state.records[usize(id)] != ir.no_array) return state.records[usize(id)] @@ -315,7 +342,7 @@ lower_global proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diag if (trap_binding(state, id)) return declaration :: ast.statement_decl(state.tree, id) if (declaration.type) |annotation| if (trap_type(state, annotation)) return - value :: try lower_expression(state, declaration.initializer) + value :: try lower_as(state, declaration.initializer, typecheck.type_of(state.checked, id)) if (state.terminated or trap_type(state, id)) return finish(state, value, declaration.span) } @@ -327,6 +354,7 @@ lower_function proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | di state.function = usize(state.storage[usize(id)].index) state.block = ir.BlockId(0) state.terminated = false + state.return_type = typecheck.type_of(state.checked, declaration.return_type) state.module.functions.items[state.function].result = lower_type(state, typecheck.type_of(state.checked, declaration.return_type)) state.module.functions.items[state.function].result_array_type = try array_shape(state, typecheck.type_of(state.checked, declaration.return_type)) for (0..ast.function_param_count(tree, id)) |i| { @@ -340,6 +368,14 @@ lower_function proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | di ) state.storage[usize(parameter)] = Storage{ kind = .local, index = u32(local) } } + if (ast.function_external(tree, id)) { + if (trap_binding(state, id) or trap_type(state, id)) return + name :: ast.source_text(tree, declaration.name) + owned :: try mem.alloc(u8, state.module.allocator, name.len) + memcopy!(owned, name) + state.module.functions.items[state.function].external_name = owned + return + } try predeclare_locals(state, declaration.body) # signature errors are entry traps; body errors are reached in statement order. if (trap_binding(state, id) or trap_type(state, declaration.return_type)) return @@ -375,10 +411,15 @@ predeclare_locals proc(state @mut State, block ast.NodeId) void ! (mem.AllocErro } .if_stmt: { conditional :: ast.if_stmt(state.tree, statement) + if (conditional.capture) |capture| try predeclare_capture(state, capture) try predeclare_locals(state, conditional.then_block) if (conditional.else_block) |alternative| try predeclare_locals(state, alternative) } - .while_stmt: try predeclare_locals(state, ast.while_stmt(state.tree, statement).body) + .while_stmt: { + loop :: ast.while_stmt(state.tree, statement) + if (loop.capture) |capture| try predeclare_capture(state, capture) + try predeclare_locals(state, loop.body) + } .for_stmt: { loop :: ast.for_stmt(state.tree, statement) family :: typecheck.type_of(state.checked, loop.iterable).range_element @@ -397,6 +438,19 @@ predeclare_locals proc(state @mut State, block ast.NodeId) void ! (mem.AllocErro } } +@hide:file +predeclare_capture proc(state @mut State, capture ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) { + type :: typecheck.type_of(state.checked, capture) + slot :: try ir.add_local(state.module, ir.FunctionId(state.function), lower_type(state, type), try array_shape(state, type)) + state.storage[usize(capture)] = Storage{ kind = .local, index = u32(slot) } +} + +@hide:file +store_capture proc(state @mut State, capture ast.NodeId, value ir.ValueId, optional_type typecheck.Type, span diagnostics.Span) void ! (mem.AllocError | diagnostics.CapacityError) { + payload :: try optional_payload(state, value, optional_type, span) + _ = try emit(state, ir.Instruction{ op = .store_local, left = payload, slot = state.storage[usize(capture)].index, span = span }) +} + @hide:file lower_block proc(state @mut State, block ast.NodeId, fresh_scope bool) void ! (mem.AllocError | diagnostics.CapacityError) { # reset every direct declaration before control can return or trap within the scope. @@ -430,8 +484,10 @@ add_block proc(state @mut State) ir.BlockId ! mem.AllocError { @hide:file lower_if proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) { conditional :: ast.if_stmt(state.tree, id) - condition :: try lower_expression(state, conditional.condition) + value :: try lower_expression(state, conditional.condition) if (state.terminated) return + condition := value + if (conditional.capture != null) condition = try optional_present(state, value, typecheck.type_of(state.checked, conditional.condition), conditional.span) origin :: state.block consequent :: try add_block(state) alternative :: try add_block(state) @@ -443,6 +499,7 @@ lower_if proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnost span = ast.node_span(state.tree, conditional.condition), } state.block = consequent + if (conditional.capture) |capture| try store_capture(state, capture, value, typecheck.type_of(state.checked, conditional.condition), conditional.span) try lower_block(state, conditional.then_block, true) consequent_end :: state.block consequent_falls :: !state.terminated @@ -479,8 +536,10 @@ lower_while proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagn kind = .jump, target = guard, span = loop.span, } state.block = guard - condition :: try lower_expression(state, loop.condition) + value :: try lower_expression(state, loop.condition) if (state.terminated) return + condition := value + if (loop.capture != null) condition = try optional_present(state, value, typecheck.type_of(state.checked, loop.condition), loop.span) guard_end :: state.block body :: try add_block(state) exit :: try add_block(state) @@ -495,6 +554,7 @@ lower_while proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagn state.loop = LoopTargets{ continue_target = guard, exit = exit } defer state.loop = previous_loop state.block = body + if (loop.capture) |capture| try store_capture(state, capture, value, typecheck.type_of(state.checked, loop.condition), loop.span) try lower_block(state, loop.body, true) if (!state.terminated) { state.module.functions.items[state.function].blocks.items[usize(state.block)].terminator = ir.Terminator{ @@ -594,7 +654,7 @@ lower_statement proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | d or ast.expr_kind(tree, assignment.target) == .unary or (ast.expr_kind(tree, assignment.target) == .selection and sema.binding(state.resolution, assignment.target).kind == .none)) { - value :: try lower_expression(state, assignment.value) + value :: try lower_as(state, assignment.value, typecheck.type_of(state.checked, assignment.target)) if (state.terminated) return if (!is_place(state, assignment.target)) { _ = try lower_expression(state, assignment.target) @@ -613,7 +673,7 @@ lower_statement proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | d return } if (trap_binding(state, assignment.target) or trap_type(state, id)) return - value :: try lower_expression(state, assignment.value) + value :: try lower_as(state, assignment.value, typecheck.type_of(state.checked, assignment.target)) if (state.terminated) return binding :: sema.binding(state.resolution, assignment.target) _ = try emit(state, ir.Instruction{ @@ -636,7 +696,7 @@ lower_statement proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | d if (trap_binding(state, id)) return declaration :: ast.statement_decl(tree, id) if (declaration.type) |annotation| if (trap_type(state, annotation)) return - value :: try lower_expression(state, declaration.initializer) + value :: try lower_as(state, declaration.initializer, typecheck.type_of(state.checked, id)) if (state.terminated or trap_type(state, id)) return _ = try emit(state, ir.Instruction{ op = .store_local, @@ -650,7 +710,7 @@ lower_statement proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | d .return_stmt: { statement :: ast.return_stmt(tree, id) value := ir.no_value - if (statement.value) |expression| value = try lower_expression(state, expression) + if (statement.value) |expression| value = try lower_as(state, expression, state.return_type) if (state.terminated or trap_type(state, id)) return finish(state, value, statement.span) } @@ -827,7 +887,7 @@ lower_match proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError | state.block = body state.terminated = false if (arm_view.expression) { - value :: try lower_expression(state, arm_view.body) + value :: try lower_as(state, arm_view.body, type) if (!state.terminated and runtime != .void) _ = try emit(state, ir.Instruction{ op = .store_local, left = value, slot = result_slot, span = span, }) @@ -867,7 +927,7 @@ lower_union proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError | if (contextual and field.type == .record and (count != 1 or ast.field(tree, ast.record_field(tree, id, 0)).name != null)) { payload = try lower_record_as(state, id, typecheck.union_payload_type(state.checked, id)) } else if (count != 0) { - payload = try lower_expression(state, ast.field(tree, ast.record_field(tree, id, 0)).value?) + payload = try lower_as(state, ast.field(tree, ast.record_field(tree, id, 0)).value?, typecheck.union_payload_type(state.checked, id)) } } if (state.terminated) return ir.no_value @@ -883,15 +943,31 @@ lower_record_as proc(state @mut State, id ast.NodeId, type typecheck.Type) ir.Va scratch_start :: state.arguments.items.len defer state.arguments.items = state.arguments.items.ptr[..scratch_start] explicit_count :: ast.record_field_count(tree, id) + if (type.kind != .record) { + for (0..explicit_count) |i| { + _ = try lower_expression(state, ast.field(tree, ast.record_field(tree, id, i)).value?) + if (state.terminated) return ir.no_value + } + _ = trap_type(state, id) + return ir.no_value + } + descriptor :: typecheck.record_type(state.checked, type.record_type) try arraylist.reserve(&state.arguments, scratch_start + explicit_count) for (0..explicit_count) |_| try arraylist.append(&state.arguments, ir.no_value) for (0..explicit_count) |i| { - value :: try lower_expression(state, ast.field(tree, ast.record_field(tree, id, i)).value?) + initializer :: ast.field(tree, ast.record_field(tree, id, i)).value? + expected := typecheck.Type{} + for (descriptor.fields) |field, slot| { + if (typecheck.initializer_field(state.checked, id, slot) == initializer) { + expected = field.type + break + } + } + value :: try lower_as(state, initializer, expected) if (state.terminated) return ir.no_value state.arguments.items[scratch_start + i] = value } if (trap_type(state, id)) return ir.no_value - descriptor :: typecheck.record_type(state.checked, type.record_type) ordered_start :: state.arguments.items.len try arraylist.reserve(&state.arguments, ordered_start + descriptor.fields.len) for (descriptor.fields) |_| try arraylist.append(&state.arguments, ir.no_value) @@ -905,7 +981,7 @@ lower_record_as proc(state @mut State, id ast.NodeId, type typecheck.Type) ir.Va break } } - if (value == ir.no_value) value = try lower_expression(state, initializer) + if (value == ir.no_value) value = try lower_as(state, initializer, descriptor.fields[i].type) if (state.terminated) return ir.no_value state.arguments.items[ordered_start + i] = value } @@ -926,10 +1002,14 @@ lower_record_as proc(state @mut State, id ast.NodeId, type typecheck.Type) ir.Va @hide:file is_place proc(state @State, id ast.NodeId) bool { match ast.expr_kind(state.tree, id) { - .unary: return ast.unary_expr(state.tree, id).operator == .dereference + .unary: { + expression :: ast.unary_expr(state.tree, id) + return expression.operator == .dereference + } .index: { base :: ast.index_expr(state.tree, id).base - return typecheck.type_of(state.checked, base).kind == .pointer or is_place(state, base) + kind :: typecheck.type_of(state.checked, base).kind + return kind == .pointer or kind == .slice or kind == .string or is_place(state, base) } .selection: { if (sema.binding(state.resolution, id).kind == .none) { @@ -967,6 +1047,259 @@ pointer_address proc( }) } +@hide:file +usize_constant proc(state @mut State, value usize, span diagnostics.Span) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) { + return try constant(state, ir.Value{ type = .usize, data = ir.ValueData{ unsigned = u64(value) } }, span) +} + +@hide:file +null_constant proc(state @mut State, type ir.Type, span diagnostics.Span) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) { + return try constant(state, ir.Value{ type = type, data = ir.ValueData{ unsigned = maxval!(u64) } }, span) +} + +@hide:file +record_pair proc(state @mut State, shape ir.ArrayId, first, second ir.ValueId, span diagnostics.Span) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) { + block :: &state.module.functions.items[state.function].blocks.items[usize(state.block)] + start :: block.arguments.items.len + try arraylist.append(&block.arguments, first) + try arraylist.append(&block.arguments, second) + return try emit(state, ir.Instruction{ + op = .record_literal, type = .record, array_type = shape, + arguments_start = start, arguments_count = 2, span = span, + }) +} + +@hide:file +project proc(state @mut State, value ir.ValueId, type ir.Type, shape ir.ArrayId, slot u32, span diagnostics.Span) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) { + return try emit(state, ir.Instruction{ op = .field, type = type, array_type = shape, left = value, slot = slot, span = span }) +} + +@hide:file +optional_present proc(state @mut State, value ir.ValueId, type typecheck.Type, span diagnostics.Span) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) { + child :: typecheck.array_type(state.checked, type.array_type).element + runtime :: lower_type(state, child) + if (child.kind == .pointer or child.kind == .function) { + absent :: try null_constant(state, runtime, span) + return try emit(state, ir.Instruction{ op = .not_equal, type = .bool, left = value, right = absent, span = span }) + } + return try project(state, value, .bool, ir.no_array, 0, span) +} + +@hide:file +optional_payload proc(state @mut State, value ir.ValueId, type typecheck.Type, span diagnostics.Span) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) { + child :: typecheck.array_type(state.checked, type.array_type).element + runtime :: lower_type(state, child) + if (child.kind == .pointer or child.kind == .function) return value + return try project(state, value, runtime, try array_shape(state, child), 1, span) +} + +@hide:file +zero_value proc(state @mut State, type ir.Type, shape ir.ArrayId, span diagnostics.Span) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) { + if (!ir.aggregate(type)) { + value := ir.Value{ type = type, data = ir.ValueData{ unsigned = 0 } } + if (type == .pointer or type == .function) value.data = ir.ValueData{ unsigned = maxval!(u64) } + return try constant(state, value, span) + } + descriptor :: state.module.arrays.items[usize(shape)] + if (descriptor.is_union) return try emit(state, ir.Instruction{ op = .union_literal, type = type, array_type = shape, span = span }) + scratch :: state.arguments.items.len + defer state.arguments.items = state.arguments.items.ptr[..scratch] + count :: if (type == .array) descriptor.len else descriptor.fields.len + for (0..count) |_| try arraylist.append(&state.arguments, ir.no_value) + for (0..count) |i| { + child :: if (type == .array) descriptor.element else descriptor.fields[i].type + child_shape :: if (type == .array) descriptor.element_array else descriptor.fields[i].array_type + value :: try zero_value(state, child, child_shape, span) + state.arguments.items[scratch + i] = value + } + block :: &state.module.functions.items[state.function].blocks.items[usize(state.block)] + start :: block.arguments.items.len + for (0..count) |i| try arraylist.append(&block.arguments, state.arguments.items[scratch + i]) + op ir.Op :: if (type == .array) .array_literal else .record_literal + return try emit(state, ir.Instruction{ + op = op, type = type, array_type = shape, + arguments_start = start, arguments_count = count, span = span, + }) +} + +@hide:file +lower_as proc(state @mut State, id ast.NodeId, expected typecheck.Type) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) { + actual :: typecheck.type_of(state.checked, id) + span :: ast.node_span(state.tree, id) + if (actual.kind == .invalid or expected.kind == .invalid) return try lower_expression(state, id) + if (expected.kind == .optional and actual.kind != .optional) { + child :: typecheck.array_type(state.checked, expected.array_type).element + runtime :: lower_type(state, child) + nullable :: child.kind == .pointer or child.kind == .function + if (actual.kind == .null and nullable) return try null_constant(state, runtime, span) + payload := ir.no_value + if (actual.kind == .null) payload = try zero_value(state, runtime, try array_shape(state, child), span) + else payload = try lower_as(state, id, child) + if (state.terminated or nullable) return payload + present :: try constant(state, ir.Value{ type = .bool, data = ir.ValueData{ boolean = actual.kind != .null } }, span) + return try record_pair(state, try array_shape(state, expected), present, payload, span) + } + if (expected.kind == .slice and actual.kind != .slice) { + parts :: try slice_parts(state, id) + if (state.terminated) return ir.no_value + return try record_pair(state, try array_shape(state, expected), parts.pointer, parts.bound, span) + } + return try lower_expression(state, id) +} + +@hide:file +SliceParts :: struct { + pointer ir.ValueId + bound ir.ValueId + element typecheck.Type +} + +@hide:file +borrow_expression proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) { + if (is_place(state, id)) return try lower_place(state, id) + value :: try lower_expression(state, id) + if (state.terminated) return ir.no_value + checked :: typecheck.type_of(state.checked, id) + type :: lower_type(state, checked) + shape :: try array_shape(state, checked) + span :: ast.node_span(state.tree, id) + slot :: u32(try ir.add_local(state.module, ir.FunctionId(state.function), type, shape)) + _ = try emit(state, ir.Instruction{ op = .store_local, type = type, array_type = shape, left = value, slot = slot, span = span }) + return try emit(state, ir.Instruction{ op = .address_local, type = .address, pointee = type, array_type = shape, slot = slot, span = span }) +} + +@hide:file +slice_parts proc(state @mut State, id ast.NodeId) SliceParts ! (mem.AllocError | diagnostics.CapacityError) { + checked :: typecheck.type_of(state.checked, id) + span :: ast.node_span(state.tree, id) + result := SliceParts{ pointer = ir.no_value, bound = ir.no_value, element = typecheck.Type{} } + if (checked.kind == .slice) { + value :: try lower_expression(state, id) + if (state.terminated) return result + result.pointer = try project(state, value, .pointer, ir.no_array, 0, span) + result.bound = try project(state, value, .usize, ir.no_array, 1, span) + result.element = typecheck.array_type(state.checked, checked.array_type).element + return result + } + if (checked.kind == .string) { + value :: try lower_expression(state, id) + if (state.terminated) return result + result.pointer = try emit(state, ir.Instruction{ op = .string_pointer, type = .pointer, left = value, span = span }) + result.bound = try emit(state, ir.Instruction{ op = .string_length, type = .usize, left = value, span = span }) + result.element = typecheck.Type{ kind = .u8 } + return result + } + if (checked.kind == .pointer) { + descriptor :: typecheck.pointer_type(state.checked, checked.pointer_type) + result.pointer = try lower_expression(state, id) + if (state.terminated) return result + result.element = descriptor.element + if (!descriptor.many and descriptor.element.kind == .array) { + array :: typecheck.array_type(state.checked, descriptor.element.array_type) + result.element = array.element + result.bound = try usize_constant(state, array.len, span) + } + return result + } + if (checked.kind == .array) { + descriptor :: typecheck.array_type(state.checked, checked.array_type) + address :: try borrow_expression(state, id) + if (state.terminated) return result + result.pointer = try emit(state, ir.Instruction{ + op = .pointer_from_address, type = .pointer, left = address, + pointee = .array, array_type = try array_shape(state, checked), span = span, + }) + result.bound = try usize_constant(state, descriptor.len, span) + result.element = descriptor.element + return result + } + _ = try lower_expression(state, id) + return result +} + +@hide:file +lower_slice proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) { + expression :: ast.slice_expr(state.tree, id) + span :: ast.node_span(state.tree, id) + parts :: try slice_parts(state, expression.base) + if (state.terminated) return ir.no_value + start := ir.no_value + if (expression.start) |node| start = try lower_expression(state, node) + else start = try usize_constant(state, 0, span) + if (state.terminated) return ir.no_value + end := parts.bound + if (expression.end) |node| end = try lower_expression(state, node) + if (state.terminated or trap_type(state, id)) return ir.no_value + _ = try emit(state, ir.Instruction{ op = .slice_check, left = start, right = end, extra = parts.bound, span = span }) + child :: parts.element + runtime :: lower_type(state, child) + shape :: try array_shape(state, child) + if (expression.sentinel) |node| { + sentinel :: try lower_as(state, node, child) + if (state.terminated) return ir.no_value + base_type :: typecheck.type_of(state.checked, expression.base) + has_sentinel := base_type.kind == .string + if (base_type.kind == .slice) has_sentinel = typecheck.array_type(state.checked, base_type.array_type).sentinel != null + if (parts.bound != ir.no_value and !has_sentinel) { + one :: try usize_constant(state, 1, span) + through :: try emit(state, ir.Instruction{ op = .add, type = .usize, left = end, right = one, span = span }) + _ = try emit(state, ir.Instruction{ op = .slice_check, left = end, right = through, extra = parts.bound, span = span }) + } + pointer :: try emit(state, ir.Instruction{ op = .pointer_offset, type = .pointer, left = parts.pointer, right = end, pointee = runtime, array_type = shape, span = span }) + _ = try emit(state, ir.Instruction{ op = .sentinel_check, left = pointer, right = sentinel, pointee = runtime, array_type = shape, span = span }) + } + pointer :: try emit(state, ir.Instruction{ op = .pointer_offset, type = .pointer, left = parts.pointer, right = start, pointee = runtime, array_type = shape, span = span }) + length :: try emit(state, ir.Instruction{ op = .subtract, type = .usize, left = end, right = start, span = span }) + return try record_pair(state, try array_shape(state, typecheck.type_of(state.checked, id)), pointer, length, span) +} + +@hide:file +lower_builtin proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) { + tree :: state.tree + span :: ast.node_span(tree, id) + name :: ast.source_text(tree, ast.node_span(tree, ast.builtin_call(tree, id).name)) + if (trap_type(state, id)) return ir.no_value + if (mem.eql(name, "sizeof") or mem.eql(name, "alignof")) { + checked :: typecheck.type_of(state.checked, ast.call_arg(tree, id, 0)) + type :: lower_type(state, checked) + shape :: try array_shape(state, checked) + size :: if (mem.eql(name, "sizeof")) ir.storage_size(state.module, type, shape) else ir.storage_alignment(state.module, type, shape) + return try usize_constant(state, size, span) + } + if (mem.eql(name, "ptrcast") or mem.eql(name, "constcast")) { + operand :: ast.call_arg(tree, id, ast.call_arg_count(tree, id) - 1) + value :: try lower_expression(state, operand) + if (state.terminated or trap_type(state, id)) return ir.no_value + type :: typecheck.type_of(state.checked, id) + if (lower_type(state, type) == .record) return value + return try emit(state, ir.Instruction{ op = .pointer_cast, type = lower_type(state, type), left = value, span = span }) + } + if (mem.eql(name, "memcopy") or mem.eql(name, "memset")) { + destination :: try slice_parts(state, ast.call_arg(tree, id, 0)) + if (state.terminated) return ir.no_value + source := SliceParts{ pointer = ir.no_value, bound = ir.no_value, element = typecheck.Type{} } + value := ir.no_value + if (mem.eql(name, "memcopy")) source = try slice_parts(state, ast.call_arg(tree, id, 1)) + else value = try lower_as(state, ast.call_arg(tree, id, 1), destination.element) + if (state.terminated or trap_type(state, id)) return ir.no_value + if (mem.eql(name, "memcopy")) { + zero :: try usize_constant(state, 0, span) + _ = try emit(state, ir.Instruction{ op = .slice_check, left = zero, right = source.bound, extra = destination.bound, span = span }) + _ = try emit(state, ir.Instruction{ op = .slice_check, left = zero, right = destination.bound, extra = source.bound, span = span }) + value = source.pointer + } + op ir.Op :: if (mem.eql(name, "memcopy")) .memory_copy else .memory_set + _ = try emit(state, ir.Instruction{ + op = op, + left = destination.pointer, right = value, extra = destination.bound, + pointee = lower_type(state, destination.element), array_type = try array_shape(state, destination.element), span = span, + }) + return ir.no_value + } + if (!trap_type(state, id)) trap(state, try diagnostics.add(state.diagnostics, .invalid_operand, span), span) + return ir.no_value +} + @hide:file lower_place proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) { span :: ast.node_span(state.tree, id) @@ -1002,6 +1335,20 @@ lower_place proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError | if (ast.expr_kind(state.tree, id) == .index) { expression :: ast.index_expr(state.tree, id) base_type :: typecheck.type_of(state.checked, expression.base) + if (base_type.kind == .slice or base_type.kind == .string) { + parts :: try slice_parts(state, expression.base) + if (state.terminated) return ir.no_value + index :: try lower_expression(state, expression.index) + if (state.terminated or trap_type(state, id)) return ir.no_value + one :: try usize_constant(state, 1, span) + end :: try emit(state, ir.Instruction{ op = .add, type = .usize, left = index, right = one, span = span }) + _ = try emit(state, ir.Instruction{ op = .slice_check, left = index, right = end, extra = parts.bound, span = span }) + pointer :: try emit(state, ir.Instruction{ + op = .pointer_offset, type = .pointer, left = parts.pointer, right = index, + pointee = lower_type(state, checked), array_type = try array_shape(state, checked), span = span, + }) + return try pointer_address(state, pointer, checked, span) + } base := ir.no_value if (base_type.kind == .pointer) { pointer :: try lower_expression(state, expression.base) @@ -1065,7 +1412,7 @@ lower_expression proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocEr # ranges have no physical value; only lower_for decomposes an iterable. if (!trap_type(state, id)) unreachable } - .array_type, .pointer_type, .function_type, .record_type, .enum_type, .union_type, .distinct_type: { + .array_type, .pointer_type, .function_type, .record_type, .enum_type, .union_type, .distinct_type, .slice_type, .optional_type: { if (!trap_type(state, id)) { trap(state, try diagnostics.add(state.diagnostics, .type_not_value, span), span) } @@ -1075,6 +1422,9 @@ lower_expression proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocEr if (!trap_type(state, id)) unreachable } .match_expr: return try lower_match(state, id) + .literal_null: return try null_constant(state, .pointer, span) + .slice: return try lower_slice(state, id) + .builtin_call: return try lower_builtin(state, id) .record_literal: { checked :: typecheck.type_of(state.checked, id) if (checked.kind == .record and typecheck.record_type(state.checked, checked.record_type).is_union) { @@ -1089,7 +1439,8 @@ lower_expression proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocEr try arraylist.reserve(&state.arguments, scratch_start + count) for (0..count) |_| try arraylist.append(&state.arguments, ir.no_value) for (0..count) |i| { - element :: try lower_expression(state, ast.array_element(tree, id, i)) + expected :: typecheck.array_type(state.checked, typecheck.type_of(state.checked, id).array_type).element + element :: try lower_as(state, ast.array_element(tree, id, i), expected) if (state.terminated) return ir.no_value state.arguments.items[scratch_start + i] = element } @@ -1144,6 +1495,14 @@ lower_expression proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocEr and sema.binding(state.resolution, id).kind == .none) { selection :: ast.selection_expr(tree, id) base_type :: typecheck.type_of(state.checked, selection.base) + if (base_type.kind == .slice or base_type.kind == .string or base_type.kind == .array) { + member :: ast.source_text(tree, ast.node_span(tree, selection.member)) + if (mem.eql(member, "len") or mem.eql(member, "ptr")) { + parts :: try slice_parts(state, selection.base) + if (state.terminated or trap_type(state, id)) return ir.no_value + return if (mem.eql(member, "len")) parts.bound else parts.pointer + } + } if (base_type.kind == .pointer) { descriptor :: typecheck.pointer_type(state.checked, base_type.pointer_type) if (!descriptor.many and descriptor.element.kind == .array) { @@ -1192,7 +1551,20 @@ lower_expression proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocEr } .unary: { expression :: ast.unary_expr(tree, id) + if (expression.operator == .unwrap) { + operand :: try lower_expression(state, expression.operand) + if (state.terminated or trap_type(state, id)) return ir.no_value + source :: typecheck.type_of(state.checked, expression.operand) + present :: try optional_present(state, operand, source, span) + _ = try emit(state, ir.Instruction{ op = .optional_check, left = present, span = span }) + return try optional_payload(state, operand, source, span) + } if (expression.operator == .address_of) { + if (typecheck.type_of(state.checked, id).kind == .slice) { + parts :: try slice_parts(state, expression.operand) + if (state.terminated or trap_type(state, id)) return ir.no_value + return try record_pair(state, shape, parts.pointer, parts.bound, span) + } if (!is_place(state, expression.operand)) { _ = try lower_expression(state, expression.operand) if (!state.terminated) _ = trap_type(state, id) @@ -1227,7 +1599,7 @@ lower_expression proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocEr op ir.Op :: match expression.operator { .negate: .negate .logical_not: .logical_not - .address_of, .dereference: unreachable + .address_of, .dereference, .unwrap: unreachable } return try emit(state, ir.Instruction{ op = op, type = type, left = operand, span = span }) } @@ -1236,6 +1608,17 @@ lower_expression proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocEr if (expression.operator == .logical_and or expression.operator == .logical_or) { return try lower_logical(state, id) } + left_type :: typecheck.type_of(state.checked, expression.left) + right_type :: typecheck.type_of(state.checked, expression.right) + if ((expression.operator == .equal or expression.operator == .not_equal) + and (left_type.kind == .null or right_type.kind == .null)) { + optional_id :: if (left_type.kind == .null) expression.right else expression.left + value :: try lower_expression(state, optional_id) + if (state.terminated or trap_type(state, id)) return ir.no_value + present :: try optional_present(state, value, typecheck.type_of(state.checked, optional_id), span) + if (expression.operator == .not_equal) return present + return try emit(state, ir.Instruction{ op = .logical_not, type = .bool, left = present, span = span }) + } left :: try lower_expression(state, expression.left) if (state.terminated) return ir.no_value right :: try lower_expression(state, expression.right) @@ -1288,7 +1671,12 @@ lower_expression proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocEr try arraylist.reserve(&state.arguments, scratch_start + count) for (0..count) |_| try arraylist.append(&state.arguments, ir.no_value) for (0..count) |i| { - argument :: try lower_expression(state, ast.call_arg(tree, id, i)) + signature :: typecheck.type_of(state.checked, ast.call_expr(tree, id).callee).function + expected := typecheck.Type{} + if (signature != ast.NodeId(maxval!(u32)) and i < ast.function_param_count(tree, signature)) { + expected = typecheck.type_of(state.checked, ast.param(tree, ast.function_param(tree, signature, i)).type) + } + argument :: try lower_as(state, ast.call_arg(tree, id, i), expected) if (state.terminated) return ir.no_value state.arguments.items[scratch_start + i] = argument } diff --git a/source/parser/parse.hon b/source/parser/parse.hon index 5ff06d3..20c6d92 100644 --- a/source/parser/parse.hon +++ b/source/parser/parse.hon @@ -110,7 +110,7 @@ parse_decl proc(state @mut State) ast.NodeId ! ParseError { next_is(state, .import) or next_is(state, .test) or (next_is(state, .ident) and next_n_is(state, 1, .double_colon) and next_n_is(state, 2, .import)) ) return try parse_import_decl(state) - if (next_is(state, .ident) and next_n_is(state, 1, .proc)) return try parse_func_decl(state) + if (next_is(state, .ident) and (next_n_is(state, 1, .proc) or next_n_is(state, 1, .c_func))) return try parse_func_decl(state) return try parse_statement_decl(state) } @@ -144,7 +144,8 @@ parse_import_decl proc(state @mut State) ast.NodeId ! ParseError { @hide:file parse_func_decl proc(state @mut State) ast.NodeId ! ParseError { name :: try consume(state, .ident) - _ = try consume(state, .proc) + external :: next_is(state, .c_func) + _ = try consume_either(state, &[.proc, .c_func]) _ = try consume(state, .open_paren) params arraylist.ArrayList(ast.NodeId) := arraylist.init(state.allocator) @@ -152,9 +153,9 @@ parse_func_decl proc(state @mut State) ast.NodeId ! ParseError { try parse_parameters(state, ¶ms) _ = try consume(state, .close_paren) return_type :: try parse_type(state) - body :: try parse_block(state) + body :: if (external) try ast.add_block(state.builder, ast.builder_span(state.builder, return_type), &[]) else try parse_block(state) body_span :: ast.builder_span(state.builder, body) - return try ast.add_function_decl( + id :: try ast.add_function_decl( state.builder, ast.Span{ start = name.span.start, @@ -167,6 +168,8 @@ parse_func_decl proc(state @mut State) ast.NodeId ! ParseError { params.items, body, ) + if (external) ast.set_function_external(state.builder, id) + return id } @hide:file @@ -245,6 +248,7 @@ parse_statement proc(state @mut State, allow_else bool) ast.NodeId ! ParseError or next_n_is(state, 1, .enum) or next_n_is(state, 1, .union) or next_n_is(state, 1, .distinct) or next_n_is(state, 1, .at) + or next_n_is(state, 1, .question) or (next_n_is(state, 1, .star) and pointer_binding_starts(state)) or (next_n_is(state, 1, .open_bracket) and array_starts_type(state)) or next_n_is(state, 1, .double_colon) @@ -260,7 +264,7 @@ parse_statement proc(state @mut State, allow_else bool) ast.NodeId ! ParseError .return: return try parse_return(state, allow_else) .break, .continue: return try parse_loop_control(state, allow_else) .ident, .int, .float, .string, .true, .false, .minus, .bang, .open_paren, .open_bracket, .open_curly, .struct, - .enum, .union, .distinct, .alias, .dot, .match, .ampersand, .at, .star: { + .enum, .union, .distinct, .alias, .dot, .match, .ampersand, .at, .star, .question, .null: { expression :: try parse_expr(state) if next_is(state, .equal) return try parse_assignment(state, expression, allow_else) return try ast.add_expression_stmt(state.builder, expression) @@ -348,6 +352,7 @@ parse_assignment proc(state @mut State, target ast.NodeId, allow_else bool) ast. parse_if proc(state @mut State) ast.NodeId ! ParseError { keyword :: try consume(state, .if) condition :: try parse_guard(state) + capture :: try parse_optional_capture(state) braced :: next_is(state, .open_curly) then_block :: try parse_arm(state, true) # only a braced arm permits its else on a later line. @@ -364,7 +369,7 @@ parse_if proc(state @mut State) ast.NodeId ! ParseError { ast.builder_span(state.builder, block).end else ast.builder_span(state.builder, then_block).end - return try ast.add_if_stmt( + id :: try ast.add_if_stmt( state.builder, ast.Span{ start = keyword.span.start, @@ -375,14 +380,17 @@ parse_if proc(state @mut State) ast.NodeId ! ParseError { then_block, alternative, ) + if (capture) |node| ast.set_capture(state.builder, id, node) + return id } @hide:file parse_while proc(state @mut State, allow_else bool) ast.NodeId ! ParseError { keyword :: try consume(state, .while) condition :: try parse_guard(state) + capture :: try parse_optional_capture(state) body :: try parse_arm(state, allow_else) - return try ast.add_while_stmt( + id :: try ast.add_while_stmt( state.builder, ast.Span{ start = keyword.span.start, @@ -392,6 +400,18 @@ parse_while proc(state @mut State, allow_else bool) ast.NodeId ! ParseError { condition, body, ) + if (capture) |node| ast.set_capture(state.builder, id, node) + return id +} + +@hide:file +parse_optional_capture proc(state @mut State) ?ast.NodeId ! ParseError { + missing ?ast.NodeId :: null + if (!next_is(state, .pipe)) return missing + state.next_token += 1 + name :: try consume(state, .ident) + _ = try consume(state, .pipe) + return try ast.add_param(state.builder, name.span, name.span, name.data.str_id, maxval!(ast.NodeId)) } @hide:file @@ -529,6 +549,7 @@ parse_guard proc(state @mut State) ast.NodeId ! ParseError { } if ( !next_is(state, .open_curly) + and !next_is(state, .pipe) and !parenthesized_guard(state, condition_start) and ast.builder_expr_kind(state.builder, condition) != .call and ast.builder_expr_kind(state.builder, condition) != .invalid @@ -609,6 +630,14 @@ parse_statement_decl proc(state @mut State) ast.NodeId ! ParseError { @hide:file parse_type proc(state @mut State) ast.NodeId ! ParseError { + if (next_is(state, .question)) { + token :: current(state) + state.next_token += 1 + child :: try parse_type(state) + return try ast.add_optional_type(state.builder, ast.Span{ + start = token.span.start, end = ast.builder_span(state.builder, child).end, source = token.span.source, + }, child) + } if (next_is(state, .at) or next_is(state, .star)) { token :: current(state) state.next_token += 1 @@ -642,6 +671,20 @@ parse_type proc(state @mut State) ast.NodeId ! ParseError { } if next_is(state, .open_bracket) { open :: try consume(state, .open_bracket) + if (next_is(state, .close_bracket) or next_is(state, .semicolon)) { + sentinel ?ast.NodeId := null + if (next_is(state, .semicolon)) { + state.next_token += 1 + sentinel = try parse_expr(state) + } + _ = try consume(state, .close_bracket) + mutable :: next_is(state, .mut) + if (mutable) state.next_token += 1 + element :: try parse_type(state) + return try ast.add_slice_type(state.builder, ast.Span{ + start = open.span.start, end = ast.builder_span(state.builder, element).end, source = open.span.source, + }, element, mutable, sentinel) + } count ?ast.NodeId := null if ( next_is(state, .ident) @@ -690,7 +733,7 @@ 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 == .at or kind == .star or kind == .question ) } @@ -699,7 +742,7 @@ member_name proc(kind lexer.TokenKind) bool { return match kind { .ident, .if, .else, .for, .while, .proc, .return, .break, .continue, .true, .false, .and, .or, .mut, .struct, .enum, .union, .distinct, - .alias, .match, .import, .test: true + .alias, .match, .import, .test, .c_func, .null: true else: false } } @@ -1124,18 +1167,25 @@ parse_expr_bp proc(state @mut State, min_bp BindingPowerScalar) ast.NodeId ! Par while true { skip_continuation_newlines(state) token = current(state) - if (token.kind == .caret) { + if (token.kind == .caret or token.kind == .question) { state.next_token += 1 + operator ast.UnaryOperator :: if (token.kind == .caret) .dereference else .unwrap left = try ast.add_unary(state.builder, ast.Span{ start = state.tokens[expression_start].span.start, end = token.span.end, source = token.span.source, - }, .dereference, left) + }, operator, left) + left_grouped = false + continue + } + if (token.kind == .bang and next_n_is(state, 1, .open_paren)) { + state.next_token += 1 + left = try parse_call(state, left, state.tokens[expression_start].span.start, true) left_grouped = false continue } if (token.kind == .open_paren) { - left = try parse_call(state, left, state.tokens[expression_start].span.start) + left = try parse_call(state, left, state.tokens[expression_start].span.start, false) left_grouped = false continue } @@ -1228,7 +1278,7 @@ validate_range_bound proc( } @hide:file -parse_call proc(state @mut State, callee ast.NodeId, start u32) ast.NodeId ! ParseError { +parse_call proc(state @mut State, callee ast.NodeId, start u32, builtin bool) ast.NodeId ! ParseError { _ = try consume(state, .open_paren) previous :: state.allow_record_literals state.allow_record_literals = true @@ -1260,16 +1310,9 @@ parse_call proc(state @mut State, callee ast.NodeId, start u32) ast.NodeId ! Par return .unexpected_token } close :: try consume(state, .close_paren) - return try ast.add_call( - state.builder, - ast.Span{ - start = start, - end = close.span.end, - source = state.builder.source_id, - }, - callee, - arguments.items, - ) + span :: ast.Span{ start = start, end = close.span.end, source = state.builder.source_id } + if (builtin) return try ast.add_builtin_call(state.builder, span, callee, arguments.items) + return try ast.add_call(state.builder, span, callee, arguments.items) } @hide:file @@ -1278,29 +1321,32 @@ parse_index proc(state @mut State, base ast.NodeId, start u32) ast.NodeId ! Pars previous :: state.allow_record_literals state.allow_record_literals = true defer state.allow_record_literals = previous - index :: try parse_expr(state) + first ?ast.NodeId := null + if (!next_is(state, .range)) first = try parse_expr_bp(state, BindingPowerScalar(1)) + if (next_is(state, .range)) { + state.next_token += 1 + end ?ast.NodeId := null + sentinel ?ast.NodeId := null + if (!next_is(state, .close_bracket) and !next_is(state, .semicolon)) end = try parse_expr(state) + if (next_is(state, .semicolon)) { + state.next_token += 1 + sentinel = try parse_expr(state) + } + close :: try consume(state, .close_bracket) + return try ast.add_slice(state.builder, ast.Span{ + start = start, end = close.span.end, source = state.builder.source_id, + }, base, first, end, sentinel) + } if !next_is(state, .close_bracket) { diagnostic :: try diagnostics.add(state.diagnostics, .unexpected_token, current(state).span) - return try ast.add_invalid_expr( - state.builder, - ast.Span{ - start = start, - end = ast.builder_span(state.builder, index).end, - source = state.builder.source_id, - }, - diagnostic, - ) + return try ast.add_invalid_expr(state.builder, ast.Span{ + start = start, end = ast.builder_span(state.builder, first?).end, source = state.builder.source_id, + }, diagnostic) } close :: try consume(state, .close_bracket) - return try ast.add_index( - state.builder, - ast.Span{ - start = start, - end = close.span.end, - source = state.builder.source_id, - }, - base, index, - ) + return try ast.add_index(state.builder, ast.Span{ + start = start, end = close.span.end, source = state.builder.source_id, + }, base, first?) } @hide:file @@ -1397,6 +1443,7 @@ named_binding_starts proc(state @State) bool { next_n_is(state, 1, .double_colon) or next_n_is(state, 1, .colon_equal) or next_n_is(state, 1, .ident) or next_n_is(state, 1, .proc) or next_n_is(state, 1, .at) + or next_n_is(state, 1, .question) or (next_n_is(state, 1, .star) and pointer_binding_starts(state)) ) ) @@ -1406,7 +1453,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: return try parse_type(state) + .at, .star, .question: 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) @@ -1468,6 +1515,10 @@ parse_primary proc(state @mut State) ast.NodeId ! ParseError { state.next_token += 1 return try ast.add_literal(state.builder, .bool, token.span) } + .null: { + state.next_token += 1 + return try ast.add_literal(state.builder, .null, token.span) + } .ident: { state.next_token += 1 return try ast.add_identifier(state.builder, token.span, token.data.str_id) diff --git a/source/sema/check/check.hon b/source/sema/check/check.hon index 7e00fdc..f843ced 100644 --- a/source/sema/check/check.hon +++ b/source/sema/check/check.hon @@ -24,6 +24,10 @@ TypeKind :: enum { type_value u8, u16, u32, u64, usize i8, i16, i32, isize + slice + optional + null + anyopaque } RecordId :: distinct u32 @@ -69,8 +73,10 @@ no_array :: maxval!(ArrayId) ArrayType :: struct { element Type - len usize - mutable bool + len usize = 0 + mutable bool = false + kind TypeKind = .array + sentinel ?i64 = null } PointerId :: distinct u32 @@ -344,6 +350,7 @@ check proc( } } # Calls use the signature result before contextual literal inference. + try prepare_optional_captures(&state, false) for (0..count) |i| { id :: ast.NodeId(i) if (cells[i].role != .expression or cells[i].assignment_value != assignment_values or ast.expr_kind(tree, id) != .call) continue @@ -478,6 +485,17 @@ check proc( } } for (0..count) |i| try validate(&state, ast.NodeId(i)) + for (0..ast.package_decl_count(tree)) |i| { + id :: ast.package_decl(tree, i) + if (ast.decl_kind(tree, id) != .function or !ast.function_external(tree, id)) continue + for (0..i) |j| { + previous :: ast.package_decl(tree, j) + if (ast.decl_kind(tree, previous) != .function or !ast.function_external(tree, previous)) continue + if (ast.name_id(tree, id) == ast.name_id(tree, previous) and !same_signature(&state, id, previous)) { + try fail(&state, id, .type_mismatch, ast.function_decl(tree, id).name) + } + } + } for state.records.items |record| { for record.fields |field| { if (field.default) |value| try compatible(&state, value, field.type) @@ -492,8 +510,10 @@ check proc( id :: ast.NodeId(i) if (cells[i].role != .if_stmt and cells[i].role != .while_stmt) continue condition :: if (cells[i].role == .if_stmt) ast.if_stmt(tree, id).condition else ast.while_stmt(tree, id).condition + capture :: if (cells[i].role == .if_stmt) ast.if_stmt(tree, id).capture else ast.while_stmt(tree, id).capture actual :: types[usize(condition)] - if (actual.kind != .invalid and (actual.kind != .bool or actual.nominal != ast.NodeId(maxval!(u32)))) { + if (capture != null and actual.kind == .optional) continue + if (actual.kind != .invalid and (capture != null or actual.kind != .bool or actual.nominal != ast.NodeId(maxval!(u32)))) { try fail(&state, condition, .condition_not_bool, ast.node_span(tree, condition)) } } @@ -511,6 +531,7 @@ check proc( for (0..ast.package_decl_count(tree)) |i| { id :: ast.package_decl(tree, i) if (ast.decl_kind(tree, id) != .function) continue + if (ast.function_external(tree, id)) continue function :: ast.function_decl(tree, id) result :: types[usize(function.return_type)] if (result.kind == .void or result.kind == .invalid or types[usize(id)].kind == .invalid) continue @@ -764,6 +785,12 @@ collect_invalid proc(state @mut State, id ast.NodeId) void { check_annotation proc(state @mut State, id ast.NodeId, allow_void bool) Type ! (mem.AllocError | diagnostics.CapacityError) { if (state.cells[usize(id)].role == .annotation) return state.types[usize(id)] state.cells[usize(id)].role = .annotation + if (ast.expr_kind(state.tree, id) == .slice_type or ast.expr_kind(state.tree, id) == .optional_type) { + type :: try check_container_type(state, id) + state.types[usize(id)] = type + state.cells[usize(id)].type = type + return type + } if (ast.expr_kind(state.tree, id) == .pointer_type) { type :: try check_pointer_type(state, id) state.types[usize(id)] = type @@ -814,6 +841,9 @@ check_annotation proc(state @mut State, id ast.NodeId, allow_void bool) Type ! ( .invalid: type = invalid_type(binding.data.diagnostic) .builtin: { type.kind = builtin_kind(binding.data.builtin) + if (type.kind == .anyopaque and state.pointer_depth == 0) { + type = invalid_type(try diagnostics.add(state.diagnostics, .expected_type, ast.node_span(state.tree, id))) + } if (type.kind == .void and !allow_void) { type = invalid_type(try diagnostics.add(state.diagnostics, .invalid_void_type, ast.node_span(state.tree, id))) } @@ -890,7 +920,15 @@ collect_function proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | state.cells[usize(id)].type = invalid_type(binding.data.diagnostic) state.cells[usize(id)].type.function = id } - try collect_block(state, function.body, function.return_type, false) + if (!ast.function_external(tree, id)) try collect_block(state, function.body, function.return_type, false) + else { + if (mem.eql(ast.source_text(tree, function.name), "main")) try fail(state, id, .invalid_entry, function.name) + if (!c_abi_type(state, state.types[usize(function.return_type)])) try fail(state, id, .invalid_operand, function.span) + for (0..ast.function_param_count(tree, id)) |i| { + parameter :: ast.function_param(tree, id, i) + if (!c_abi_type(state, state.cells[usize(parameter)].type)) try fail(state, id, .invalid_operand, ast.node_span(tree, parameter)) + } + } } @hide:file @@ -941,6 +979,11 @@ collect_block proc(state @mut State, id, return_annotation ast.NodeId, in_loop b state.cells[usize(statement)].type = Type{ kind = .void } try collect_expression(state, conditional.condition) try depend(state, conditional.condition, statement) + if (conditional.capture) |capture| { + try collect_capture(state, capture, conditional.then_block) + state.cells[usize(capture)].type = Type{} + state.cells[usize(conditional.condition)].capture = capture + } try collect_block(state, conditional.then_block, return_annotation, in_loop) try depend(state, conditional.then_block, id) if (conditional.else_block) |other| { @@ -974,6 +1017,11 @@ collect_block proc(state @mut State, id, return_annotation ast.NodeId, in_loop b state.cells[usize(statement)].type = Type{ kind = .void } try collect_expression(state, loop.condition) try depend(state, loop.condition, statement) + if (loop.capture) |capture| { + try collect_capture(state, capture, loop.body) + state.cells[usize(capture)].type = Type{} + state.cells[usize(loop.condition)].capture = capture + } try collect_block(state, loop.body, return_annotation, true) try depend(state, loop.body, id) } @@ -1042,6 +1090,25 @@ collect_expression proc(state @mut State, id ast.NodeId) void ! (mem.AllocError } .literal_bool: state.cells[usize(id)].type = Type{ kind = .bool } .literal_string: state.cells[usize(id)].type = Type{ kind = .string } + .literal_null: state.cells[usize(id)].type = Type{ kind = .null } + .builtin_call: try collect_builtin(state, id) + .slice: { + view :: ast.slice_expr(tree, id) + try collect_expression(state, view.base) + try depend(state, view.base, id) + if (view.start) |child| { + try collect_expression(state, child) + try depend(state, child, id) + } + if (view.end) |child| { + try collect_expression(state, child) + try depend(state, child, id) + } + if (view.sentinel) |child| { + try collect_expression(state, child) + try depend(state, child, id) + } + } .identifier, .selection: { binding :: sema.binding(state.resolution, id) if (binding.kind == .none and ast.expr_kind(tree, id) == .selection) { @@ -1135,7 +1202,8 @@ collect_expression proc(state @mut State, id ast.NodeId) void ! (mem.AllocError .array_type: { state.cells[usize(id)].type = invalid_type(try diagnostics.add(state.diagnostics, .type_not_value, ast.node_span(tree, id))) } - .record_type, .union_type, .enum_type, .distinct_type, .pointer_type, .function_type: { + .record_type, .union_type, .enum_type, .distinct_type, + .pointer_type, .function_type, .slice_type, .optional_type: { type := try check_type_expression(state, id, state.cells[usize(id)].nominal) if (type.kind != .invalid) { type.value_kind = type.kind @@ -1236,6 +1304,15 @@ builtin_kind proc(type sema.BuiltinType) TypeKind { .bool: .bool .string: .string .void: .void + .anyopaque: .anyopaque + .c_int: .i32 + .c_uint: .u32 + .c_long, .c_longlong: .int + .c_ulong, .c_ulonglong: .u64 + .c_char: .i8 + .c_uchar: .u8 + .c_short: .i16 + .c_ushort: .u16 } } @@ -1269,6 +1346,7 @@ pointer_addition proc(state @mut State, id ast.NodeId) bool { expression.operator == .add and left.kind == .pointer and left.nominal == ast.NodeId(maxval!(u32)) and state.pointers.items[usize(left.pointer_type)].many + and state.pointers.items[usize(left.pointer_type)].element.kind != .anyopaque ) } @@ -1281,7 +1359,9 @@ bool_equality proc(state @mut State, id ast.NodeId) bool { (expression.operator == .equal or expression.operator == .not_equal) and ((left.kind == .bool and right.kind == .bool) or left.kind == .enumeration or right.kind == .enumeration - or left.kind == .pointer or right.kind == .pointer) + or left.kind == .pointer or right.kind == .pointer + or left.kind == .optional or right.kind == .optional + or left.kind == .null or right.kind == .null) ) } @@ -1349,6 +1429,41 @@ numeric_constraint proc(state @mut State, expression, operand, operation ast.Nod demand proc(state @mut State, id ast.NodeId, expected Type, code diagnostics.Code) void ! (mem.AllocError | diagnostics.CapacityError) { if (state.types[usize(id)].kind == .invalid) return index :: root(state, usize(id)) + if (state.cells[usize(id)].role == .expression and ast.expr_kind(state.tree, id) == .builtin_call) { + name :: builtin_name(state, id) + if (mem.eql(name, "ptrcast") or mem.eql(name, "constcast")) { + state.cells[index].expected = expected + seed(state, id, expected) + return + } + } + if (expected.kind == .optional) { + if (group_type(state, id).kind != .optional and group_type(state, id).kind != .null) { + try demand(state, id, state.arrays.items[usize(expected.array_type)].element, code) + } + return + } + if (expected.kind == .slice) { + state.cells[index].expected = expected + if (ast.expr_kind(state.tree, id) == .unary and ast.unary_expr(state.tree, id).operator == .address_of) { + operand :: ast.unary_expr(state.tree, id).operand + if (ast.expr_kind(state.tree, operand) == .array_literal) { + descriptor :: state.arrays.items[usize(expected.array_type)] + array :: try intern_array(state, ArrayType{ + element = descriptor.element, len = ast.array_element_count(state.tree, operand), mutable = descriptor.mutable, + }) + try demand(state, operand, array, code) + seed(state, id, expected) + } + } else if (ast.expr_kind(state.tree, id) == .array_literal) { + descriptor :: state.arrays.items[usize(expected.array_type)] + array :: try intern_array(state, ArrayType{ + element = descriptor.element, len = ast.array_element_count(state.tree, id), mutable = descriptor.mutable, + }) + try demand(state, id, array, code) + } + return + } if (expected.kind == .pointer and state.cells[usize(id)].role == .expression and ast.expr_kind(state.tree, id) == .unary and ast.unary_expr(state.tree, id).operator == .address_of) { @@ -1471,6 +1586,7 @@ mutable_target proc(state @mut State, id ast.NodeId, allow_global bool) bool { return pointer.mutable and (pointer.many or (pointer.element.kind == .array and state.arrays.items[usize(pointer.element.array_type)].mutable)) } + if (base.kind == .slice) return state.arrays.items[usize(base.array_type)].mutable return base.kind == .array and state.arrays.items[usize(base.array_type)].mutable and mutable_target(state, view.base, allow_global) } if (kind == .selection and sema.binding(state.resolution, id).kind == .none) { @@ -1492,10 +1608,10 @@ mutable_target proc(state @mut State, id ast.NodeId, allow_global bool) bool { @hide:file indexable_type proc(state @State, type Type) bool { if (type.nominal != ast.NodeId(maxval!(u32))) return false - if (type.kind == .array) return true + if (type.kind == .array or type.kind == .slice or type.kind == .string) return true if (type.kind != .pointer) return false pointer :: state.pointers.items[usize(type.pointer_type)] - return pointer.many or pointer.element.kind == .array + return (pointer.many and pointer.element.kind != .anyopaque) or pointer.element.kind == .array } @hide:file @@ -1508,7 +1624,7 @@ addressable_target proc(state @mut State, id ast.NodeId) bool { if (kind == .index) { view :: ast.index_expr(state.tree, id) base :: group_type(state, view.base) - return indexable_type(state, base) and (base.kind == .pointer or addressable_target(state, view.base)) + return indexable_type(state, base) and (base.kind == .pointer or base.kind == .slice or base.kind == .string or addressable_target(state, view.base)) } if (kind == .selection and sema.binding(state.resolution, id).kind == .none) { view :: ast.selection_expr(state.tree, id) @@ -1528,6 +1644,7 @@ addressable_target proc(state @mut State, id ast.NodeId) bool { @hide:file same_signature proc(state @State, left, right ast.NodeId) bool { if (left == right) return true + if (ast.function_external(state.tree, left) != ast.function_external(state.tree, right)) return false tree :: state.tree if (ast.function_param_count(tree, left) != ast.function_param_count(tree, right)) return false left_result :: ast.callable_result(tree, left) @@ -1544,10 +1661,32 @@ same_signature proc(state @State, left, right ast.NodeId) bool { @hide:file assignable_type proc(state @State, actual, expected Type) bool { if (same_type(state, actual, expected)) return true + if (expected.kind == .optional) { + child :: state.arrays.items[usize(expected.array_type)].element + if (actual.kind == .null) return true + if (actual.kind == .optional) { + source :: state.arrays.items[usize(actual.array_type)].element + if (source.kind != child.kind) return false + return assignable_type(state, source, child) + } + return assignable_type(state, actual, child) + } + if (expected.kind == .slice) { + b :: state.arrays.items[usize(expected.array_type)] + if (actual.kind == .string) return !b.mutable and b.element.kind == .u8 and (b.sentinel == null or b.sentinel? == 0) + if (actual.kind == .slice or actual.kind == .array) { + a :: state.arrays.items[usize(actual.array_type)] + return ( + (!b.mutable or a.mutable) and (b.sentinel == null or same_sentinel(a.sentinel, b.sentinel)) + and same_type(state, a.element, b.element) + ) + } + } if (actual.kind != .pointer or expected.kind != .pointer or actual.nominal != expected.nominal) return false a :: state.pointers.items[usize(actual.pointer_type)] b :: state.pointers.items[usize(expected.pointer_type)] if (a.many != b.many or (!a.mutable and b.mutable)) return false + if (b.element.kind == .anyopaque) return true if (b.mutable) return same_type(state, a.element, b.element) return assignable_type(state, a.element, b.element) } @@ -1556,11 +1695,22 @@ assignable_type proc(state @State, actual, expected Type) bool { compatible proc(state @mut State, value ast.NodeId, expected Type) void ! (mem.AllocError | diagnostics.CapacityError) { actual :: state.types[usize(value)] if (actual.kind == .invalid or expected.kind == .invalid) return - if (!assignable_type(state, actual, expected)) { + if (!assignable_value(state, value, actual, expected)) { try fail(state, value, .type_mismatch, ast.node_span(state.tree, value)) } } +@hide:file +assignable_value proc(state @mut State, value ast.NodeId, actual, expected Type) bool { + if (!assignable_type(state, actual, expected)) return false + target := expected + while (target.kind == .optional) target = state.arrays.items[usize(target.array_type)].element + if (actual.kind == .array and target.kind == .slice + and state.arrays.items[usize(target.array_type)].mutable + and addressable_target(state, value)) return mutable_target(state, value, true) + return true +} + @hide:file validate proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) { tree :: state.tree @@ -1571,6 +1721,9 @@ validate proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnost match state.cells[usize(id)].role { .declaration: { declaration :: ast.statement_decl(tree, id) + if (declaration.type == null and state.types[usize(declaration.initializer)].kind == .null) { + try fail(state, declaration.initializer, .cannot_infer_type, ast.node_span(tree, declaration.initializer)) + } if (state.types[usize(declaration.initializer)].kind == .void) { try fail(state, declaration.initializer, .type_mismatch, ast.node_span(tree, declaration.initializer)) } @@ -1603,7 +1756,7 @@ validate proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnost actual :: state.types[usize(value)] if (actual.kind == .invalid) { state.types[usize(id)] = actual - } else if (!assignable_type(state, actual, expected)) { + } else if (!assignable_value(state, value, actual, expected)) { try fail(state, value, .return_type_mismatch, ast.node_span(tree, value)) state.types[usize(id)] = state.types[usize(value)] } @@ -1621,10 +1774,17 @@ validate proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnost } if (expression.operator == .address_of) { if (!addressable_target(state, expression.operand)) { + if (state.types[usize(id)].kind == .slice) return try fail(state, id, .invalid_address_operand, expression.span) } return } + if (expression.operator == .unwrap) { + if (state.types[usize(expression.operand)].kind != .optional and state.types[usize(expression.operand)].kind != .invalid) { + try fail(state, id, .invalid_operand, expression.span) + } + return + } if (expression.operator == .dereference) { if (state.types[usize(expression.operand)].kind != .invalid and state.types[usize(expression.operand)].kind != .pointer) { @@ -1653,6 +1813,8 @@ validate proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnost } return } + if ((expression.operator == .equal or expression.operator == .not_equal) + and ((left.kind == .optional and right.kind == .null) or (left.kind == .null and right.kind == .optional))) return if ((expression.operator == .equal or expression.operator == .not_equal) and (left.kind == .bool or left.kind == .enumeration or left.kind == .pointer) and (assignable_type(state, left, right) or assignable_type(state, right, left))) return @@ -1718,6 +1880,12 @@ validate proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnost } } .call: try validate_call(state, id) + .builtin_call: _ = try infer_builtin(state, id, true) + .slice: { + view :: ast.slice_expr(tree, id) + if (view.start) |node| try compatible(state, node, Type{ kind = .usize }) + if (view.end) |node| try compatible(state, node, Type{ kind = .usize }) + } else: {} } } @@ -1959,13 +2127,214 @@ check_function_type proc(state @mut State, id ast.NodeId) Type ! (mem.AllocError @hide:file intern_array proc(state @mut State, descriptor ArrayType) Type ! mem.AllocError { for state.arrays.items |existing, i| { - if (existing.len == descriptor.len and existing.mutable == descriptor.mutable - and same_type(state, existing.element, descriptor.element)) { - return Type{ kind = .array, array_type = ArrayId(i) } + if (existing.kind == descriptor.kind and existing.len == descriptor.len and existing.mutable == descriptor.mutable + and same_sentinel(existing.sentinel, descriptor.sentinel) and same_type(state, existing.element, descriptor.element)) { + return Type{ kind = descriptor.kind, array_type = ArrayId(i) } } } try arraylist.append(&state.arrays, descriptor) - return Type{ kind = .array, array_type = ArrayId(state.arrays.items.len - 1) } + return Type{ kind = descriptor.kind, array_type = ArrayId(state.arrays.items.len - 1) } +} + +@hide:file +check_container_type proc(state @mut State, id ast.NodeId) Type ! (mem.AllocError | diagnostics.CapacityError) { + optional :: ast.expr_kind(state.tree, id) == .optional_type + node :: if (optional) ast.type_operand(state.tree, id) else ast.slice_type(state.tree, id).element + if (!optional) state.pointer_depth += 1 + child :: try check_annotation(state, node, false) + if (!optional) state.pointer_depth -= 1 + if (child.kind == .invalid) return child + if (child.kind == .void or child.kind == .anyopaque or child.kind == .range or has_inferred_count(state, child)) { + return invalid_type(try diagnostics.add(state.diagnostics, .invalid_array_element_type, ast.node_span(state.tree, node))) + } + kind TypeKind :: if (optional) .optional else .slice + descriptor := ArrayType{ kind = kind, element = child } + if (!optional) { + view :: ast.slice_type(state.tree, id) + descriptor.mutable = view.mutable + if (view.sentinel) |sentinel| { + try collect_expression(state, sentinel) + try demand(state, sentinel, child, .type_mismatch) + if (constant_integer(state, sentinel)) |value| { + if (!integer_kind(child.kind) or !integer_fits(value, child.kind)) { + return invalid_type(try diagnostics.add(state.diagnostics, .invalid_operand, ast.node_span(state.tree, sentinel))) + } + descriptor.sentinel = sentinel_bits(value) + } else return invalid_type(try diagnostics.add(state.diagnostics, .invalid_operand, ast.node_span(state.tree, sentinel))) + } + } + return try intern_array(state, descriptor) +} + +@hide:file +c_abi_type proc(state @State, type Type) bool { + if (type.kind == .optional) return state.arrays.items[usize(type.array_type)].element.kind == .pointer + return integer_kind(type.kind) or type.kind == .float or type.kind == .bool or type.kind == .pointer or type.kind == .void or type.kind == .invalid +} + +@hide:file +builtin_name proc(state @State, id ast.NodeId) []u8 { + return ast.source_text(state.tree, ast.node_span(state.tree, ast.builtin_call(state.tree, id).name)) +} + +@hide:file +collect_builtin proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) { + name :: builtin_name(state, id) + count :: ast.call_arg_count(state.tree, id) + layout :: mem.eql(name, "sizeof") or mem.eql(name, "alignof") + cast :: mem.eql(name, "ptrcast") or mem.eql(name, "constcast") + for (0..count) |i| { + argument :: ast.call_arg(state.tree, id, i) + if (layout or (cast and count == 2 and i == 0)) { + _ = try check_annotation(state, argument, true) + } else try collect_expression(state, argument) + try depend(state, argument, id) + } + if (layout) state.cells[usize(id)].type = Type{ kind = .usize } + if (mem.eql(name, "memcopy") or mem.eql(name, "memset")) state.cells[usize(id)].type = Type{ kind = .void } +} + +@hide:file +sequence_type proc(state @mut State, node ast.NodeId, defaults bool) ArrayType ! (mem.AllocError | diagnostics.CapacityError) { + type := try infer_shape(state, node, defaults) + if (type.kind == .pointer) { + pointer :: state.pointers.items[usize(type.pointer_type)] + if (pointer.many or pointer.element.kind != .array) return ArrayType{ element = Type{} } + type = pointer.element + descriptor := state.arrays.items[usize(type.array_type)] + descriptor.mutable = descriptor.mutable and pointer.mutable + return descriptor + } + if (type.kind == .array or type.kind == .slice) { + descriptor := state.arrays.items[usize(type.array_type)] + if (type.kind == .array) descriptor.mutable = descriptor.mutable and mutable_target(state, node, true) + return descriptor + } + if (type.kind == .string) return ArrayType{ element = Type{ kind = .u8 }, kind = .slice, sentinel = 0 } + return ArrayType{ element = Type{} } +} + +@hide:file +infer_slice proc(state @mut State, id ast.NodeId, defaults bool) Type ! (mem.AllocError | diagnostics.CapacityError) { + view :: ast.slice_expr(state.tree, id) + base :: try infer_shape(state, view.base, defaults) + descriptor := try sequence_type(state, view.base, defaults) + if (base.kind == .pointer and state.pointers.items[usize(base.pointer_type)].many) { + pointer :: state.pointers.items[usize(base.pointer_type)] + descriptor = ArrayType{ element = pointer.element, mutable = pointer.mutable } + if (view.end == null or pointer.element.kind == .anyopaque) { + try fail(state, id, .invalid_operand, ast.node_span(state.tree, id)) + seed(state, id, state.types[usize(id)]) + return group_type(state, id) + } + } + if (view.start) |node| try demand(state, node, Type{ kind = .usize }, .invalid_index_type) + if (view.end) |node| try demand(state, node, Type{ kind = .usize }, .invalid_index_type) + if (descriptor.element.kind == .none) { + if (defaults and base.kind != .none and base.kind != .invalid) try fail(state, id, .not_indexable, ast.node_span(state.tree, view.base)) + return group_type(state, id) + } + descriptor.kind = .slice + descriptor.len = 0 + descriptor.sentinel = null + if (view.sentinel) |node| { + try demand(state, node, descriptor.element, .invalid_operand) + if (constant_integer(state, node)) |value| { + if (!integer_kind(descriptor.element.kind) or !integer_fits(value, descriptor.element.kind)) { + try fail(state, id, .invalid_operand, ast.node_span(state.tree, node)) + } else { + descriptor.sentinel = sentinel_bits(value) + } + } else if (defaults) try fail(state, id, .invalid_operand, ast.node_span(state.tree, node)) + } + type :: try intern_array(state, descriptor) + seed(state, id, type) + return group_type(state, id) +} + +@hide:file +cast_pointer_type proc(state @State, type Type) Type { + if (type.kind == .optional) return state.arrays.items[usize(type.array_type)].element + return type +} + +@hide:file +infer_builtin proc(state @mut State, id ast.NodeId, defaults bool) Type ! (mem.AllocError | diagnostics.CapacityError) { + name :: builtin_name(state, id) + count :: ast.call_arg_count(state.tree, id) + span :: ast.node_span(state.tree, id) + layout :: mem.eql(name, "sizeof") or mem.eql(name, "alignof") + cast :: mem.eql(name, "ptrcast") or mem.eql(name, "constcast") + memory :: mem.eql(name, "memcopy") or mem.eql(name, "memset") + valid_count :: (layout and count == 1) or (cast and (count == 1 or count == 2)) or (memory and count == 2) + if (!valid_count) { + code diagnostics.Code :: if (layout or cast or memory) .argument_count_mismatch else .invalid_operand + if (defaults) try fail(state, id, code, span) + return group_type(state, id) + } + if (layout) return group_type(state, id) + if (cast) { + result := state.cells[root(state, usize(id))].expected + if (count == 2) result = state.types[usize(ast.call_arg(state.tree, id, 0))] + operand :: ast.call_arg(state.tree, id, count - 1) + source :: try infer_shape(state, operand, defaults) + if (result.kind == .none and mem.eql(name, "constcast")) { + result = source + if (source.kind == .slice) { + descriptor := state.arrays.items[usize(source.array_type)] + descriptor.mutable = true + result = try intern_array(state, descriptor) + } else if (source.kind == .pointer) { + descriptor := state.pointers.items[usize(source.pointer_type)] + descriptor.mutable = true + result = try intern_pointer(state, descriptor) + } + } + if (result.kind == .none) { + if (defaults) try fail(state, id, .cannot_infer_type, span) + return group_type(state, id) + } + a :: cast_pointer_type(state, source) + b :: cast_pointer_type(state, result) + allowed := false + if (a.kind == .pointer and b.kind == .pointer) { + left :: state.pointers.items[usize(a.pointer_type)] + right :: state.pointers.items[usize(b.pointer_type)] + if (mem.eql(name, "constcast")) allowed = left.many == right.many and same_type(state, left.element, right.element) + else allowed = left.mutable or !right.mutable + if (source.kind == .optional and result.kind != .optional) allowed = false + } else if (a.kind == .slice and b.kind == .slice and mem.eql(name, "constcast")) { + left :: state.arrays.items[usize(a.array_type)] + right :: state.arrays.items[usize(b.array_type)] + allowed = same_sentinel(left.sentinel, right.sentinel) and same_type(state, left.element, right.element) + } else if (source.kind == .null and result.kind == .optional and b.kind == .pointer) allowed = true + if (!allowed and source.kind != .none and source.kind != .invalid and defaults) { + try fail(state, id, .invalid_operand, span) + seed(state, id, state.types[usize(id)]) + return group_type(state, id) + } + seed(state, id, result) + return group_type(state, id) + } + destination :: try sequence_type(state, ast.call_arg(state.tree, id, 0), defaults) + if (mem.eql(name, "memset") and destination.element.kind != .none) { + try demand(state, ast.call_arg(state.tree, id, 1), destination.element, .type_mismatch) + } + if (defaults) { + if (!destination.mutable or destination.element.kind == .none) { + try fail(state, id, .invalid_operand, span) + } else if (mem.eql(name, "memcopy")) { + source :: try sequence_type(state, ast.call_arg(state.tree, id, 1), defaults) + if (!same_type(state, destination.element, source.element)) try fail(state, id, .type_mismatch, span) + } else { + argument :: ast.call_arg(state.tree, id, 1) + actual :: try infer_shape(state, argument, defaults) + if (actual.kind != .none and actual.kind != .invalid and !assignable_value(state, argument, actual, destination.element)) { + try fail(state, argument, .type_mismatch, ast.node_span(state.tree, argument)) + } + } + } + return group_type(state, id) } @hide:file @@ -2003,15 +2372,40 @@ same_type proc(state @State, left, right Type) bool { } return true } - if (left.kind != .array) return true + if (left.kind != .array and left.kind != .slice and left.kind != .optional) return true a :: state.arrays.items[usize(left.array_type)] b :: state.arrays.items[usize(right.array_type)] - return a.len == b.len and a.mutable == b.mutable and same_type(state, a.element, b.element) + return a.len == b.len and a.mutable == b.mutable and same_sentinel(a.sentinel, b.sentinel) and same_type(state, a.element, b.element) +} + +@hide:file +same_sentinel proc(left, right ?i64) bool { + if (left) |a| { + if (right) |b| return a == b + return false + } + return right == null +} + +@hide:file +sentinel_bits proc(value ConstantInteger) i64 { + if (!value.negative) return ptrcast!(@i64, &value.magnitude)^ + if (value.magnitude == u64(maxval!(i64)) + 1) return minval!(i64) + return -i64(value.magnitude) } @hide:file array_cells proc(state @State, type Type) ?usize { if (type.kind == .void) return usize(0) + if (type.kind == .slice) return usize(2) + if (type.kind == .optional) { + child :: state.arrays.items[usize(type.array_type)].element + if (child.kind == .pointer or child.kind == .function) return usize(1) + if (array_cells(state, child)) |count| { + if (count < maxval!(usize)) return count + 1 + } + return null + } if (type.kind == .record) { total := usize(0) descriptor :: state.records.items[usize(type.record_type)] @@ -2084,6 +2478,20 @@ infer_shape proc(state @mut State, id ast.NodeId, defaults bool) Type ! (mem.All if (state.cells[index].type.kind == .invalid) return state.cells[index].type state.cells[index].visiting = true defer state.cells[index].visiting = false + if (state.cells[usize(id)].role == .expression) { + kind :: ast.expr_kind(state.tree, id) + if (kind == .builtin_call) return try infer_builtin(state, id, defaults) + if (kind == .slice) return try infer_slice(state, id, defaults) + if (kind == .unary and ast.unary_expr(state.tree, id).operator == .unwrap) { + operand :: try infer_shape(state, ast.unary_expr(state.tree, id).operand, defaults) + if (operand.kind == .optional) { + child :: state.arrays.items[usize(operand.array_type)].element + seed(state, id, child) + return child + } + return group_type(state, id) + } + } if (state.cells[usize(id)].role == .expression and ast.expr_kind(state.tree, id) == .binary) { view :: ast.binary_expr(state.tree, id) if (view.operator == .add) { @@ -2113,11 +2521,23 @@ infer_shape proc(state @mut State, id ast.NodeId, defaults bool) Type ! (mem.All return group_type(state, id) } element :: state.pointers.items[usize(operand.pointer_type)].element + if (element.kind == .anyopaque) { + try fail(state, id, .invalid_dereference, view.span) + seed(state, id, state.types[usize(id)]) + return group_type(state, id) + } seed(state, id, element) return element } mutable :: mutable_target(state, view.operand, true) expected :: state.cells[index].expected + if (expected.kind == .slice and operand.kind == .array) { + array :: state.arrays.items[usize(operand.array_type)] + writable :: array.mutable and (mutable or ast.expr_kind(state.tree, view.operand) == .array_literal) + type :: try intern_array(state, ArrayType{ kind = .slice, element = array.element, mutable = writable }) + seed(state, id, type) + return type + } many :: expected.kind == .pointer and state.pointers.items[usize(expected.pointer_type)].many type :: try intern_pointer(state, PointerType{ element = operand, mutable = mutable, many = many }) seed(state, id, type) @@ -2133,6 +2553,13 @@ infer_shape proc(state @mut State, id ast.NodeId, defaults bool) Type ! (mem.All seed(state, id, Type{ kind = destination }) return group_type(state, id) } + if (state.backings[usize(id)].kind == .none) { + signature :: try infer_shape(state, ast.call_expr(state.tree, id).callee, defaults) + if (signature.kind == .function) { + seed(state, id, state.types[usize(ast.callable_result(state.tree, signature.function))]) + return group_type(state, id) + } + } } if (state.cells[usize(id)].role == .expression and ast.expr_kind(state.tree, id) == .selection @@ -2143,6 +2570,23 @@ infer_shape proc(state @mut State, id ast.NodeId, defaults bool) Type ! (mem.All if (enum_shape.kind == .enumeration) return try select_enum_member(state, id, view.member, enum_shape) } base := try infer_shape(state, view.base, defaults) + if ((base.kind == .array or base.kind == .slice or base.kind == .string) and base.nominal == ast.NodeId(maxval!(u32))) { + member :: ast.source_text(state.tree, ast.node_span(state.tree, view.member)) + if (mem.eql(member, "len")) { + seed(state, id, Type{ kind = .usize }) + return group_type(state, id) + } + if (mem.eql(member, "ptr")) { + element :: if (base.kind == .string) Type{ kind = .u8 } else state.arrays.items[usize(base.array_type)].element + mutable :: ( + base.kind != .string and state.arrays.items[usize(base.array_type)].mutable + and (base.kind == .slice or mutable_target(state, view.base, true)) + ) + type :: try intern_pointer(state, PointerType{ element = element, mutable = mutable, many = true }) + seed(state, id, type) + return type + } + } if (base.kind == .pointer and base.nominal == ast.NodeId(maxval!(u32))) { pointer :: state.pointers.items[usize(base.pointer_type)] if (!pointer.many and pointer.element.kind == .array @@ -2260,7 +2704,11 @@ infer_shape proc(state @mut State, id ast.NodeId, defaults bool) Type ! (mem.All } base = pointer.element } - if (base.kind == .array and base.nominal == ast.NodeId(maxval!(u32))) { + if (base.kind == .string) { + seed(state, id, Type{ kind = .u8 }) + return group_type(state, id) + } + if ((base.kind == .array or base.kind == .slice) and base.nominal == ast.NodeId(maxval!(u32))) { element :: state.arrays.items[usize(base.array_type)].element seed(state, id, element) return element @@ -2277,8 +2725,27 @@ infer_shape proc(state @mut State, id ast.NodeId, defaults bool) Type ! (mem.All return group_type(state, id) } +@hide:file +prepare_optional_captures proc(state @mut State, defaults bool) void ! (mem.AllocError | diagnostics.CapacityError) { + for (0..(state.cells.len)) |i| { + capture :: state.cells[i].capture + if (capture == ast.NodeId(maxval!(u32)) or state.cells[i].role != .expression) continue + if (state.cells[usize(capture)].role != .loop_capture) continue + type :: try infer_shape(state, ast.NodeId(i), defaults) + if (type.kind != .optional) continue + child :: state.arrays.items[usize(type.array_type)].element + seed(state, capture, child) + for (0..(state.cells.len)) |j| { + if (state.cells[j].role != .expression or !declaration_reference(state.tree, ast.NodeId(j))) continue + binding :: sema.binding(state.resolution, ast.NodeId(j)) + if (binding.kind == .declaration and binding.data.declaration == capture) seed(state, ast.NodeId(j), child) + } + } +} + @hide:file prepare_shapes proc(state @mut State, assignment_values, defaults bool) void ! (mem.AllocError | diagnostics.CapacityError) { + try prepare_optional_captures(state, defaults) for 0..(state.cells.len) |i| { if (state.cells[i].role != .expression or state.cells[i].assignment_value != assignment_values) continue id :: ast.NodeId(i) @@ -2365,7 +2832,8 @@ apply_contexts proc(state @mut State, assignment_values, early bool) void ! (mem @hide:file context_demand proc(state @mut State, id ast.NodeId, expected Type, code diagnostics.Code, early bool) void ! (mem.AllocError | diagnostics.CapacityError) { if (early and expected.kind != .array and expected.kind != .record - and expected.kind != .pointer and expected.kind != .enumeration and !integer_kind(expected.kind)) return + and expected.kind != .pointer and expected.kind != .slice and expected.kind != .optional + and expected.kind != .enumeration and !integer_kind(expected.kind)) return try demand(state, id, expected, code) } @@ -2748,7 +3216,7 @@ type_syntax proc(kind ast.ExprKind) bool { return ( kind == .record_type or kind == .union_type or kind == .enum_type or kind == .distinct_type or kind == .alias_expr - or kind == .pointer_type or kind == .function_type + or kind == .pointer_type or kind == .function_type or kind == .slice_type or kind == .optional_type ) } @@ -2779,6 +3247,7 @@ check_type_expression proc(state @mut State, id ast.NodeId, nominal bool) Type ! if (kind == .record_type) return try check_record(state, id, nominal) if (kind == .pointer_type) return try check_pointer_type(state, id) if (kind == .function_type) return try check_function_type(state, id) + if (kind == .slice_type or kind == .optional_type) return try check_container_type(state, id) if (kind == .union_type) { type :: try check_union(state, id, nominal) state.cells[usize(id)].type = type diff --git a/source/sema/resolve.hon b/source/sema/resolve.hon index f752b38..d6fefd2 100644 --- a/source/sema/resolve.hon +++ b/source/sema/resolve.hon @@ -14,6 +14,9 @@ BuiltinType :: enum { f64, usize, i8, i16, i32, isize, u8, u16, u32, u64, uint, + anyopaque + c_int, c_uint, c_long, c_ulong, c_longlong, c_ulonglong + c_char, c_uchar, c_short, c_ushort } @hide:file @@ -41,6 +44,17 @@ builtins [_]Builtin :: [ { name = "u32", type = .u32 }, { name = "u64", type = .u64 }, { name = "uint", type = .uint }, + { name = "anyopaque", type = .anyopaque }, + { name = "c_int", type = .c_int }, + { name = "c_uint", type = .c_uint }, + { name = "c_long", type = .c_long }, + { name = "c_ulong", type = .c_ulong }, + { name = "c_longlong", type = .c_longlong }, + { name = "c_ulonglong", type = .c_ulonglong }, + { name = "c_char", type = .c_char }, + { name = "c_uchar", type = .c_uchar }, + { name = "c_short", type = .c_short }, + { name = "c_ushort", type = .c_ushort }, ] @hide:file @@ -371,7 +385,7 @@ resolve_block proc(state @mut State, scope @mut Scope, id ast.NodeId) void ! (me .if_stmt: { conditional :: ast.if_stmt(tree, statement) try resolve_expr(state, scope, conditional.condition) - try resolve_arm(state, scope, conditional.then_block) + try resolve_capture_arm(state, scope, conditional.then_block, conditional.capture) if (conditional.else_block) |other| try resolve_arm(state, scope, other) } .assignment_stmt: { @@ -382,7 +396,7 @@ resolve_block proc(state @mut State, scope @mut Scope, id ast.NodeId) void ! (me .while_stmt: { loop :: ast.while_stmt(tree, statement) try resolve_expr(state, scope, loop.condition) - try resolve_arm(state, scope, loop.body) + try resolve_capture_arm(state, scope, loop.body, loop.capture) } .for_stmt: { loop :: ast.for_stmt(tree, statement) @@ -405,6 +419,14 @@ resolve_block proc(state @mut State, scope @mut Scope, id ast.NodeId) void ! (me } } +@hide:file +resolve_capture_arm proc(state @mut State, scope @Scope, block ast.NodeId, capture ?ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) { + child := Scope{ names = hashmap.init(state.allocator), parent = scope } + defer hashmap.deinit(&child.names) + if (capture) |id| try predeclare(state, &child, id, ast.node_span(state.tree, id)) + try resolve_block(state, &child, block) +} + @hide:file resolve_statement_decl proc( state @mut State, @@ -487,9 +509,9 @@ resolve_expr proc( if (state.bindings[usize(id)].kind != .none) return tree :: state.tree match ast.expr_kind(tree, id) { - .literal_int, .literal_float, .literal_bool, .literal_string: {} + .literal_int, .literal_float, .literal_bool, .literal_string, .literal_null: {} .enum_literal: {} - .distinct_type, .alias_expr: try resolve_expr(state, scope, ast.type_operand(tree, id)) + .distinct_type, .alias_expr, .optional_type: try resolve_expr(state, scope, ast.type_operand(tree, id)) .enum_type: { if (ast.enum_type(tree, id).backing) |backing| try resolve_expr(state, scope, backing) for (0..ast.enum_member_count(tree, id)) |i| { @@ -571,6 +593,21 @@ resolve_expr proc( try resolve_expr(state, scope, ast.call_arg(tree, id, i)) } } + .builtin_call: { + for (0..ast.call_arg_count(tree, id)) |i| try resolve_expr(state, scope, ast.call_arg(tree, id, i)) + } + .slice: { + expression :: ast.slice_expr(tree, id) + try resolve_expr(state, scope, expression.base) + if (expression.start) |node| try resolve_expr(state, scope, node) + if (expression.end) |node| try resolve_expr(state, scope, node) + if (expression.sentinel) |node| try resolve_expr(state, scope, node) + } + .slice_type: { + type :: ast.slice_type(tree, id) + try resolve_expr(state, scope, type.element) + if (type.sentinel) |node| try resolve_expr(state, scope, node) + } .pointer_type: try resolve_expr(state, scope, ast.pointer_type(tree, id).element) .function_type: { try resolve_expr(state, scope, ast.callable_result(tree, id)) diff --git a/tests/native.py b/tests/native.py index f533372..dcac285 100644 --- a/tests/native.py +++ b/tests/native.py @@ -11,6 +11,279 @@ HONEY = ROOT / "build/honey" # Explicit expectations guard against frontend bugs shared by both execution paths. CASES = { + "slice_mutation_alias_calls": ( + """ + update proc(values []mut i16) []mut i16 { + values[1] = values[0] + 2 + return values[1..] + } + main proc() i16 { + values [4]mut i16 := [0, 40, 0, 0] + view :: update(values[1..3]) + copy :: view + copy[0] = copy[0] + 1 + return values[2] - i16(view.len) + } + """, b"42\n", None, + ), + "slice_empty_one_past_and_pointer_identity": ( + """ + main proc() bool { + values [2]mut i16 := [40, 2] + all :: values[..] + empty :: all[2..2] + return empty.len == 0 and empty.ptr == all.ptr + 2 + } + """, b"true\n", None, + ), + "slice_nested_record_storage": ( + """ + Row :: struct { values []mut i16 } + identity proc(row Row) Row { return row } + main proc() i16 { + values [2]mut i16 := [40, 2] + rows [1]Row :: [{ values = values[..] }] + selected :: identity(rows[0]) + selected.values[0] = selected.values[0] + selected.values[1] + return values[0] + } + """, b"42\n", None, + ), + "slice_empty_mutable_literal": ( + "main proc() usize { values []mut i16 :: &[]\nreturn values.len }", + b"0\n", None, + ), + "slice_string_sentinel": ( + """ + read proc(text [;0]u8) u8 { + suffix :: text[1..;0] + return suffix.ptr[suffix.len] + suffix[0] + } + main proc() u8 { return read("!*") } + """, b"42\n", None, + ), + "slice_checked_sentinel": ( + """ + main proc() u8 { + values [3]mut u8 := [40, 2, 0] + pointer *mut u8 :: &values[0] + text [;0]mut u8 :: pointer[..2;0] + text[0] = text[0] + text[1] + plain []u8 :: text + return plain[0] + } + """, b"42\n", None, + ), + "slice_reversed_bounds": ( + "main proc() usize { a :: [1, 2]\nreturn a[2..1].len }", + b"", b"slice", + ), + "slice_excessive_bounds": ( + "main proc() usize { a :: [1, 2]\nreturn a[..3].len }", + b"", b"slice", + ), + "slice_index_at_length": ( + "main proc() i64 { a :: [40, 2]\ns :: a[..]\nreturn s[s.len] }", + b"", b"slice", + ), + "slice_sentinel_mismatch": ( + "main proc() usize { a :: [1, 2, 3]\nreturn a[..2;0].len }", + b"", b"sentinel", + ), + "slice_readonly_write_rejected": ( + "main proc() int { a :: [40, 2]\ns :: a[..]\ns[0] = 42\nreturn 42 }", + b"", b"writable", + ), + "optional_values_storage_calls": ( + """ + Box :: struct { value ?i16 } + choose proc(value ?i16) ?i16 { return value } + main proc() i16 { + box Box := { value = 40 } + options [2]mut ?i16 := [null, 2] + options[0] = choose(box.value) + return options[0]? + options[1]? + } + """, b"42\n", None, + ), + "optional_capture_and_null_transition": ( + """ + main proc() i16 { + value ?i16 := 40 + total i16 := 0 + while value |payload| { + total = total + payload + value = null + continue + } + if value |unused| return unused + if (value != null) return 0 + value = 2 + if value |payload| return total + payload + return 0 + } + """, b"42\n", None, + ), + "optional_pointer_identity_and_mutation": ( + """ + main proc() i16 { + value i16 := 40 + pointer ?@mut i16 := &value + if pointer |present| present^ = present^ + 2 + readonly ?@i16 :: pointer + if (readonly == null) return 0 + return readonly?^ + } + """, b"42\n", None, + ), + "optional_null_unwrap": ( + "main proc() i16 { value ?i16 :: null\nreturn value? }", + b"", b"null", + ), + "optional_untaken_unwrap": ( + "main proc() i16 { value ?i16 :: null\nif (false) return value?\nreturn 42 }", + b"42\n", None, + ), + "memory_layout_padded_aggregates": ( + """ + Padded :: struct { tag u8, value u64 } + Empty :: struct {} + main proc() bool { + return ( + sizeof!(Padded) == 16 and alignof!(Padded) == 8 + and sizeof!([3]Padded) == 48 and sizeof!([]u8) == 16 + and sizeof!(?@i16) == 8 and sizeof!(Empty) == 0 + ) + } + """, b"true\n", None, + ), + "memory_pointer_cast_byte_alias": ( + """ + main proc() u16 { + value u16 := 40 + bytes *mut u8 :: ptrcast!(&value) + bytes[0] = bytes[0] + 2 + whole @mut u16 :: ptrcast!(bytes) + return whole^ + } + """, b"42\n", None, + ), + "memory_constcast_slice": ( + """ + main proc() i16 { + values [2]mut i16 := [40, 2] + readonly []i16 :: values[..] + writable []mut i16 :: constcast!(readonly) + writable[0] = writable[0] + writable[1] + return values[0] + } + """, b"42\n", None, + ), + "memory_typed_copy_and_set": ( + """ + Pair :: struct { number i16, ready bool } + main proc() i16 { + source [2]Pair :: [{ number = 40, ready = true }, { number = 2, ready = false }] + target [2]mut Pair := [{ number = 0, ready = false }, { number = 0, ready = false }] + memcopy!(target[..], source[..]) + saved :: target[0] + memset!(target[..], { number = 21, ready = true }) + if (!saved.ready or !target[1].ready) return 0 + return target[0].number + target[1].number + } + """, b"42\n", None, + ), + "memory_copy_overlap": ( + "main proc() int { a [3]mut i16 := [1, 2, 3]\nmemcopy!(a[1..], a[..2])\nreturn 42 }", + b"", b"overlap", + ), + "memory_copy_length_mismatch": ( + "main proc() int { a [3]mut i16 := [1, 2, 3]\nmemcopy!(a[..1], a[1..])\nreturn 42 }", + b"", b"slice", + ), + "memory_empty_copy_and_set": ( + "main proc() int { a [0]mut i16 := []\nmemcopy!(a[..], a[..])\nmemset!(a[..], 42)\nreturn 42 }", + b"42\n", None, + ), + "optional_nested_presence": ( + "main proc() i16 { value ??i16 :: 42\nreturn value?? }", b"42\n", None, + ), + "optional_callback_capture_from_call": ( + """ + increment proc(value i16) i16 { return value + 2 } + factory proc() ?@proc(value i16) i16 { return increment } + main proc() i16 { + if factory() |callback| return callback(40) + return 0 + } + """, b"42\n", None, + ), + "slice_recursive_record": ( + """ + Node :: struct { value i16, children []Node } + main proc() i16 { + children [0]Node :: [] + node Node :: { value = 42, children = children[..] } + return node.value + i16(node.children.len) + } + """, b"42\n", None, + ), + "slice_full_width_sentinel": ( + """ + main proc() usize { + values [2]u64 :: [42, 18446744073709551615] + sentinel [;18446744073709551615]u64 :: values[..1;18446744073709551615] + return sentinel.len + } + """, b"1\n", None, + ), + "memory_pointer_cast_preserves_readonly": ( + "main proc() int { value i16 :: 40\npointer *mut u8 :: ptrcast!(&value)\nreturn 42 }", + b"", b"operator requires", + ), + "slice_sentinel_strengthening_rejected": ( + "main proc() int { a :: [1, 0]\ns []i64 :: a[..1]\nt [;0]i64 :: s\nreturn 42 }", + b"", b"incompatible", + ), + "slice_fixed_signed_index_rejected": ( + "main proc() i64 { a :: [42]\ns :: a[..]\ni i16 :: 0\nreturn s[i] }", + b"", b"index", + ), + "optional_nonoptional_capture_rejected": ( + "main proc() int { if true |payload| return 1\nreturn 42 }", + b"", b"condition", + ), + "memory_foreign_main_rejected": ( + "main c_func() i16", b"", b"main must", + ), + "memory_opaque_dereference_rejected": ( + "main proc() int { value i16 := 42\npointer *mut anyopaque :: ptrcast!(&value)\ndiscard :: pointer^\nreturn 42 }", + b"", b"dereference", + ), + "memory_opaque_index_rejected": ( + "main proc() int { value i16 := 42\npointer *mut anyopaque :: ptrcast!(&value)\ndiscard :: pointer[0]\nreturn 42 }", + b"", b"indexing", + ), + "memory_opaque_slice_rejected": ( + "main proc() int { value i16 := 42\npointer *mut anyopaque :: ptrcast!(&value)\ndiscard :: pointer[..1]\nreturn 42 }", + b"", b"operator requires", + ), + "memory_opaque_arithmetic_rejected": ( + "main proc() int { value i16 := 42\npointer *mut anyopaque :: ptrcast!(&value)\ndiscard :: pointer + 1\nreturn 42 }", + b"", b"operator requires", + ), + "slice_contextual_borrow_preserves_readonly": ( + "main proc() i16 { values [1]i16 :: [40]\nview []mut i16 :: &values\nview[0] = 42\nreturn values[0] }", + b"", b"incompatible", + ), + "slice_contextual_scalar_borrow_rejected": ( + "main proc() i16 { value i16 := 42\nview []mut i16 :: &value\nreturn view[0] }", + b"", b"incompatible", + ), + "slice_implicit_borrow_preserves_binding_mutability": ( + "main proc() i16 { values [1]mut i16 :: [40]\nview []mut i16 :: values\nview[0] = 42\nreturn values[0] }", + b"", b"incompatible", + ), "callback_concrete_vtable": ( """ VTable :: struct { run @proc(context @mut State, amount i16) i16 } @@ -1657,6 +1930,66 @@ def check_dependency_provenance(temporary, executable): executable.unlink() return len(cases) * 2 +def check_allocator_libc(temporary, executable): + source = temporary / "allocator_libc.hon" + source.write_text(textwrap.dedent(""" + malloc c_func(size c_ulong) ?*mut anyopaque + realloc c_func(memory ?*mut anyopaque, size c_ulong) ?*mut anyopaque + free c_func(memory ?*mut anyopaque) void + posix_memalign c_func(output *mut ?*mut anyopaque, alignment c_ulong, size c_ulong) c_int + VTable :: struct { + alloc @proc(context ?@mut anyopaque, size usize, alignment usize) ?*mut u8 + release @proc(context ?@mut anyopaque, memory ?*mut u8) void + } + Allocator :: struct { context ?@mut anyopaque, vtable @VTable } + allocate proc(_ ?@mut anyopaque, size usize, alignment usize) ?*mut u8 { + if (alignment <= 16) return ptrcast!(malloc(c_ulong(size))) + output [1]mut ?*mut anyopaque := [null] + status :: posix_memalign((&output).ptr, c_ulong(alignment), c_ulong(size)) + if (status != 0) return null + return ptrcast!(output[0]) + } + release proc(_ ?@mut anyopaque, memory ?*mut u8) void { free(memory) } + table VTable :: { alloc = allocate, release = release } + main proc() i16 { + allocator Allocator :: { context = null, vtable = &table } + memory :: allocator.vtable.alloc(allocator.context, 4, 8) + if memory |bytes| { + values *mut i16 :: ptrcast!(bytes) + values[0] = 40 + values[1] = 2 + resized ?*mut i16 :: ptrcast!(realloc(bytes, 8)) + if resized |items| { + aligned :: allocator.vtable.alloc(null, 8, 64) + if aligned |other| { + target *mut i16 :: ptrcast!(other) + memcopy!(target[..2], items[..2]) + answer :: target[0] + target[1] + allocator.vtable.release(null, other) + free(items) + return answer + } + free(items) + return -1 + } + free(bytes) + return -2 + } + return -3 + } + """).strip() + "\n") + interpreted = run(str(HONEY), "run", str(source)) + assert interpreted.returncode == 1 and interpreted.stdout == b"", outcome(interpreted) + assert b"foreign function calls are unsupported by the interpreter" in interpreted.stderr, outcome(interpreted) + assert len(interpreted.stderr.splitlines()) == 1, outcome(interpreted) + for optimization in ("-O0", "-O2"): + built = run(str(HONEY), "build", str(source), "-o", str(executable), optimization) + assert outcome(built) == (0, b"", b""), outcome(built) + assert outcome(run(str(executable))) == (0, b"42\n", b""), optimization + executable.unlink() + return 1 + + def main(): with tempfile.TemporaryDirectory(prefix="honey native ' ; $") as directory: @@ -1671,6 +2004,7 @@ def main(): source.write_text(textwrap.dedent(text).strip() + "\n") check(source, executable, (stdout, error)) count += 1 + allocator_count = check_allocator_libc(temporary, executable) count += check_packages(temporary, executable) count += check_namespace_packages(temporary, executable) count += check_record_packages(temporary, executable) @@ -1694,7 +2028,7 @@ def main(): assert not executable.exists() invalid = run(str(HONEY), "build", str(source), "-o", str(executable), "-O3") assert invalid.returncode == 2 and b"usage:" in invalid.stderr, outcome(invalid) - print(f"{count} programs matched interpreter at -O0 and -O2; LLVM output and CLI errors passed") + print(f"{count} programs matched interpreter at -O0 and -O2; {allocator_count} native allocator program, LLVM output and CLI errors passed") if __name__ == "__main__":