From 676f3666b4ab85da519c275b830d3b464628a16c Mon Sep 17 00:00:00 2001 From: hl-valdemar Date: Mon, 26 Oct 2026 10:37:08 +0100 Subject: [PATCH] feat: support source pointers and references --- README.md | 8 +- TODO.md | 5 +- source/ast/ast.hon | 48 +++- source/diagnostics/diagnostics.hon | 10 + source/interpreter/interpreter.hon | 171 +++++++++++++- source/ir/ir.hon | 11 +- source/lexer/scan.hon | 2 + source/lexer/token.hon | 3 +- source/llvm/instructions.hon | 21 ++ source/llvm/llvm.hon | 3 +- source/llvm/runtime.hon | 1 + source/lower/lower.hon | 104 ++++++++- source/main.hon | 1 + source/parser/parse.hon | 61 ++++- source/sema/check/check.hon | 347 +++++++++++++++++++++++++---- source/sema/resolve.hon | 1 + tests/native.py | 262 +++++++++++++++++++++- 17 files changed, 981 insertions(+), 78 deletions(-) diff --git a/README.md b/README.md index 0fa1a58..67f46a1 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,10 @@ array lengths currently support nonnegative integer literals, unary signs, `+`, indices require `usize`; integer literals and flexible inferred values receive this context, while fixed signed integers do not convert implicitly. out-of-bounds access traps before loading or storing, including for empty arrays. reads evaluate the base before the index; indexed assignment evaluates its right-hand side before the destination, each once. leading nested assignment targets follow Bro's declaration ambiguity: write `(matrix[1])[0] = 9`, not `matrix[1][0] = 9`. [programs/array_sum.hon](programs/array_sum.hon) uses a `usize` range bound to index, mutate and sum an array; `./build/honey run programs/array_sum.hon` prints `17`. +data pointers use `@T` for one value and `*T` for many values, with `mut` after the pointer marker for writable pointees. `&value` borrows addressable initialized storage; postfix `^` reads or writes the pointee. pointer mutability is independent of binding mutability: an immutable pointer binding or parameter can mutate its `mut` pointee. mutable pointers weaken to readonly pointers with the same child type. address-taking also accepts an expected many-pointer type, such as `pointer *mut i16 :: &values[0]`; arbitrary temporaries are not addressable. + +pointer-to-record field selection dereferences implicitly. pointers to arrays support bounded indexing, `.len`, and `.ptr`; `.ptr` returns a many-pointer and retains both pointer and array-element mutability restrictions. many-pointer indices require `usize` and caller-provided valid storage. pointers preserve alias identity through calls, returns, globals and aggregate copies; dereferenced aggregates still copy by value. `==` and `!=` compare locations. recursive records may contain pointers to themselves without recursive by-value layout. CLI results render pointer leaves as ``. pointee lifetimes remain caller-managed; there is no borrow checker. function-pointer signatures, general pointer arithmetic, slices, optionals, pointer casts and allocator libc calls remain unimplemented. + 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. tuple types use `struct { i64, bool }`; tuple values use `{42, true}`, `{42,}` and `{}`. anonymous keyed records, such as `{ x = 40, ready = true }`, infer structural types. fields use `.x` or canonical tuple ordinals such as `.0`, including chained selections. mutable local roots permit field updates; constants, parameters and globals remain readonly, and traversed arrays still require mutable elements. construction, binding, assignment, arguments and returns copy nested records/tuples and arrays by value. fields can be read from returned temporaries; records containing no scalar leaves retain their logical shapes. CLI results print declaration-order values as `{value, value}`. @@ -125,7 +129,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, array/record/enum descriptors and construction mappings; query with `check.type_of`, `check.array_type`, `check.record_type`, `check.enum_type`, `check.field_index` and `check.initializer_field`, and release with `check.deinit`. declarations infer their initializer types, including forward references and function aliases; function types identify the declaration supplying the callable signature. annotations support builtin types, fixed arrays and local/imported records, enums, unions and distinct/alias types; type names are not ordinary runtime values. `void` is valid as a function result or tagged-union payload, not as a parameter or standalone stored value. +type checking (`@source/sema/check`) runs after name resolution. `check.check` borrows the AST and binding table and returns owned per-node types, pointer/array/record/enum descriptors and construction mappings; query with `check.type_of`, `check.pointer_type`, `check.array_type`, `check.record_type`, `check.enum_type`, `check.field_index` and `check.initializer_field`, and release with `check.deinit`. declarations infer their initializer types, including forward references and function aliases; function types identify the declaration supplying the callable signature. annotations support builtin types, data pointers, fixed arrays and local/imported records, enums, unions and distinct/alias types; type names are not ordinary runtime values. `void` is valid as a function result or tagged-union payload, not as a parameter or standalone stored value. 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. @@ -143,7 +147,7 @@ type errors carry source spans and diagnostic identities; invalid operands propa `lower.lower` turns the checked AST and binding table into an owned `@source/ir` module. functions, globals, local slots and instruction-result temporaries have explicit IDs; typed instructions execute in source evaluation order and basic blocks end in jump, conditional-branch, return or diagnostic-trap terminators. conditional arms join only when execution can continue; loops use condition/body/exit blocks and backedges. short-circuit expressions use branches and synthetic boolean local slots to merge results; call arguments are packed in the block reached after evaluating them. local slots and temporaries are function-wide, without SSA or phi nodes. literals are parsed and strings decoded during lowering; the module retains no AST, binding-table or source-buffer references. release it with `ir.deinit`; keep and reuse the diagnostic store used during lowering so diagnostic IDs remain valid. the IR is target-independent and is intended to feed later native backends as well as the current interpreter. -aggregate IR retains recursive array element types/counts/mutability and declaration-order record fields with flattened offsets. union descriptors additionally retain overlapping byte layouts and per-variant discriminants. typed construction, field/element addresses and load/store instructions use storage reserved per invocation; nested copies and accesses reuse that storage without per-iteration allocation. addresses are internal IR values, not source-language pointers. match lowering uses ordinary branches and local result slots; enum and distinct identities are checked before erasure to their runtime backing. +aggregate IR retains recursive array element types/counts/mutability and declaration-order record fields with flattened offsets. union descriptors additionally retain overlapping byte layouts and per-variant discriminants. typed construction, field/element addresses and load/store instructions use storage reserved per invocation; nested copies and accesses reuse that storage without per-iteration allocation. internal `address` values remain distinct from source `pointer` values: explicit bridge instructions preserve initialization checks, while pointer offsets retain typed element strides. native pointers occupy eight bytes; interpreter handles intern root storage identity and byte offset across frames, including union serialization. match lowering uses ordinary branches and local result slots; enum and distinct identities are checked before erasure to their runtime backing. IR construction owns derived layout metadata. `ir.add_array`, `ir.add_record` and `ir.add_union` check shape capacity and compute their interpreter storage and native layouts; record/union shapes own copied field metadata. all aggregate kinds use `ArrayId` shapes and the existing backing segments. `ir.add_parameter` and `ir.add_local` create slots with disjoint aggregate backing. parameters precede other locals. `ir.emit` assigns fresh result IDs by default and maintains temporary counts, aggregate-result segments and address presence. hand-authored non-SSA IR may supply explicit result IDs; repeated aggregate results retain their shape and reuse the same segment. producers do not register backing separately or write derived counts, offsets or flags. construction failures leave logical contents unchanged, and no finalization pass or extra runtime allocation is required. diff --git a/TODO.md b/TODO.md index 2cddd33..bee74b1 100644 --- a/TODO.md +++ b/TODO.md @@ -9,7 +9,8 @@ selection through the interpreter and native backend. concrete 8/16/32/64-bit integers, checked numeric conversions, records/tuples, construction/defaults, enums, distinct/alias types, overlapping untagged/tagged unions, field reads/writes and exhaustive statement/value matching work through both execution paths. -the driver retains frontend storage +source data pointers/references now support aliasing, mutation, calls and aggregate +storage through both execution paths. the driver retains frontend storage and source provenance through `@source/compilation`, with compilation-wide AST node identities. file commands retain single-file behavior; directory commands compile the package's production files and load its dependencies in separate scopes. @@ -40,6 +41,8 @@ test discovery and FFI remain outside these completed slices. - [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] source data pointers/references: `@T`/`*T`, `mut`, address-taking and dereference, pointer-aware fields/indexing, array `.len`/`.ptr`, readonly weakening, location equality, recursive pointer-linked records and shared pointees across interpreter/native calls and aggregate copies. + - [ ] function-pointer signatures and general pointer arithmetic; mutable/sentinel slices, optionals, layout/cast builtins and allocator-required libc calls. - [ ] 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 0d718b1..c1c6eca 100644 --- a/source/ast/ast.hon +++ b/source/ast/ast.hon @@ -1,9 +1,9 @@ #! grammar: -#! * type ..... -> identifier {'.' member} | '[' (expr | '_') ']' ['mut'] type | 'struct' '{' fields '}' | 'enum' ['(' type ')'] '{' members '}' | 'union' ['(' (type | 'enum') ')'] '{' fields '}' | ('distinct' | 'alias') type +#! * type ..... -> identifier {'.' member} | ('@' | '*') ['mut'] type | '[' (expr | '_') ']' ['mut'] type | 'struct' '{' fields '}' | 'enum' ['(' type ')'] '{' members '}' | 'union' ['(' (type | 'enum') ')'] '{' fields '}' | ('distinct' | 'alias') type #! * binding ... -> :: | := #! * literal .. -> int | float | string | 'true' | 'false' #! * op ....... -> '+' | '-' | '*' | '/' | '==' | '!=' | '<' | '<=' | '>' | '>=' | 'and' | 'or' -#! * expr ..... -> identifier | literal | type | '.' member | '[' [expr {',' expr} [',']] ']' | [type] '{' [initializer {',' initializer} [',']] '}' | '(' expr ')' | ('-' | '!') expr | expr op expr | expr '(' [expr {',' expr} [',']] ')' | expr '[' expr ']' | expr '.' (member | int) | expr ('..' | '..=') expr | 'match' expr '{' match_arms '}' +#! * 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 #! * block .... -> '{' {stmt} '}' #! * arm ...... -> block | stmt (single statements become canonical blocks) @@ -11,7 +11,7 @@ #! 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, unary -/!, multiplication/division, addition/subtraction, relational comparisons, equality, and, or, ranges. compound range endpoints require their own parentheses. +#! 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. #! binary operators associate left; unary operators associate right. import "@std/debug" @@ -44,6 +44,7 @@ ExprKind :: enum { call range array_type + pointer_type array_literal record_type record_literal @@ -82,7 +83,12 @@ Mutability :: enum { constant, variable } -UnaryOperator :: enum { negate, logical_not } +UnaryOperator :: enum { + negate + logical_not + address_of + dereference +} BinaryOperator :: enum { add, subtract @@ -105,6 +111,7 @@ NodeKind :: enum { expr_call expr_range expr_array_type + expr_pointer_type expr_array_literal expr_record_type expr_record_literal @@ -231,6 +238,12 @@ ArrayTypeView :: struct { mutable bool } +PointerTypeView :: struct { + element NodeId + mutable bool + many bool +} + RecordLiteralView :: struct { span Span type ?NodeId @@ -482,6 +495,22 @@ add_array_type proc( }, span) } +add_pointer_type proc( + builder @mut Builder, + span Span, + element NodeId, + mutable, many bool, +) NodeId ! mem.AllocError { + mutable_flag :: if (mutable) u32(1) else u32(0) + many_flag :: if (many) u32(1) else u32(0) + return try add_node(builder, Node{ + kind = .expr_pointer_type, + main = NodeData{ extra = mutable_flag }, + data0 = NodeData{ extra = many_flag }, + data1 = NodeData{ node_id = element }, + }, span) +} + add_array_literal proc(builder @mut Builder, span Span, elements []NodeId) NodeId ! mem.AllocError { start :: builder.storage.extra.items.len for (elements) |element| _ = try add_extra(builder, u32(element)) @@ -1073,6 +1102,7 @@ expression_kind proc(kind NodeKind) ExprKind { .expr_call: .call .expr_range: .range .expr_array_type: .array_type + .expr_pointer_type: .pointer_type .expr_array_literal: .array_literal .expr_record_type: .record_type .expr_record_literal: .record_literal @@ -1154,6 +1184,16 @@ array_type proc(tree @Tree, id NodeId) ArrayTypeView { } } +pointer_type proc(tree @Tree, id NodeId) PointerTypeView { + node :: get_node(&tree.storage, id) + debug.assert(node.kind == .expr_pointer_type) + return PointerTypeView{ + element = node.data1.node_id, + mutable = node.main.extra != 0, + many = node.data0.extra != 0, + } +} + array_element_count proc(tree @Tree, id NodeId) usize { node :: get_node(&tree.storage, id) debug.assert(node.kind == .expr_array_literal) diff --git a/source/diagnostics/diagnostics.hon b/source/diagnostics/diagnostics.hon index 6d71189..b3dde9f 100644 --- a/source/diagnostics/diagnostics.hon +++ b/source/diagnostics/diagnostics.hon @@ -89,6 +89,11 @@ Code :: enum { unreachable_match_arm invalid_match_capture inactive_union_field + invalid_pointer_type + invalid_address_operand + invalid_dereference + invalid_pointer_assignment + invalid_pointer_access } Diagnostic :: struct { @@ -199,5 +204,10 @@ messages :: $enummap.init(Code, []u8, { unreachable_match_arm = "match arm is unreachable", invalid_match_capture = "payload capture requires compatible nonvoid tagged union variants", inactive_union_field = "tagged union field is not active", + invalid_pointer_type = "pointer requires a supported concrete pointee type", + invalid_address_operand = "address-of requires addressable storage", + 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", }) diff --git a/source/interpreter/interpreter.hon b/source/interpreter/interpreter.hon index 19af408..7cf8f7c 100644 --- a/source/interpreter/interpreter.hon +++ b/source/interpreter/interpreter.hon @@ -42,6 +42,19 @@ Address :: struct { scalar ?@mut ir.Value = null initialized ?@mut bool = null raw bool = false + # source handles retain root identity and native offsets, never frame-local address IDs. + root ?@mut ir.Value = null + root_type ir.Type = .invalid + root_array ir.ArrayId = ir.no_array + byte_offset usize = 0 +} + +@hide:file +Pointer :: struct { + root @mut ir.Value + root_type ir.Type + root_array ir.ArrayId + byte_offset usize } @hide:file @@ -90,6 +103,8 @@ run proc( globals[i].value = aggregate_view(global.type, global.array_type, aggregate_globals[global_offset..global_offset + cells]) global_offset += cells } + pointers := arraylist.init(Pointer, allocator) + defer arraylist.deinit(&pointers) frames := arraylist.init(Frame, allocator) defer { for (frames.items) |frame| free_frame(allocator, frame) @@ -261,6 +276,9 @@ run proc( cells = &[], initialized = &local.initialized, } + address.root = &local.value + address.root_type = address.type + address.root_array = address.array_type if (ir.aggregate(address.type)) address.cells = local.value.data.elements else address.scalar = &local.value frames.items[frame_index].addresses[usize(instruction.destination)] = address @@ -287,6 +305,9 @@ run proc( array_type = instruction.array_type, cells = &[], } + address.root = &global.value + address.root_type = address.type + address.root_array = address.array_type if (ir.aggregate(address.type)) address.cells = global.value.data.elements else address.scalar = &global.value frames.items[frame_index].addresses[usize(instruction.destination)] = address @@ -305,10 +326,12 @@ run proc( } width :: if (base.raw) ir.storage_size(module, shape.element, shape.element_array) else element_cells(module, shape) start :: usize(index) * width - address := Address{ - type = instruction.pointee, array_type = instruction.array_type, - cells = base.cells[start..start + width], initialized = base.initialized, raw = base.raw, - } + address := base + address.type = instruction.pointee + address.array_type = instruction.array_type + 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] frames.items[frame_index].addresses[usize(instruction.destination)] = address value = ir.Value{ type = .address, data = ir.ValueData{ address = u32(instruction.destination) } } @@ -329,14 +352,70 @@ run proc( 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 start :: if (raw) field.byte_offset else field.offset - address := Address{ - type = instruction.pointee, array_type = instruction.array_type, - cells = base.cells[start..start + width], initialized = base.initialized, raw = raw, - } + address := base + address.type = instruction.pointee + address.array_type = instruction.array_type + 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] frames.items[frame_index].addresses[usize(instruction.destination)] = address value = ir.Value{ type = .address, data = ir.ValueData{ address = u32(instruction.destination) } } } + .pointer_from_address: { + id :: frames.items[frame_index].values[usize(instruction.left)].data.address + address :: frames.items[frame_index].addresses[usize(id)] + if address.initialized |initialized| { + if (!initialized^) return try runtime_trap(diagnostic_store, .read_before_initialization, instruction.span) + } + value = try pointer_value(&pointers, Pointer{ + root = address.root?, + root_type = address.root_type, + root_array = address.root_array, + byte_offset = address.byte_offset, + }) + } + .pointer_address: { + 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) + } + 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| { + frames.items[frame_index].addresses[usize(instruction.destination)] = address + value = ir.Value{ type = .address, data = ir.ValueData{ address = u32(instruction.destination) } } + } else { + return try runtime_trap(diagnostic_store, .invalid_pointer_access, instruction.span) + } + } + .pointer_offset: { + 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 := pointers.items[usize(handle)] + index :: frames.items[frame_index].values[usize(instruction.right)].data.unsigned + stride :: ir.storage_size(module, instruction.pointee, instruction.array_type) + if (stride != 0 and index > u64(divtrunc!(maxval!(usize) - address.byte_offset, stride))) { + 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)) { + return try runtime_trap(diagnostic_store, .invalid_pointer_access, instruction.span) + } + value = try pointer_value(&pointers, address) + } .load_address, .store_address: { id :: frames.items[frame_index].values[usize(instruction.left)].data.address address :: frames.items[frame_index].addresses[usize(id)] @@ -447,6 +526,11 @@ run proc( .not_equal: left.data.boolean != right.data.boolean else: unreachable } + .pointer: result = match instruction.op { + .equal: left.data.unsigned == right.data.unsigned + .not_equal: left.data.unsigned != right.data.unsigned + else: unreachable + } else: unreachable } value = ir.Value{ type = .bool, data = ir.ValueData{ boolean = result } } @@ -459,6 +543,77 @@ run proc( unreachable } +@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. + root_bits :: ptrcast!(@usize, &address.root)^ + for (pointers.items) |location, handle| { + if (ptrcast!(@usize, &location.root)^ == root_bits and location.byte_offset == address.byte_offset) { + # refresh metadata if a later invocation reused a released frame's allocation. + pointers.items[handle] = address + return ir.Value{ type = .pointer, data = ir.ValueData{ unsigned = u64(handle) } } + } + } + handle :: pointers.items.len + try arraylist.append(pointers, address) + return ir.Value{ type = .pointer, data = ir.ValueData{ unsigned = u64(handle) } } +} + +@hide:file +project_pointer proc( + module @ir.Module, + base Address, + offset usize, + type ir.Type, + array_type ir.ArrayId, +) ?Address { + if (offset == 0 and base.type == type and base.array_type == array_type) return base + if (!ir.aggregate(base.type)) return null + shape :: module.arrays.items[usize(base.array_type)] + if (base.raw or shape.is_union) { + width :: ir.storage_size(module, type, array_type) + if (offset > base.cells.len or width > base.cells.len - offset) return null + address := base + address.type = type + address.array_type = array_type + address.cells = base.cells[offset..offset + width] + address.scalar = null + address.raw = true + address.byte_offset += offset + return address + } + if (shape.kind == .array) { + stride :: ir.storage_size(module, shape.element, shape.element_array) + index :: if (stride == 0) usize(0) else divtrunc!(offset, stride) + if (index >= shape.len) return null + width :: element_cells(module, shape) + start :: index * width + child := base + child.type = shape.element + child.array_type = shape.element_array + child.cells = base.cells[start..start + width] + child.scalar = null + child.byte_offset += index * stride + if (!ir.aggregate(child.type)) child.scalar = &child.cells[0] + return project_pointer(module, child, offset - index * stride, type, array_type) + } + for (shape.fields) |field| { + if (offset < field.byte_offset) continue + relative :: offset - field.byte_offset + if (relative > ir.storage_size(module, field.type, field.array_type)) continue + width :: if (ir.aggregate(field.type)) module.arrays.items[usize(field.array_type)].cells else 1 + child := base + child.type = field.type + child.array_type = field.array_type + child.cells = base.cells[field.offset..field.offset + width] + child.scalar = null + child.byte_offset += field.byte_offset + if (!ir.aggregate(child.type)) child.scalar = &child.cells[0] + if (project_pointer(module, child, relative, type, array_type)) |address| return address + } + return null +} + @hide:file push_frame proc( allocator mem.Allocator, diff --git a/source/ir/ir.hon b/source/ir/ir.hon index 81c2456..e6ac183 100644 --- a/source/ir/ir.hon +++ b/source/ir/ir.hon @@ -24,6 +24,7 @@ Type :: enum { array record address + pointer i8, i16, i32, isize u8, u16, u32, u64, usize } @@ -74,6 +75,7 @@ integer_min proc(type Type) i64 { ValueData :: union { integer i64 + # source pointers use run-wide handles; internal addresses use frame-local IDs below. unsigned u64 real f64 boolean bool @@ -141,6 +143,9 @@ Op :: enum { field_address load_address store_address + pointer_from_address + pointer_address + pointer_offset negate convert logical_not @@ -160,6 +165,7 @@ Op :: enum { Instruction :: struct { op Op type Type = .void + # pointer operations describe their pointee here, not aggregate result storage. array_type ArrayId = no_array pointee Type = .invalid span diagnostics.Span = diagnostics.Span{ start = 0, end = 0 } @@ -395,7 +401,7 @@ storage_alignment proc(module @Module, type Type, shape ArrayId) usize { if (aggregate(type)) return module.arrays.items[usize(shape)].alignment bits :: integer_bits(type) if (bits != 0) return divtrunc!(bits, 8) - return if (type == .float or type == .string or type == .function) 8 else 1 + return if (type == .float or type == .string or type == .function or type == .pointer) 8 else 1 } storage_size proc(module @Module, type Type, shape ArrayId) usize { @@ -403,7 +409,7 @@ storage_size proc(module @Module, type Type, shape ArrayId) usize { bits :: integer_bits(type) if (bits != 0) return divtrunc!(bits, 8) return match type { - .float, .function: 8 + .float, .function, .pointer: 8 .string: 16 .bool: 1 .void: 0 @@ -457,6 +463,7 @@ write_storage proc(module @Module, cells []mut Value, value Value) void { .bool: bits = if (value.data.boolean) 1 else 0 .string: bits = u64(value.data.string) .function: bits = u64(value.data.function) + .pointer: bits = value.data.unsigned .void: return else: bits = ptrcast!(@u64, &value.data.integer)^ } diff --git a/source/lexer/scan.hon b/source/lexer/scan.hon index d4ad7d8..bf5a515 100644 --- a/source/lexer/scan.hon +++ b/source/lexer/scan.hon @@ -295,6 +295,8 @@ scan proc( ',': .comma '|': .pipe '@': .at + '&': .ampersand + '^': .caret '(': .open_paren ')': .close_paren '[': .open_bracket diff --git a/source/lexer/token.hon b/source/lexer/token.hon index bfd05c3..8ef0f64 100644 --- a/source/lexer/token.hon +++ b/source/lexer/token.hon @@ -48,7 +48,8 @@ TokenKind :: enum { star, slash range, range_inclusive - pipe, at + at, ampersand, caret + pipe # logical bang diff --git a/source/llvm/instructions.hon b/source/llvm/instructions.hon index 0e6e918..e112a98 100644 --- a/source/llvm/instructions.hon +++ b/source/llvm/instructions.hon @@ -168,6 +168,27 @@ emit_instruction proc(state @mut State, block @ir.Block, instruction ir.Instruct 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}) } + .pointer_from_address: { + address :: try load(state, instruction.left) + flag :: fresh(state) + position :: location(state.line_starts, instruction.span) + try io.print(" %r{} = extractvalue %address %r{}, 1\n", output, {flag, address}) + try io.print(" call void @check_init(ptr %r{}, i1 true, i64 {}, i64 {}, i64 {})\n", output, {flag, usize(instruction.span.source), position.line, position.column}) + try io.print(" %r{} = extractvalue %address %r{}, 0\n", output, {result, address}) + } + .pointer_address: { + pointer :: try load(state, instruction.left) + 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}) + } + .pointer_offset: { + pointer :: try load(state, instruction.left) + index :: try load(state, instruction.right) + try io.print(" %r{} = getelementptr ", output, {result}) + 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}) + } .load_address, .store_address: { address :: try load(state, instruction.left) pointer :: fresh(state) diff --git a/source/llvm/llvm.hon b/source/llvm/llvm.hon index d83a026..6eae6aa 100644 --- a/source/llvm/llvm.hon +++ b/source/llvm/llvm.hon @@ -133,7 +133,7 @@ write_type proc(output io.Writer, module @ir.Module, type Type) void ! io.WriteE .float: try io.write_all(output, "double") .bool: try io.write_all(output, "i1") .string: try io.write_all(output, "%string") - .function: try io.write_all(output, "ptr") + .function, .pointer: try io.write_all(output, "ptr") .address: try io.write_all(output, "%address") .array: { shape :: module.arrays.items[usize(type.shape)] @@ -275,6 +275,7 @@ print_call proc(output io.Writer, module @ir.Module, type Type, operand usize) v .float: try io.print(" call void @print_float(double %r{})\n", output, {operand}) .bool: try io.print(" call void @print_bool(i1 %r{})\n", output, {operand}) .string: try io.print(" call void @print_string(%string %r{})\n", output, {operand}) + .pointer: try io.write_all(output, " call void @out(i32 1, ptr @pointer_text, i64 9)\n") .array, .record: { try io.print(" call void @print_array{}(", output, {u32(type.shape)}) try write_type(output, module, type) diff --git a/source/llvm/runtime.hon b/source/llvm/runtime.hon index 16fcd86..334c265 100644 --- a/source/llvm/runtime.hon +++ b/source/llvm/runtime.hon @@ -37,6 +37,7 @@ runtime :: `@fmt_float = private constant [6 x i8] c"%.17g\00" `@true = private constant [4 x i8] c"true" `@false = private constant [5 x i8] c"false" + `@pointer_text = private constant [9 x i8] c"" `@colon = private constant [1 x i8] c":" `@error = private constant [9 x i8] c": error: " `@runtime_trap = private constant [16 x i8] c": runtime trap: " diff --git a/source/lower/lower.hon b/source/lower/lower.hon index c92a65e..4ef1649 100644 --- a/source/lower/lower.hon +++ b/source/lower/lower.hon @@ -149,6 +149,7 @@ lower_type proc(state @State, type typecheck.Type) ir.Type { .u64: .u64 .array: .array .record: .record + .pointer: .pointer .float: .float .bool: .bool .string: .string @@ -590,6 +591,7 @@ lower_statement proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | d .assignment_stmt: { assignment :: ast.assignment_stmt(tree, id) if (ast.expr_kind(tree, assignment.target) == .index + 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) @@ -924,10 +926,20 @@ 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) { - .index: return is_place(state, ast.index_expr(state.tree, id).base) + .unary: return ast.unary_expr(state.tree, id).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) + } .selection: { if (sema.binding(state.resolution, id).kind == .none) { - return is_place(state, ast.selection_expr(state.tree, id).base) + base :: ast.selection_expr(state.tree, id).base + checked :: typecheck.type_of(state.checked, base) + if (checked.kind == .pointer) { + pointer :: typecheck.pointer_type(state.checked, checked.pointer_type) + return pointer.element.kind == .record + } + return is_place(state, base) } binding :: sema.binding(state.resolution, id) if (binding.kind != .declaration) return false @@ -945,14 +957,37 @@ is_place proc(state @State, id ast.NodeId) bool { } } +@hide:file +pointer_address proc( + state @mut State, pointer ir.ValueId, element typecheck.Type, span diagnostics.Span, +) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) { + return try emit(state, ir.Instruction{ + op = .pointer_address, type = .address, left = pointer, + pointee = lower_type(state, element), array_type = try array_shape(state, element), span = span, + }) +} + @hide:file lower_place proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) { span :: ast.node_span(state.tree, id) checked :: typecheck.type_of(state.checked, id) + if (ast.expr_kind(state.tree, id) == .unary) { + expression :: ast.unary_expr(state.tree, id) + pointer :: try lower_expression(state, expression.operand) + if (state.terminated or trap_type(state, id)) return ir.no_value + return try pointer_address(state, pointer, checked, span) + } if (ast.expr_kind(state.tree, id) == .selection and sema.binding(state.resolution, id).kind == .none) { expression :: ast.selection_expr(state.tree, id) - base :: try lower_place(state, expression.base) + base_type :: typecheck.type_of(state.checked, expression.base) + base := ir.no_value + if (base_type.kind == .pointer) { + pointer :: try lower_expression(state, expression.base) + if (state.terminated or trap_type(state, id)) return ir.no_value + element :: typecheck.pointer_type(state.checked, base_type.pointer_type).element + base = try pointer_address(state, pointer, element, span) + } else base = try lower_place(state, expression.base) if (state.terminated or trap_type(state, id)) return ir.no_value return try emit(state, ir.Instruction{ op = .field_address, @@ -966,7 +1001,23 @@ 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 :: try lower_place(state, expression.base) + base_type :: typecheck.type_of(state.checked, expression.base) + base := ir.no_value + if (base_type.kind == .pointer) { + pointer :: try lower_expression(state, expression.base) + if (state.terminated or trap_type(state, id)) return ir.no_value + descriptor :: typecheck.pointer_type(state.checked, base_type.pointer_type) + if (descriptor.many) { + index :: try lower_expression(state, expression.index) + if (state.terminated) return ir.no_value + offset :: try emit(state, ir.Instruction{ + op = .pointer_offset, type = .pointer, left = pointer, right = index, + pointee = lower_type(state, checked), array_type = try array_shape(state, checked), span = span, + }) + return try pointer_address(state, offset, checked, span) + } + base = try pointer_address(state, pointer, descriptor.element, span) + } else base = try lower_place(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 @@ -1014,7 +1065,7 @@ lower_expression proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocEr # ranges have no physical value; only lower_for decomposes an iterable. if (!trap_type(state, id)) unreachable } - .array_type, .record_type, .enum_type, .union_type, .distinct_type: { + .array_type, .pointer_type, .record_type, .enum_type, .union_type, .distinct_type: { if (!trap_type(state, id)) { trap(state, try diagnostics.add(state.diagnostics, .type_not_value, span), span) } @@ -1091,6 +1142,27 @@ lower_expression proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocEr .identifier, .selection: { if (ast.expr_kind(tree, id) == .selection 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 == .pointer) { + descriptor :: typecheck.pointer_type(state.checked, base_type.pointer_type) + if (!descriptor.many and descriptor.element.kind == .array) { + pointer :: try lower_expression(state, selection.base) + if (state.terminated or trap_type(state, id)) return ir.no_value + member :: ast.source_text(tree, ast.node_span(tree, selection.member)) + array :: typecheck.array_type(state.checked, descriptor.element.array_type) + if (mem.eql(member, "len")) return try constant(state, ir.Value{ + type = .usize, data = ir.ValueData{ unsigned = u64(array.len) }, + }, span) + if (mem.eql(member, "ptr")) { + zero :: try constant(state, ir.Value{ type = .usize, data = ir.ValueData{ unsigned = 0 } }, span) + return try emit(state, ir.Instruction{ + op = .pointer_offset, type = .pointer, left = pointer, right = zero, + pointee = lower_type(state, array.element), array_type = try array_shape(state, array.element), span = span, + }) + } + } + } if (is_place(state, id)) { address :: try lower_place(state, id) if (state.terminated) return ir.no_value @@ -1120,6 +1192,27 @@ lower_expression proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocEr } .unary: { expression :: ast.unary_expr(tree, id) + if (expression.operator == .address_of) { + if (!is_place(state, expression.operand)) { + _ = try lower_expression(state, expression.operand) + if (!state.terminated) _ = trap_type(state, id) + return ir.no_value + } + address :: try lower_place(state, expression.operand) + if (state.terminated or trap_type(state, id)) return ir.no_value + element :: typecheck.type_of(state.checked, expression.operand) + return try emit(state, ir.Instruction{ + op = .pointer_from_address, type = .pointer, left = address, + pointee = lower_type(state, element), array_type = try array_shape(state, element), span = span, + }) + } + if (expression.operator == .dereference) { + address :: try lower_place(state, id) + if (state.terminated) return ir.no_value + return try emit(state, ir.Instruction{ + op = .load_address, type = type, array_type = shape, left = address, span = span, + }) + } if (expression.operator == .negate and ir.integer_bits(type) != 0 and !ir.unsigned_integer(type) and ast.expr_kind(tree, expression.operand) == .literal_int) { text :: ast.source_text(tree, ast.node_span(tree, expression.operand)) magnitude_limit :: ir.integer_max(type) + 1 @@ -1134,6 +1227,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 } return try emit(state, ir.Instruction{ op = op, type = type, left = operand, span = span }) } diff --git a/source/main.hon b/source/main.hon index 28f8548..d2acbdc 100644 --- a/source/main.hon +++ b/source/main.hon @@ -208,6 +208,7 @@ print_value proc(output io.Writer, module @ir.Module, value ir.Value) void ! io. .float: try io.print("{d}", output, {value.data.real}) .bool: try io.print("{}", output, {value.data.boolean}) .string: try io.print("{s}", output, {ir.string_bytes(module, value.data.string)}) + .pointer: try io.write_all(output, "") .array, .record: { shape :: module.arrays.items[usize(value.array_type)] count :: if (value.type == .array) shape.len else shape.fields.len diff --git a/source/parser/parse.hon b/source/parser/parse.hon index 41bdb2a..cbe4077 100644 --- a/source/parser/parse.hon +++ b/source/parser/parse.hon @@ -240,6 +240,8 @@ parse_statement proc(state @mut State, allow_else bool) ast.NodeId ! ParseError or next_n_is(state, 1, .struct) 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, .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) or next_n_is(state, 1, .colon_equal) @@ -254,7 +256,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: { + .enum, .union, .distinct, .alias, .dot, .match, .ampersand, .at, .star: { 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) @@ -603,6 +605,18 @@ 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, .at) or next_is(state, .star)) { + token :: current(state) + state.next_token += 1 + mutable :: next_is(state, .mut) + if (mutable) state.next_token += 1 + element :: try parse_type(state) + return try ast.add_pointer_type(state.builder, ast.Span{ + start = token.span.start, + end = ast.builder_span(state.builder, element).end, + source = token.span.source, + }, element, mutable, token.kind == .star) + } if next_is(state, .open_bracket) { open :: try consume(state, .open_bracket) count ?ast.NodeId := null @@ -653,6 +667,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 ) } @@ -979,11 +994,7 @@ parse_record_field proc(state @mut State, declaration bool) ast.NodeId ! ParseEr value ?ast.NodeId := null named :: member_name(current(state).kind) and ( (!declaration and next_n_is(state, 1, .equal)) - or (declaration and ( - next_n_is(state, 1, .ident) or next_n_is(state, 1, .open_bracket) - or next_n_is(state, 1, .struct) or next_n_is(state, 1, .enum) - or next_n_is(state, 1, .union) or next_n_is(state, 1, .distinct) - )) + or (declaration and starts_type(state.tokens[state.next_token + 1].kind)) ) if (named) { token :: if (declaration) try consume(state, .ident) else try consume_member(state, false) @@ -1067,7 +1078,11 @@ parse_expr_bp proc(state @mut State, min_bp BindingPowerScalar) ast.NodeId ! Par state.next_token += 1 operand :: try parse_expr_bp(state, right_bp) operand_span :: ast.builder_span(state.builder, operand) - operator ast.UnaryOperator :: if (token.kind == .bang) .logical_not else .negate + operator ast.UnaryOperator :: match token.kind { + .bang: .logical_not + .ampersand: .address_of + else: .negate + } yield try ast.add_unary( state.builder, ast.Span{ @@ -1086,6 +1101,16 @@ 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) { + state.next_token += 1 + 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) + left_grouped = false + continue + } if (token.kind == .open_paren) { left = try parse_call(state, left, state.tokens[expression_start].span.start) left_grouped = false @@ -1324,6 +1349,23 @@ invalid_array_literal proc(state @mut State, start, end u32) ast.NodeId ! ParseE }, diagnostic) } +@hide:file +pointer_binding_starts proc(state @State) bool { + depth := usize(0) + for (state.next_token + 2)..(state.tokens.len) |i| { + kind :: state.tokens[i].kind + if (kind == .open_curly or kind == .open_bracket or kind == .open_paren) depth += 1 + if (kind == .close_curly or kind == .close_bracket or kind == .close_paren) { + if (depth == 0) return false + depth -= 1 + } + if (depth != 0) continue + if (kind == .double_colon or kind == .colon_equal) return true + if (kind == .newline or kind == .eof or kind == .equal) return false + } + return false +} + @hide:file named_binding_starts proc(state @State) bool { return ( @@ -1331,6 +1373,8 @@ named_binding_starts proc(state @State) bool { and ( 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, .star) and pointer_binding_starts(state)) ) ) } @@ -1339,6 +1383,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) .struct: return try parse_record_type(state) .enum: return try parse_enum_type(state) .union: return try parse_union_type(state) @@ -1620,7 +1665,7 @@ consume_either proc(state @mut State, kinds []lexer.TokenKind) lexer.Token ! Syn @hide:file prefix_binding_power proc(kind lexer.TokenKind) ?BindingPowerScalar { return match kind { - .minus, .bang: BindingPowerScalar(50) + .minus, .bang, .ampersand: BindingPowerScalar(50) else: null } } diff --git a/source/sema/check/check.hon b/source/sema/check/check.hon index 6438cc0..182eef0 100644 --- a/source/sema/check/check.hon +++ b/source/sema/check/check.hon @@ -18,6 +18,7 @@ TypeKind :: enum { function range array + pointer record enumeration type_value @@ -72,11 +73,21 @@ ArrayType :: struct { mutable bool } +PointerId :: distinct u32 +no_pointer :: maxval!(PointerId) + +PointerType :: struct { + element Type + mutable bool + many bool +} + Type :: struct { kind TypeKind = .none function ast.NodeId = ast.NodeId(maxval!(u32)) diagnostic diagnostics.DiagnosticId = diagnostics.no_diagnostic array_type ArrayId = no_array + pointer_type PointerId = no_pointer range_element TypeKind = .none record_type RecordId = no_record enum_type EnumId = no_enum @@ -88,6 +99,7 @@ Type :: struct { Storage :: struct { types []mut Type arrays arraylist.ArrayList(ArrayType) + pointers arraylist.ArrayList(PointerType) records arraylist.ArrayList(RecordType) enums arraylist.ArrayList(EnumType) backings []mut Type @@ -151,6 +163,7 @@ Cell :: struct { producer ast.NodeId = ast.NodeId(maxval!(u32)) capture ast.NodeId = ast.NodeId(maxval!(u32)) record_checking bool = false + record_pointer_depth usize = 0 nominal bool = false match_pattern bool = false first_edge usize = no_index @@ -179,6 +192,7 @@ State :: struct { groups []mut GroupLink edges arraylist.ArrayList(Edge) arrays arraylist.ArrayList(ArrayType) + pointers arraylist.ArrayList(PointerType) records arraylist.ArrayList(RecordType) initializers arraylist.ArrayList(RecordInitializer) enums arraylist.ArrayList(EnumType) @@ -191,6 +205,7 @@ State :: struct { slots []mut usize collecting_assignment bool = false has_assignments bool = false + pointer_depth usize = 0 } #! borrows the AST, resolution, and diagnostic store only during checking. @@ -235,6 +250,7 @@ check proc( groups = groups, edges = arraylist.init(allocator), arrays = arraylist.init(allocator), + pointers = arraylist.init(allocator), records = arraylist.init(allocator), initializers = arraylist.init(allocator), slots = slots, @@ -247,6 +263,7 @@ check proc( defer arraylist.deinit(&state.edges) defer arraylist.deinit(&state.variants) errdefer arraylist.deinit(&state.arrays) + errdefer arraylist.deinit(&state.pointers) errdefer { for (state.enums.items) |descriptor| mem.free(allocator, descriptor.members) arraylist.deinit(&state.enums) @@ -278,7 +295,7 @@ check proc( binding :: sema.binding(resolution, target) if (binding.kind == .invalid) { cells[i].type = invalid_type(binding.data.diagnostic) - } else if (binding.kind == .declaration and !mutable_target(&state, target)) { + } else if (binding.kind == .declaration and !mutable_target(&state, target, false)) { cells[i].type = invalid_type(try diagnostics.add( diagnostic_store, .invalid_assignment_target, ast.node_span(tree, target), )) @@ -511,6 +528,7 @@ check proc( return Result{ storage = Storage{ types = types, arrays = state.arrays, + pointers = state.pointers, records = state.records, initializers = state.initializers, enums = state.enums, @@ -544,6 +562,10 @@ array_type proc(result @Result, id ArrayId) ArrayType { return result.storage.arrays.items[usize(id)] } +pointer_type proc(result @Result, id PointerId) PointerType { + return result.storage.pointers.items[usize(id)] +} + record_type_count proc(result @Result) usize { return result.storage.records.items.len } @@ -621,6 +643,7 @@ initializer_field proc(result @Result, literal ast.NodeId, index usize) ast.Node deinit proc(result @mut Result) void { mem.free(result.storage.allocator, result.storage.types) arraylist.deinit(&result.storage.arrays) + arraylist.deinit(&result.storage.pointers) for (result.storage.enums.items) |descriptor| mem.free(result.storage.allocator, descriptor.members) arraylist.deinit(&result.storage.enums) mem.free(result.storage.allocator, result.storage.backings) @@ -741,6 +764,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) == .pointer_type) { + type :: try check_pointer_type(state, id) + state.types[usize(id)] = type + state.cells[usize(id)].type = type + return type + } if (type_syntax(ast.expr_kind(state.tree, id))) { type :: try check_type_expression(state, id, false) state.types[usize(id)] = type @@ -1047,7 +1076,7 @@ collect_expression proc(state @mut State, id ast.NodeId) void ! (mem.AllocError if (expression.operator == .negate) state.cells[usize(id)].producer = id if (expression.operator == .logical_not) { state.cells[usize(id)].type = Type{ kind = .bool } - } else { + } else if (expression.operator == .negate) { state.cells[usize(id)].numeric = true } operand :: expression.operand @@ -1106,7 +1135,7 @@ collect_expression proc(state @mut State, id ast.NodeId) void ! (mem.AllocError .array_type: { state.cells[usize(id)].type = invalid_type(try diagnostics.add(state.diagnostics, .type_not_value, ast.node_span(tree, id))) } - .record_type, .union_type, .enum_type, .distinct_type: { + .record_type, .union_type, .enum_type, .distinct_type, .pointer_type: { type := try check_type_expression(state, id, state.cells[usize(id)].nominal) if (type.kind != .invalid) { type.value_kind = type.kind @@ -1240,7 +1269,8 @@ bool_equality proc(state @mut State, id ast.NodeId) bool { return ( (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 == .enumeration or right.kind == .enumeration + or left.kind == .pointer or right.kind == .pointer) ) } @@ -1308,6 +1338,14 @@ 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 (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) { + state.cells[index].expected = expected + try demand(state, ast.unary_expr(state.tree, id).operand, + state.pointers.items[usize(expected.pointer_type)].element, code) + return + } if (state.cells[usize(id)].role == .expression and ast.expr_kind(state.tree, id) == .match_expr) { state.cells[index].expected = expected for 0..ast.match_arm_count(state.tree, id) |i| { @@ -1404,24 +1442,78 @@ require_bool proc(state @mut State, operand ast.NodeId) void ! (mem.AllocError | } @hide:file -mutable_target proc(state @State, id ast.NodeId) bool { - if (ast.expr_kind(state.tree, id) == .index) { - view :: ast.index_expr(state.tree, id) - base :: state.types[usize(view.base)] - return base.kind == .array and state.arrays.items[usize(base.array_type)].mutable and mutable_target(state, view.base) +mutable_target proc(state @mut State, id ast.NodeId, allow_global bool) bool { + kind :: ast.expr_kind(state.tree, id) + if (kind == .unary) { + view :: ast.unary_expr(state.tree, id) + base :: group_type(state, view.operand) + return ( + view.operator == .dereference and base.kind == .pointer + and state.pointers.items[usize(base.pointer_type)].mutable + ) } - if (ast.expr_kind(state.tree, id) == .selection - and sema.binding(state.resolution, id).kind == .none) { + if (kind == .index) { + view :: ast.index_expr(state.tree, id) + base :: group_type(state, view.base) + if (base.kind == .pointer) { + pointer :: state.pointers.items[usize(base.pointer_type)] + return pointer.mutable and (pointer.many + or (pointer.element.kind == .array and state.arrays.items[usize(pointer.element.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) { view :: ast.selection_expr(state.tree, id) - return state.types[usize(view.base)].kind == .record and mutable_target(state, view.base) + base :: group_type(state, view.base) + if (base.kind == .pointer) { + pointer :: state.pointers.items[usize(base.pointer_type)] + return pointer.element.kind == .record and pointer.mutable + } + return base.kind == .record and mutable_target(state, view.base, allow_global) } binding :: sema.binding(state.resolution, id) if (binding.kind != .declaration) return false target :: binding.data.declaration - if (state.cells[usize(target)].role != .declaration or !state.cells[usize(target)].local) return false + if (state.cells[usize(target)].role != .declaration or (!allow_global and !state.cells[usize(target)].local)) return false return ast.statement_decl(state.tree, target).mutability == .variable } +@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 != .pointer) return false + pointer :: state.pointers.items[usize(type.pointer_type)] + return pointer.many or pointer.element.kind == .array +} + +@hide:file +addressable_target proc(state @mut State, id ast.NodeId) bool { + kind :: ast.expr_kind(state.tree, id) + if (kind == .unary) { + view :: ast.unary_expr(state.tree, id) + return view.operator == .dereference and group_type(state, view.operand).kind == .pointer + } + 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)) + } + if (kind == .selection and sema.binding(state.resolution, id).kind == .none) { + view :: ast.selection_expr(state.tree, id) + base :: group_type(state, view.base) + if (base.kind == .pointer) { + pointer :: state.pointers.items[usize(base.pointer_type)] + return pointer.element.kind == .record + } + return base.kind == .record and addressable_target(state, view.base) + } + binding :: sema.binding(state.resolution, id) + if (binding.kind != .declaration) return false + role :: state.cells[usize(binding.data.declaration)].role + return role == .declaration or role == .parameter or role == .loop_capture +} + @hide:file same_signature proc(state @State, left, right ast.NodeId) bool { tree :: state.tree @@ -1437,11 +1529,22 @@ same_signature proc(state @State, left, right ast.NodeId) bool { return true } +@hide:file +assignable_type proc(state @State, actual, expected Type) bool { + if (same_type(state, actual, expected)) return true + 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.mutable) return same_type(state, a.element, b.element) + return assignable_type(state, a.element, b.element) +} + @hide:file 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 (!same_type(state, actual, expected)) { + if (!assignable_type(state, actual, expected)) { try fail(state, value, .type_mismatch, ast.node_span(state.tree, value)) } } @@ -1465,11 +1568,18 @@ validate proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnost assignment :: ast.assignment_stmt(tree, id) target_type :: state.types[usize(assignment.target)] if (target_type.kind == .invalid) return - if (mutable_target(state, assignment.target)) { + if (mutable_target(state, assignment.target, false)) { try compatible(state, assignment.value, target_type) } else { - code :: if (ast.expr_kind(tree, assignment.target) == .index) - diagnostics.Code.invalid_index_assignment else diagnostics.Code.invalid_assignment_target + kind :: ast.expr_kind(tree, assignment.target) + code := if (kind == .index) diagnostics.Code.invalid_index_assignment else diagnostics.Code.invalid_assignment_target + if (kind == .unary and ast.unary_expr(tree, assignment.target).operator == .dereference) { + code = .invalid_pointer_assignment + } else if (kind == .index and state.types[usize(ast.index_expr(tree, assignment.target).base)].kind == .pointer) { + code = .invalid_pointer_assignment + } else if (kind == .selection and state.types[usize(ast.selection_expr(tree, assignment.target).base)].kind == .pointer) { + code = .invalid_pointer_assignment + } try fail(state, id, code, ast.node_span(tree, assignment.target)) } } @@ -1481,7 +1591,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 (!same_type(state, actual, expected)) { + } else if (!assignable_type(state, actual, expected)) { try fail(state, value, .return_type_mismatch, ast.node_span(tree, value)) state.types[usize(id)] = state.types[usize(value)] } @@ -1497,6 +1607,19 @@ validate proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnost try require_bool(state, expression.operand) return } + if (expression.operator == .address_of) { + if (!addressable_target(state, expression.operand)) { + try fail(state, id, .invalid_address_operand, expression.span) + } + return + } + if (expression.operator == .dereference) { + if (state.types[usize(expression.operand)].kind != .invalid + and state.types[usize(expression.operand)].kind != .pointer) { + try fail(state, id, .invalid_dereference, expression.span) + } + return + } operand :: state.types[usize(expression.operand)] if (operand.kind != .invalid and (!numeric_kind(operand.kind) or unsigned_kind(operand.kind))) { try fail(state, id, .invalid_operand, ast.node_span(tree, id)) @@ -1513,8 +1636,8 @@ validate proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnost right :: state.types[usize(expression.right)] if (left.kind == .invalid or right.kind == .invalid) return if ((expression.operator == .equal or expression.operator == .not_equal) - and (left.kind == .bool or left.kind == .enumeration) - and same_type(state, left, right)) return + 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 if (!numeric_kind(left.kind) or !numeric_kind(right.kind) or !same_type(state, left, right)) { try fail(state, id, .invalid_operand, expression.span) } else if (expression.operator == .divide and left.kind != .float) { @@ -1572,8 +1695,7 @@ validate proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | diagnost if (index.kind != .invalid and (index.kind != .usize or index.nominal != ast.NodeId(maxval!(u32)))) { try fail(state, view.index, .invalid_index_type, ast.node_span(tree, view.index)) } - if (base.kind != .invalid - and (base.kind != .array or base.nominal != ast.NodeId(maxval!(u32)))) { + if (base.kind != .invalid and !indexable_type(state, base)) { try fail(state, id, .not_indexable, ast.node_span(tree, view.base)) } } @@ -1758,6 +1880,35 @@ evaluate_integer proc(state @mut State, id ast.NodeId, allow_local bool) ?Consta } } +@hide:file +intern_pointer proc(state @mut State, descriptor PointerType) Type ! mem.AllocError { + for state.pointers.items |existing, i| { + if (existing.mutable == descriptor.mutable and existing.many == descriptor.many + and same_type(state, existing.element, descriptor.element)) { + return Type{ kind = .pointer, pointer_type = PointerId(i) } + } + } + try arraylist.append(&state.pointers, descriptor) + return Type{ kind = .pointer, pointer_type = PointerId(state.pointers.items.len - 1) } +} + +@hide:file +check_pointer_type proc(state @mut State, id ast.NodeId) Type ! (mem.AllocError | diagnostics.CapacityError) { + view :: ast.pointer_type(state.tree, id) + state.pointer_depth += 1 + defer state.pointer_depth -= 1 + element :: try check_annotation(state, view.element, true) + if (element.kind == .invalid) return element + binding :: sema.binding(state.resolution, view.element) + if (element.kind == .none or element.kind == .void or element.kind == .range + or element.kind == .type_value or element.kind == .function + or has_inferred_count(state, element) + or (binding.kind == .builtin and (binding.data.builtin == .int or binding.data.builtin == .float))) { + return invalid_type(try diagnostics.add(state.diagnostics, .invalid_pointer_type, ast.node_span(state.tree, id))) + } + return try intern_pointer(state, PointerType{ element = element, mutable = view.mutable, many = view.many }) +} + @hide:file intern_array proc(state @mut State, descriptor ArrayType) Type ! mem.AllocError { for state.arrays.items |existing, i| { @@ -1775,6 +1926,11 @@ same_type proc(state @State, left, right Type) bool { if (left.kind != right.kind) return false if (left.nominal != right.nominal) return false if (left.kind == .enumeration) return left.enum_type == right.enum_type + if (left.kind == .pointer) { + a :: state.pointers.items[usize(left.pointer_type)] + b :: state.pointers.items[usize(right.pointer_type)] + return a.mutable == b.mutable and a.many == b.many and same_type(state, a.element, b.element) + } if (left.kind == .type_value) { a := left b := right @@ -1881,6 +2037,33 @@ infer_shape proc(state @mut State, id ast.NodeId, defaults bool) Type ! (mem.All if (state.cells[index].type.kind == .invalid) return state.cells[index].type state.cells[index].visiting = true defer state.cells[index].visiting = false + if (state.cells[usize(id)].role == .expression and ast.expr_kind(state.tree, id) == .unary) { + view :: ast.unary_expr(state.tree, id) + if (view.operator == .address_of or view.operator == .dereference) { + operand :: try infer_shape(state, view.operand, defaults) + if (operand.kind == .none) return Type{} + if (operand.kind == .invalid) { + seed(state, id, operand) + return operand + } + if (view.operator == .dereference) { + if (operand.kind != .pointer) { + try fail(state, id, .invalid_dereference, view.span) + seed(state, id, state.types[usize(id)]) + return group_type(state, id) + } + element :: state.pointers.items[usize(operand.pointer_type)].element + seed(state, id, element) + return element + } + mutable :: mutable_target(state, view.operand, true) + expected :: state.cells[index].expected + 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) + return type + } + } if (state.cells[usize(id)].role == .expression and ast.expr_kind(state.tree, id) == .match_expr) { return try infer_match(state, id, defaults) } @@ -1899,7 +2082,28 @@ infer_shape proc(state @mut State, id ast.NodeId, defaults bool) Type ! (mem.All enum_shape :: try check_annotation(state, view.base, false) if (enum_shape.kind == .enumeration) return try select_enum_member(state, id, view.member, enum_shape) } - base :: try infer_shape(state, view.base, defaults) + base := try infer_shape(state, view.base, defaults) + 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 + and ast.expr_kind(state.tree, view.member) == .identifier) { + member :: ast.source_text(state.tree, ast.node_span(state.tree, view.member)) + if (mem.eql(member, "len")) { + type :: Type{ kind = .usize } + seed(state, id, type) + return type + } + if (mem.eql(member, "ptr")) { + array :: state.arrays.items[usize(pointer.element.array_type)] + type :: try intern_pointer(state, PointerType{ + element = array.element, mutable = pointer.mutable and array.mutable, many = true, + }) + seed(state, id, type) + return type + } + } + if (!pointer.many or pointer.element.kind == .record) base = pointer.element + } if (base.kind == .record and base.nominal == ast.NodeId(maxval!(u32))) { fields :: state.records.items[usize(base.record_type)].fields slot :: selected_field(state, fields, view.member) @@ -1987,7 +2191,15 @@ infer_shape proc(state @mut State, id ast.NodeId, defaults bool) Type ! (mem.All } if (state.cells[usize(id)].role == .expression and ast.expr_kind(state.tree, id) == .index) { view :: ast.index_expr(state.tree, id) - base :: try infer_shape(state, view.base, defaults) + base := try infer_shape(state, view.base, defaults) + if (base.kind == .pointer and base.nominal == ast.NodeId(maxval!(u32))) { + pointer :: state.pointers.items[usize(base.pointer_type)] + if (pointer.many) { + seed(state, id, pointer.element) + return pointer.element + } + base = pointer.element + } if (base.kind == .array and base.nominal == ast.NodeId(maxval!(u32))) { element :: state.arrays.items[usize(base.array_type)].element seed(state, id, element) @@ -2057,7 +2269,7 @@ apply_contexts proc(state @mut State, assignment_values, early bool) void ! (mem } if (assignment_values and state.cells[i].role == .assignment_stmt) { view :: ast.assignment_stmt(tree, id) - if (mutable_target(state, view.target)) try context_demand(state, view.value, state.types[usize(view.target)], .type_mismatch, early) + if (mutable_target(state, view.target, false)) try context_demand(state, view.value, state.types[usize(view.target)], .type_mismatch, early) } if (state.cells[i].role != .expression or state.cells[i].assignment_value != assignment_values or ast.expr_kind(tree, id) != .call) continue if (state.backings[i].kind != .none) { @@ -2093,7 +2305,7 @@ 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 != .enumeration and !integer_kind(expected.kind)) return + and expected.kind != .pointer and expected.kind != .enumeration and !integer_kind(expected.kind)) return try demand(state, id, expected, code) } @@ -2281,18 +2493,35 @@ intern_record proc(state @mut State, declaration ast.NodeId, fields []mut Record @hide:file check_record proc(state @mut State, id ast.NodeId, nominal bool) Type ! (mem.AllocError | diagnostics.CapacityError) { cached := state.cells[usize(id)].type - if (cached.kind == .record or cached.kind == .type_value) { + if (cached.kind == .invalid) return cached + if (!state.cells[usize(id)].record_checking and (cached.kind == .record or cached.kind == .type_value)) { cached.kind = .record return cached } - if (cached.kind == .invalid) return cached if (state.cells[usize(id)].record_checking) { + if (cached.kind == .record and state.pointer_depth > state.cells[usize(id)].record_pointer_depth) { + cached.kind = .record + return cached + } type :: invalid_type(try diagnostics.add(state.diagnostics, .recursive_record, ast.node_span(state.tree, id))) state.cells[usize(id)].type = type return type } state.cells[usize(id)].record_checking = true defer state.cells[usize(id)].record_checking = false + state.cells[usize(id)].record_pointer_depth = state.pointer_depth + record_id := no_record + result_type := Type{} + if (nominal) { + record_id = RecordId(state.records.items.len) + empty :: try mem.alloc(RecordField, state.records.allocator, 0) + arraylist.append(&state.records, RecordType{ declaration = id, fields = empty }) catch |err| { + mem.free(state.records.allocator, empty) + return err + } + result_type = Type{ kind = .record, record_type = record_id } + state.cells[usize(id)].type = result_type + } count :: ast.record_field_count(state.tree, id) fields :: try mem.alloc(RecordField, state.records.allocator, count) owned := true @@ -2319,10 +2548,12 @@ check_record proc(state @mut State, id ast.NodeId, nominal bool) Type ! (mem.All state.cells[usize(id)].type = failure return failure } - declaration :: if (nominal) id else ast.NodeId(maxval!(u32)) - type :: try intern_record(state, declaration, fields) + if (nominal) { + mem.free(state.records.allocator, state.records.items[usize(record_id)].fields) + state.records.items[usize(record_id)].fields = fields + } else result_type = try intern_record(state, ast.NodeId(maxval!(u32)), fields) owned = false - state.cells[usize(id)].type = type + state.cells[usize(id)].type = result_type state.cells[usize(id)].record_checking = false saved_assignment :: state.collecting_assignment state.collecting_assignment = false @@ -2332,7 +2563,7 @@ check_record proc(state @mut State, id ast.NodeId, nominal bool) Type ! (mem.All try collect_expression(state, value) } } - return type + return result_type } @hide:file @@ -2457,6 +2688,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 ) } @@ -2483,16 +2715,22 @@ immediate_backing proc(state @State, type Type) Type { @hide:file check_type_expression proc(state @mut State, id ast.NodeId, nominal bool) Type ! (mem.AllocError | diagnostics.CapacityError) { + kind :: ast.expr_kind(state.tree, id) + if (kind == .record_type) return try check_record(state, id, nominal) + if (kind == .pointer_type) return try check_pointer_type(state, id) + if (kind == .union_type) { + type :: try check_union(state, id, nominal) + state.cells[usize(id)].type = type + return type + } cached := state.cells[usize(id)].type if (cached.kind == .type_value) { cached.kind = cached.value_kind return cached } if (cached.kind != .none) return cached - kind :: ast.expr_kind(state.tree, id) - if (kind == .record_type) return try check_record(state, id, nominal) - if (kind == .union_type or kind == .enum_type) { - type :: if (kind == .union_type) try check_union(state, id, nominal) else try check_enum(state, id) + if (kind == .enum_type) { + type :: try check_enum(state, id) state.cells[usize(id)].type = type return type } @@ -2630,11 +2868,33 @@ check_enum proc(state @mut State, id ast.NodeId) Type ! (mem.AllocError | diagno @hide:file check_union proc(state @mut State, id ast.NodeId, nominal bool) Type ! (mem.AllocError | diagnostics.CapacityError) { + cached := state.cells[usize(id)].type + if (cached.kind == .invalid) return cached + if (!state.cells[usize(id)].record_checking and (cached.kind == .record or cached.kind == .type_value)) { + cached.kind = .record + return cached + } if (state.cells[usize(id)].record_checking) { + if (state.pointer_depth > state.cells[usize(id)].record_pointer_depth) { + cached.kind = .record + return cached + } return invalid_type(try diagnostics.add(state.diagnostics, .recursive_record, ast.node_span(state.tree, id))) } state.cells[usize(id)].record_checking = true defer state.cells[usize(id)].record_checking = false + state.cells[usize(id)].record_pointer_depth = state.pointer_depth + record_id :: RecordId(state.records.items.len) + empty :: try mem.alloc(RecordField, state.records.allocator, 0) + declaration :: if (nominal) id else ast.NodeId(maxval!(u32)) + arraylist.append(&state.records, RecordType{ + declaration = declaration, fields = empty, is_union = true, + }) catch |err| { + mem.free(state.records.allocator, empty) + return err + } + result_type :: Type{ kind = .record, record_type = record_id } + state.cells[usize(id)].type = result_type count :: ast.record_field_count(state.tree, id) fields :: try mem.alloc(RecordField, state.records.allocator, count) owned := true @@ -2688,16 +2948,13 @@ check_union proc(state @mut State, id ast.NodeId, nominal bool) Type ! (mem.Allo members_owned = false tag = Type{ kind = .enumeration, enum_type = EnumId(state.enums.items.len - 1) } } - declaration :: if (nominal) id else ast.NodeId(maxval!(u32)) - try arraylist.append(&state.records, RecordType{ - declaration = declaration, - fields = fields, is_union = true, tag_type = tag, - declared_tag_type = declared_tag, - }) + mem.free(state.records.allocator, state.records.items[usize(record_id)].fields) + state.records.items[usize(record_id)].fields = fields + state.records.items[usize(record_id)].tag_type = tag + state.records.items[usize(record_id)].declared_tag_type = declared_tag owned = false - type :: Type{ kind = .record, record_type = RecordId(state.records.items.len - 1) } - state.cells[usize(id)].type = type - return type + state.cells[usize(id)].type = result_type + return result_type } @hide:file diff --git a/source/sema/resolve.hon b/source/sema/resolve.hon index a13e8c5..ca313bc 100644 --- a/source/sema/resolve.hon +++ b/source/sema/resolve.hon @@ -571,6 +571,7 @@ resolve_expr proc( try resolve_expr(state, scope, ast.call_arg(tree, id, i)) } } + .pointer_type: try resolve_expr(state, scope, ast.pointer_type(tree, id).element) .array_type: { type :: ast.array_type(tree, id) if (type.count) |count| try resolve_expr(state, scope, count) diff --git a/tests/native.py b/tests/native.py index 19502a5..54fdf35 100644 --- a/tests/native.py +++ b/tests/native.py @@ -11,6 +11,245 @@ HONEY = ROOT / "build/honey" # Explicit expectations guard against frontend bugs shared by both execution paths. CASES = { + "pointer_assignment_evaluates_rhs_first_once": ( + """ + Box :: struct { value i16, count i16 } + target proc(p @mut Box) @mut Box { + p.count = p.count + 1 + return p + } + input proc(p @mut Box) i16 { + p.count = p.count + 10 + return p.count + } + main proc() [2]i16 { + box Box := { value = 0, count = 0 } + target(&box).value = input(&box) + return [box.value, box.count] + } + """, b"[10, 11]\n", None, + ), + "pointer_recursive_record_type": ( + """ + Node :: struct { value i16, next @Node } + read_next proc(p @Node) i16 { return p.next.value } + main proc() i16 { return 42 } + """, b"42\n", None, + ), + "pointer_many_dereference": ( + """ + read proc(p *i16) i16 { return p^ } + main proc() i16 { + values [1]mut i16 := [42] + return read((&values).ptr) + } + """, b"42\n", None, + ), + "pointer_many_from_array_element": ( + """ + main proc() i16 { + values [3]mut i16 := [1, 2, 3] + pointer *mut i16 :: &values[0] + pointer[2] = 42 + return values[2] + } + """, b"42\n", None, + ), + "pointer_readonly_child_type_rejected": ( + "read proc(p @u16) u16 { return p^ }\nmain proc() u16 { value i16 := 42\nreturn read(&value) }", + b"", b"incompatible value type", + ), + "pointer_scalar_alias_across_calls": ( + """ + identity proc(p @mut i16) @mut i16 { return p } + update proc(p @mut i16) void { p^ = p^ + 2 } + main proc() i16 { + value i16 := 40 + pointer :: identity(&value) + update(pointer) + return value + } + """, b"42\n", None, + ), + "pointer_readonly_parameter": ( + """ + read proc(p @i16) i16 { return p^ } + main proc() i16 { + value i16 := 42 + return read(&value) + } + """, b"42\n", None, + ), + "pointer_record_alias_and_value_copy": ( + """ + Point :: struct { x i16, y i16 } + set proc(p @mut Point) void { p.x = 40 } + main proc() i16 { + point Point := { x = 1, y = 2 } + pointer :: &point + copy :: pointer^ + set(pointer) + if (copy.x != 1) return -1 + return point.x + pointer.y + } + """, b"42\n", None, + ), + "pointer_array_and_many_index": ( + """ + update proc(p @mut [3]mut i16) void { + p[1] = p[0] + 2 + values :: p.ptr + values[2] = values[1] + } + main proc() i16 { + values [3]mut i16 := [40, 0, 0] + update(&values) + if ((&values).len != 3) return -1 + return values[2] + } + """, b"42\n", None, + ), + "pointer_aggregate_copy_keeps_alias": ( + """ + Holder :: struct { pointer @mut i16 } + identity proc(h Holder) Holder { return h } + main proc() i16 { + value i16 := 40 + first :: Holder { pointer = &value } + second :: identity(first) + pointers [2]@mut i16 :: [first.pointer, second.pointer] + pointers[1]^ = pointers[0]^ + 2 + return value + } + """, b"42\n", None, + ), + "pointer_global_returned_from_callee": ( + """ + value i16 :: 42 + address proc() @i16 { return &value } + recurse proc(p @i16, n i16) i16 { + if (n == 0) return p^ + return recurse(p, n - 1) + } + main proc() i16 { return recurse(address(), 20) } + """, b"42\n", None, + ), + "pointer_location_identity": ( + """ + Point :: struct { x i16, y i16 } + identity proc(p @mut i16) @mut i16 { return p } + main proc() bool { + point Point := { x = 40, y = 2 } + values [2]mut i16 := [40, 2] + first :: &point + second :: &point + return ( + first == second + and identity(&point.x) == &point.x + and &point.x != &point.y + and (&values).ptr == (&values).ptr + ) + } + """, b"true\n", None, + ), + "pointer_union_roundtrip": ( + """ + Storage :: union { pointer @mut i16, same @mut i16 } + main proc() i16 { + value i16 := 40 + storage :: Storage { pointer = &value } + pointer :: storage.same + pointer^ = pointer^ + 2 + return value + } + """, b"42\n", None, + ), + "pointer_nested_array_aggregate": ( + """ + Point :: struct { x i16, y i16 } + main proc() i16 { + values [2]mut Point := [Point { x = 40, y = 2 }, Point { x = 0, y = 0 }] + pointer :: (&values).ptr + pointer[1] = pointer[0] + pointer[0].x = 9 + return values[1].x + values[1].y + } + """, b"42\n", None, + ), + "pointer_to_pointer": ( + """ + set proc(p @mut @mut i16) void { p^^ = 42 } + main proc() i16 { + value i16 := 0 + pointer := &value + set(&pointer) + return value + } + """, b"42\n", None, + ), + "pointer_repeated_address_in_loop": ( + """ + main proc() i16 { + value i16 := 0 + for 0..42 |i| { + pointer :: &value + pointer^ = i16(i) + 1 + } + return value + } + """, b"42\n", None, + ), + "pointer_zero_sized_array_elements": ( + """ + Empty :: struct {} + main proc() bool { + values [2]mut Empty := [Empty {}, Empty {}] + pointer :: (&values).ptr + pointer[1] = pointer[0] + return (&values).len == 2 + } + """, b"true\n", None, + ), + "pointer_array_bounds_trap": ( + "main proc() i16 { values [1]mut i16 := [42]\nreturn (&values)[1] }", + b"", b"index out of bounds", + ), + "pointer_address_before_initialization": ( + "main proc() i16 { pointer :: &value\nvalue i16 := 42\nreturn pointer^ }", + b"", b"read before initialization", + ), + "pointer_constant_is_readonly": ( + "main proc() i16 { value i16 :: 40\npointer :: &value\npointer^ = 42\nreturn value }", + b"", b"", + ), + "pointer_readonly_parameter_write_rejected": ( + "set proc(p @i16) void { p^ = 42 }\nmain proc() i16 { value i16 := 0\nset(&value)\nreturn value }", + b"", b"", + ), + "pointer_array_readonly_elements": ( + "main proc() i16 { values [1]i16 := [40]\npointer :: &values\npointer[0] = 42\nreturn values[0] }", + b"", b"", + ), + "pointer_readonly_not_mutable_argument": ( + "set proc(p @mut i16) void { p^ = 42 }\nmain proc() i16 { value i16 :: 0\nset(&value)\nreturn value }", + b"", b"incompatible value type", + ), + "pointer_nonpointer_dereference_rejected": ( + "main proc() i16 { value i16 :: 42\nreturn value^ }", + b"", b"dereference requires", + ), + "pointer_void_pointee_rejected": ( + "unused proc(p @void) void {}\nmain proc() int { return 42 }", + b"42\n", b"pointer requires", + ), + "pointer_temporary_address_rejected": ( + "main proc() i16 { pointer :: &(40 + 2)\nreturn pointer^ }", + b"", b"address-of requires", + ), + "pointer_unused_invalid_path": ( + "unused proc() int { return 42^ }\nmain proc() int { return 42 }", + b"42\n", b"dereference requires", + ), "match_float_ranges": ( """ score proc(n f64) i16 { @@ -1000,11 +1239,25 @@ def check_record_packages(temporary, executable): } """).strip() + "\n") check(app, executable, (b"42\n", None)) + (library / "functions.hon").write_text( + "set proc(p @mut Point) void { p.x = 40 }\n" + "identity proc(p @mut Point) @mut Point { return p }\n" + ) + (app / "main.hon").write_text(textwrap.dedent(""" + lib :: import "../library" + main proc() i16 { + p lib.Point := { x = 1 } + pointer :: lib.identity(&p) + lib.set(pointer) + return pointer.x + p.y + } + """).strip() + "\n") + check(app, executable, (b"42\n", None)) types.write_text("Point :: struct { x i16, y i16 = 32767 + 1 }\n") check(app, executable, (b"", b"integer arithmetic overflow")) trapped = run(str(HONEY), "run", str(app)) assert trapped.stderr.startswith(f"{types.resolve()}:1:".encode()), outcome(trapped) - return 2 + return 3 def check_data_model_packages(temporary, executable): @@ -1105,6 +1358,13 @@ def check_dependency_provenance(temporary, executable): return a[1] } """, 5, 12, False), + (""" + fail proc() int { + a :: [42] + pointer :: &a + return pointer[1] + } + """, 6, 12, False), (""" fail proc() int { before :: after