feat: implement compile-time field selection reflection

This commit is contained in:
2026-10-27 16:25:26 +01:00
parent 88b51274df
commit e00a2abe27
9 changed files with 302 additions and 16 deletions
+18 -1
View File
@@ -514,6 +514,21 @@ add_identifier proc(
}, span)
}
#! copies selector spelling into AST-owned storage, independently of its diagnostic span.
add_generated_identifier proc(
builder @mut Builder,
at Span,
interned_name strpool.StringId,
text []u8,
) NodeId ! mem.AllocError {
id :: try add_generated_literal(builder, at, .string, text)
node :: &builder.storage.nodes.items[usize(id)]
node.kind = .expr_identifier
node.data1 = node.main
node.main = NodeData{ name_id = interned_name }
return id
}
add_unary proc(
builder @mut Builder,
span Span,
@@ -2141,10 +2156,12 @@ add_owned_literal proc(
return id
}
#! borrows a literal's spelling, independently of its diagnostic source span.
#! borrows literal or selector spelling, independently of its diagnostic source span.
#! query float_value first for generated IEEE literals.
literal_text proc(tree @Tree, id NodeId) []u8 {
node :: get_node(&tree.storage, id)
if (node.kind == .expr_identifier and node.data0.extra == 1)
return tree.storage.generated.items[usize(node.data1.extra)]
if (node.data0.extra == 1) return tree.storage.generated.items[usize(node.main.extra)]
return source_text(tree, get_span(&tree.storage, id))
}
+8 -1
View File
@@ -398,7 +398,14 @@ compile proc(
}
resolution := try sema.resolve_packages(compilation.allocator, &tree, roots.items, imports.items, &compilation.strings, diagnostic_store)
defer sema.deinit(&resolution)
elaborated := try specialize.elaborate(compilation.allocator, &compilation.builder, compilation.module_root?, &resolution, diagnostic_store)
elaborated := try specialize.elaborate(
compilation.allocator,
&compilation.builder,
compilation.module_root?,
&resolution,
&compilation.strings,
diagnostic_store,
)
defer specialize.deinit(&elaborated)
tree = ast.view(&compilation.builder, compilation.module_root?)
checked := try typecheck.check_elaborated(compilation.allocator, &tree, &resolution, elaborated.constraints.items, diagnostic_store)
+3 -3
View File
@@ -138,8 +138,8 @@ recompiles_specialized_calls_without_borrowing_previous_ir test {
` config struct { counts [2]mut usize, bias i16 } := { counts = [0, 0], bias = 0 }
` original :: config
` for (usize(0)..2) |index| {
` config.counts[index] = index + 1
` config.bias = config.bias + 21
` field!(config, "counts")[index] = index + 1
` field!(config, "bias") = field!(config, "bias") + 21
` }
` return struct {
` values [config.counts[1]]i16
@@ -232,7 +232,7 @@ recompiles_specialized_calls_without_borrowing_previous_ir test {
` if (same_type_array.values[0] + same_type_array.values[1] + same_type_array.bias != 42) return 0
` boundary BoundaryType(TypeRows()) :: { values = [19, 21] }
` same_boundary BoundaryType(TypeRows()) :: boundary
` if (same_boundary.values[0] + same_boundary.values[1] + same_boundary.bias != 42) return 0
` if (same_boundary.values[0] + same_boundary.values[1] + field!(same_boundary, "bias") != 42) return 0
` if (copy_type_rows(TypeRows())[0][0] != u8 or copy_type_rows(TypeRows())[1][0] != i16) return 0
` if (TypeRows()[0][0] != i16) return 0
` if (read_type_rows(TypeRows(), 40) != 42 or read_type_rows(TypeRows(), 40) != 42) return 0
+3 -2
View File
@@ -1972,9 +1972,10 @@ 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)
if (trap_type(state, id)) return ir.no_value
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))
member :: ast.literal_text(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
@@ -1986,7 +1987,7 @@ lower_expression proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocEr
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))
member :: ast.literal_text(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) },
+3 -3
View File
@@ -2830,7 +2830,7 @@ infer_shape proc(state @mut State, id ast.NodeId, defaults bool) Type ! (mem.All
}
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))
member :: ast.literal_text(state.tree, view.member)
if (mem.eql(member, "len")) {
seed(state, id, Type{ kind = .usize })
return group_type(state, id)
@@ -2852,7 +2852,7 @@ infer_shape proc(state @mut State, id ast.NodeId, defaults bool) Type ! (mem.All
!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))
member :: ast.literal_text(state.tree, view.member)
if (mem.eql(member, "len")) {
type :: Type{ kind = .usize }
seed(state, id, type)
@@ -3259,7 +3259,7 @@ named_field proc(tree @ast.Tree, fields []RecordField, name ast.NodeId) usize {
selected_field proc(state @State, fields []RecordField, member ast.NodeId) usize {
if (ast.expr_kind(state.tree, member) == .identifier) return named_field(state.tree, fields, member)
value := usize(0)
for ast.source_text(state.tree, ast.node_span(state.tree, member)) |byte| {
for ast.literal_text(state.tree, member) |byte| {
if (byte < '0' or byte > '9') return no_index
digit :: usize(byte - '0')
if (value > divtrunc!(maxval!(usize) - digit, usize(10))) return no_index
+124 -4
View File
@@ -15,7 +15,7 @@ typecheck :: import "@source/sema/check"
@hide:file no_index :: maxval!(usize)
@hide:file
Error :: alias mem.AllocError | diagnostics.CapacityError
Error :: alias mem.AllocError | diagnostics.CapacityError | strpool.InternError
@hide:file
ValueKind :: enum {
@@ -82,6 +82,7 @@ State :: struct {
allocator mem.Allocator
builder @mut ast.Builder
tree ast.Tree
strings @mut strpool.StringPool
bindings arraylist.ArrayList(sema.Binding)
declarations arraylist.ArrayList(ast.NodeId)
instances arraylist.ArrayList(Instance)
@@ -108,6 +109,7 @@ elaborate proc(
builder @mut ast.Builder,
root ast.NodeId,
resolution @mut sema.Resolution,
strings @mut strpool.StringPool,
diagnostic_store @mut diagnostics.Store,
) Result ! Error {
result := Result{ builder = builder, changes = arraylist.init(allocator), constraints = arraylist.init(allocator) }
@@ -119,6 +121,10 @@ elaborate proc(
id :: ast.NodeId(i)
class :: ast.node_class(&original, id)
if (class == .expression and ast.expr_kind(&original, id) == .comptime_expr) needed = true
if (class == .expression and ast.expr_kind(&original, id) == .builtin_call) {
name :: ast.builtin_call(&original, id).name
if (mem.eql(ast.source_text(&original, ast.node_span(&original, name)), "field")) needed = true
}
if (class == .function and ast.function_generic(&original, id)) needed = true
if (class == .function) {
result_type :: sema.binding(resolution, ast.function_decl(&original, id).return_type)
@@ -134,7 +140,7 @@ elaborate proc(
mapping :: try mem.alloc(ast.NodeId, allocator, ast.node_count(&original))
defer mem.free(allocator, mapping)
state := State{
allocator = allocator, builder = builder, tree = ast.view(builder, root),
allocator = allocator, builder = builder, tree = ast.view(builder, root), strings = strings,
bindings = arraylist.init(allocator), declarations = arraylist.init(allocator),
instances = arraylist.init(allocator), cache = hashmap.init(allocator),
expanded = arraylist.init(allocator), diagnostics = diagnostic_store,
@@ -269,6 +275,13 @@ expand proc(state @mut State, id ast.NodeId) void ! Error {
try refresh(state)
return
}
if (class == .expression and ast.expr_kind(&state.tree, id) == .builtin_call) {
name :: ast.builtin_call(&state.tree, id).name
if (mem.eql(ast.source_text(&state.tree, ast.node_span(&state.tree, name)), "field")) {
try expand_field(state, id)
return
}
}
if (class == .expression and ast.expr_kind(&state.tree, id) == .call) {
callee :: ast.call_expr(&state.tree, id).callee
try expand(state, callee)
@@ -318,6 +331,66 @@ expand proc(state @mut State, id ast.NodeId) void ! Error {
}
}
# erase only the name: ordinary selection checks the base and retains its place,
# evaluation order, mutability and active-union checks through both backends.
@hide:file
expand_field proc(state @mut State, id ast.NodeId) void ! Error {
if (ast.call_arg_count(&state.tree, id) != 2) {
try reject(state, id, .argument_count_mismatch)
return
}
base :: ast.call_arg(&state.tree, id, 0)
argument :: ast.call_arg(&state.tree, id, 1)
state.evaluation_diagnostic = diagnostics.no_diagnostic
value :: try evaluate(state, argument)
if (value.kind != .string) {
if (state.evaluation_diagnostic == diagnostics.no_diagnostic)
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics,
.comptime_argument_required, ast.node_span(&state.tree, argument))
try reject_evaluation(state, id)
return
}
try arraylist.append(&state.rewrites.constraints, argument)
text :: ast.literal_text(&state.tree, value.node)
bytes := arraylist.init(u8, state.allocator)
defer arraylist.deinit(&bytes)
cursor := usize(1)
while (cursor < text.len - 1) {
byte :: ast.string_next(text, &cursor) catch {
try reject(state, id, .invalid_escape)
return
}
try arraylist.append(&bytes, byte)
}
# only canonical decimal names denote tuple ordinals; "01" is a name.
numeric := bytes.items.len > 0 and (bytes.items.len == 1 or bytes.items[0] != '0')
ordinal := u64(0)
for (bytes.items) |byte| {
if (byte < '0' or byte > '9') {
numeric = false
break
}
digit :: u64(byte - '0')
if (ordinal > divtrunc!(maxval!(u64) - digit, u64(10))) {
numeric = false
break
}
ordinal = ordinal * 10 + digit
}
span :: ast.node_span(&state.tree, argument)
member := no_node
if (numeric) member = try ast.add_integer(state.builder, span, ordinal)
else {
interned :: try strpool.intern(state.strings, bytes.items)
member = try ast.add_generated_identifier(state.builder, span, interned, bytes.items)
}
selection :: try ast.add_selection(state.builder, ast.node_span(&state.tree, id), base, member)
try replace(state, id, selection)
try refresh(state)
state.expanded.items[usize(id)] = 0
try expand(state, id)
}
# runtime bodies share the type-producer selector, but retain ordinary runtime
# parameters and control transfers in the selected arm.
@hide:file
@@ -1413,7 +1486,7 @@ type_array_projection proc(state @mut State, id ast.NodeId) bool ! Error {
selected = ast.array_element(&state.tree, array.value, index)
} else {
member :: ast.selection_expr(&state.tree, id).member
if (!mem.eql(ast.source_text(&state.tree, ast.node_span(&state.tree, member)), "len")) {
if (!mem.eql(ast.literal_text(&state.tree, member), "len")) {
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics,
.unknown_field, ast.node_span(&state.tree, member))
return false
@@ -1717,6 +1790,41 @@ execute_type_block proc(state @mut State, execution @mut TypeExecution, block as
return .next
}
# substitute selector names without snapshotting away the assignment's root place.
@hide:file
type_assignment_target proc(state @mut State, execution @mut TypeExecution, id ast.NodeId) ast.NodeId ! Error {
kind :: ast.expr_kind(&state.tree, id)
if (kind == .builtin_call) {
name :: ast.builtin_call(&state.tree, id).name
if (!mem.eql(ast.source_text(&state.tree, ast.node_span(&state.tree, name)), "field")) return id
if (ast.call_arg_count(&state.tree, id) != 2) {
try reject(state, id, .argument_count_mismatch)
return no_node
}
base :: try type_assignment_target(state, execution, ast.call_arg(&state.tree, id, 0))
if (base == no_node) return no_node
argument :: try type_snapshot(state, execution, ast.call_arg(&state.tree, id, 1))
arguments [2]ast.NodeId :: [base, argument]
result :: try ast.add_builtin_call(state.builder, ast.node_span(&state.tree, id), name, arguments[..])
try refresh(state)
try expand(state, result)
if (binding(state, result).kind == .invalid) return no_node
return result
}
if (kind != .selection and kind != .index) return id
base :: if (kind == .selection) ast.selection_expr(&state.tree, id).base else ast.index_expr(&state.tree, id).base
selected :: try type_assignment_target(state, execution, base)
if (selected == no_node or selected == base) return if (selected == no_node) no_node else id
result := no_node
if (kind == .selection)
result = try ast.add_selection(state.builder, ast.node_span(&state.tree, id),
selected, ast.selection_expr(&state.tree, id).member)
else result = try ast.add_index(state.builder, ast.node_span(&state.tree, id),
selected, ast.index_expr(&state.tree, id).index)
try refresh(state)
return result
}
@hide:file
execute_type_statement proc(state @mut State, execution @mut TypeExecution, statement ast.NodeId) TypeFlow ! Error {
match ast.stmt_kind(&state.tree, statement) {
@@ -1756,6 +1864,18 @@ execute_type_statement proc(state @mut State, execution @mut TypeExecution, stat
}
.assignment_stmt: {
assignment :: ast.assignment_stmt(&state.tree, statement)
target :: try type_assignment_target(state, execution, assignment.target)
if (target == no_node) return .invalid
if (target != assignment.target) {
normalized :: try ast.add_assignment_stmt(state.builder,
ast.node_span(&state.tree, statement), target, assignment.value)
try refresh(state)
# synthesized places still need local substitution in snapshots.
previous :: execution.mapping.len
execution.mapping = try mem.realloc(state.allocator, execution.mapping, ast.node_count(&state.tree))
mem.set(execution.mapping[previous..], no_node)
return try execute_type_statement(state, execution, normalized)
}
root := assignment.target
while (true) {
kind :: ast.expr_kind(&state.tree, root)
@@ -2564,7 +2684,7 @@ expression_type proc(state @mut State, id ast.NodeId, depth usize) ast.NodeId !
if (base == no_node) return no_node
if (ast.expr_kind(&state.tree, base) == .pointer_type) base = try canonical_type(state, ast.pointer_type(&state.tree, base).element)
if (base == no_node) return no_node
member :: ast.source_text(&state.tree, ast.node_span(&state.tree, view.member))
member :: ast.literal_text(&state.tree, view.member)
base_kind :: ast.expr_kind(&state.tree, base)
if (
mem.eql(member, "len") and (