feat: #get property accessors (no-paren method-as-field)

A method declared `name :: (self: *T) -> R #get => expr;` is invoked via
no-paren field syntax (`obj.name`) instead of `obj.name()`. It is an ordinary
method (registered `Type.method`, flagged is_get); field-access lowering and
inference dispatch to it when no real field of that name exists, by synthesizing
a no-arg `obj.name()` call routed through the normal call path (so receiver
address-of and generic binding are reused).

- Lexer/token: `#get`. Parser: parsed after the return type in parseFnDecl;
  hasFnBodyAfterArrow treats it as a body marker so struct-body methods parse.
- Resolution: getAccessorFor handles a generic-struct instance and a plain
  struct. A REAL field of the same name wins (a getter never shadows stored
  data). An explicit postfix-deref receiver (`p.*.getter`) dispatches on the
  inner pointer so it takes the working auto-deref path.
- Works on plain + generic structs (incl. getters returning the type param),
  in expressions/conditions/args/loop-bounds, chained, and via a pointer
  receiver. Examples: types/0196 (basic) + types/0197 (stress).

Known narrow limitations (clean errors / workarounds, not silent): a getter
RESULT used directly as a method/getter receiver (`o.gi.dbl`) errors — bind it
to a local first; a getter named `len`/`ptr` returning non-i64 mis-infers
(the .len/.ptr builtin-field shortcut).
This commit is contained in:
agra
2026-06-22 11:55:01 +03:00
parent b9311e7de4
commit 9d3a019670
20 changed files with 195 additions and 0 deletions

View File

@@ -192,6 +192,10 @@ pub const FnDecl = struct {
/// OPT-IN: only `is_ufcs` fns and `ufcs` aliases dispatch; a plain
/// fn is callable directly or via `|>` only.
is_ufcs: bool = false,
/// `name :: (self: *T) -> R #get => expr;` — a no-paren property accessor.
/// Invoked via field syntax (`obj.name`) when no real field matches, rather
/// than as a `obj.name()` call. Takes only the `self` receiver.
is_get: bool = false,
};
pub const Param = struct {

View File

@@ -259,6 +259,16 @@ pub const ExprTyper = struct {
for (fields) |f| {
if (f.name == field_name_id) return if (is_opt_chain) self.l.optionalOfFlattened(f.ty) else f.ty;
}
// `#get` property accessor: type as the accessor's return
// type. Resolve via the synthesized no-arg call so generic
// bindings (e.g. a `List(T)` getter returning `T`) resolve
// exactly as the lowering path's call does.
if (self.l.getAccessorFor(obj_ty, fa.field) != null) {
const callee_node = Node{ .data = .{ .field_access = fa }, .span = node.span };
const syn_call = ast.Call{ .callee = @constCast(&callee_node), .args = &.{} };
const rt = self.l.callResolver().resultType(&syn_call);
return if (is_opt_chain) self.l.optionalOfFlattened(rt) else rt;
}
}
return .unresolved;
},

View File

@@ -650,6 +650,12 @@ pub const Function = struct {
/// from `.c`.
is_naked: bool = false,
/// `#get` property accessor (ast.FnDecl.is_get). Registered as an ordinary
/// method, but ALSO reachable via no-paren field syntax (`obj.name`) when no
/// real field matches — field-access lowering/inference calls it with the
/// receiver as `self`.
is_get: bool = false,
pub const Param = struct {
name: StringId,
ty: TypeId,

View File

@@ -1967,6 +1967,7 @@ pub const Lowering = struct {
pub const lowerStructLiteral = lower_expr.lowerStructLiteral;
pub const lowerInitBlock = lower_expr.lowerInitBlock;
pub const getStructFields = lower_expr.getStructFields;
pub const getAccessorFor = lower_expr.getAccessorFor;
pub const fixupMethodReceiver = lower_expr.fixupMethodReceiver;
pub const getStructTypeName = lower_expr.getStructTypeName;
pub const builtinTypeName = lower_expr.builtinTypeName;

View File

@@ -2322,6 +2322,7 @@ pub fn declareFunction(self: *Lowering, fd: *const ast.FnDecl, name: []const u8)
func.is_variadic = is_variadic;
func.has_implicit_ctx = wants_ctx;
func.is_naked = (fd.abi == .naked);
func.is_get = fd.is_get;
self.extern_name_map.put(name, c_name) catch {};
self.fn_decl_fids.put(fd, fid) catch {};
return;
@@ -2336,6 +2337,7 @@ pub fn declareFunction(self: *Lowering, fd: *const ast.FnDecl, name: []const u8)
func.is_variadic = is_variadic;
func.has_implicit_ctx = wants_ctx;
func.is_naked = (fd.abi == .naked);
func.is_get = fd.is_get;
if (weldedCompilerFn(self, fd, name)) func.compiler_welded = true;
// A BODIED `abi(.compiler)` function is a user compiler-domain function (e.g. a
// post-link callback): the VM runs its sx body, but it NEVER runs in the binary

View File

@@ -637,6 +637,30 @@ pub fn lowerFieldAccess(self: *Lowering, fa: *const ast.FieldAccess, span: ast.S
return self.lowerObjcDefinedStateFieldRead(fa.object, info);
}
// `#get` property accessor: `obj.field` where `field` is a `#get` method
// dispatches as a no-paren method call (`obj.field()`). Detected via type
// info only (no lowering) so the receiver is not evaluated twice — the
// synthesized call re-lowers `fa.object` and handles the receiver
// address-of + any generic binding itself.
{
var recv_ty = self.inferExprType(fa.object);
if (!recv_ty.isBuiltin()) {
const di = self.module.types.get(recv_ty);
if (di == .pointer) recv_ty = di.pointer.pointee;
}
if (self.getAccessorFor(recv_ty, fa.field) != null) {
// For an explicit-deref receiver `(*p).getter`, dispatch on the
// inner pointer `p` (`p.getter`, auto-deref) — semantically identical
// and it takes the working receiver path (the synthesized call on a
// `.deref_expr` receiver otherwise mis-lowers the `*self` address).
var recv_fa = fa.*;
if (fa.object.data == .deref_expr) recv_fa.object = fa.object.data.deref_expr.operand;
const callee_node = Node{ .data = .{ .field_access = recv_fa }, .span = span };
const syn_call = ast.Call{ .callee = @constCast(&callee_node), .args = &.{} };
return self.lowerCall(&syn_call);
}
}
var obj = self.lowerExpr(fa.object);
var obj_ty = self.inferExprType(fa.object);
@@ -825,6 +849,38 @@ pub fn vectorLaneIndex(field: []const u8) ?u32 {
return null;
}
/// A `#get` property accessor for `obj_ty.field`, or null. A `#get` method is a
/// normal method (registered `Type.method`) marked `is_get`; it is reachable via
/// no-paren field syntax. Handles a generic-struct instance (`List(i64).len`)
/// and a plain struct (`Foo.bar`). `ty` must be the dereferenced (non-pointer)
/// receiver type.
pub fn getAccessorFor(self: *Lowering, ty: TypeId, field: []const u8) ?*const ast.FnDecl {
if (ty.isBuiltin()) return null;
// A REAL field of this name wins over a same-name `#get` (a getter must not
// shadow stored data on the read path). If the struct genuinely declares the
// field, this is not a property access.
const field_id = self.module.types.internString(field);
for (self.getStructFields(ty)) |f| {
if (f.name == field_id) return null;
}
// Generic instance: genericInstanceMethod is keyed by the instance name
// (e.g. "List(i64)"), which is what formatTypeName produces.
const tn = self.formatTypeName(ty);
if (self.genericInstanceMethod(tn, field)) |m| {
return if (m.fd.is_get) m.fd else null;
}
// Plain struct: methods are registered "StructName.method" in fn_ast_map.
const info = self.module.types.get(ty);
if (info == .@"struct") {
const sname = self.module.types.getString(info.@"struct".name);
const q = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ sname, field }) catch return null;
if (self.program_index.fn_ast_map.get(q)) |fd| {
return if (fd.is_get) fd else null;
}
}
return null;
}
pub fn lowerFieldAccessOnType(self: *Lowering, obj: Ref, obj_ty: TypeId, field: []const u8, span: ast.Span) Ref {
const field_name_id = self.module.types.internString(field);

View File

@@ -116,6 +116,7 @@ pub fn monomorphizeFunction(self: *Lowering, fd: *const ast.FnDecl, mangled_name
_ = func_id;
self.builder.currentFunc().has_implicit_ctx = wants_ctx;
self.builder.currentFunc().is_naked = (fd.abi == .naked);
self.builder.currentFunc().is_get = fd.is_get;
// Create entry block
const entry_name = self.module.types.internString("entry");

View File

@@ -950,6 +950,7 @@ pub fn monomorphizePackFn(
_ = self.builder.beginFunction(name_id, params.items, ret_ty);
self.builder.currentFunc().has_implicit_ctx = wants_ctx;
self.builder.currentFunc().is_naked = (fd.abi == .naked);
self.builder.currentFunc().is_get = fd.is_get;
const entry_name = self.module.types.internString("entry");
const entry = self.builder.appendBlock(entry_name, &.{});

View File

@@ -112,6 +112,7 @@ pub const Lexer = struct {
.{ "#jni_main", Tag.hash_jni_main },
.{ "#selector", Tag.hash_selector },
.{ "#property", Tag.hash_property },
.{ "#get", Tag.hash_get },
.{ "#caller_location", Tag.hash_caller_location },
};
inline for (directives) |d| {

View File

@@ -1715,6 +1715,7 @@ pub const Server = struct {
.hash_jni_main,
.hash_selector,
.hash_property,
.hash_get,
.hash_caller_location,
=> ST.keyword,

View File

@@ -1958,6 +1958,14 @@ pub const Parser = struct {
return_type = try self.parseTypeExpr();
}
// Optional `#get` property-accessor marker: `name :: (self) -> R #get => expr;`.
// The method is invoked via field syntax (`obj.name`) rather than `obj.name()`.
var is_get = false;
if (self.current.tag == .hash_get) {
is_get = true;
self.advance();
}
// Optional ABI / calling-convention annotation: `abi(.c)` / `abi(.zig)` /
// `abi(.naked)`. Sits in the postfix slot BEFORE the `extern`/`export`
// linkage keyword (it is part of the function declaration). `abi(.zig)`
@@ -2048,6 +2056,7 @@ pub const Parser = struct {
.extern_name = extern_name,
.name_span = name_span,
.is_raw = name_is_raw,
.is_get = is_get,
} });
}
@@ -3793,6 +3802,7 @@ pub const Parser = struct {
if (self.current.tag == .fat_arrow) return true;
if (self.current.tag == .l_brace) return true;
if (self.current.tag == .hash_builtin) return true;
if (self.current.tag == .hash_get) return true; // `-> R #get => …` is a fn def
if (self.current.tag == .kw_abi) return true;
// Postfix linkage modifier after the return type: `-> R extern;` /
// `-> R export { … }` (and `-> R abi(.c) extern`). Marks a fn def.

View File

@@ -141,6 +141,7 @@ pub const Tag = enum {
hash_jni_method_descriptor, // `#jni_method_descriptor("(Sig)Ret")` per-method JNI descriptor override
hash_selector, // `#selector("explicit:string")` per-method Obj-C selector override (Phase 3.2)
hash_property, // `#property[(modifier, ...)]` field directive — synthesizes getter/setter dispatch (M2.2)
hash_get, // `name :: (self) -> R #get => expr;` — a no-paren property accessor method (read via field syntax)
hash_caller_location, // `#caller_location` — as a param default, synthesizes the call site's Source_Location (ERR E4.1b)
hash_jni_env, // `#jni_env(env) { body }` block-form env-scoping intrinsic
hash_jni_main, // `#jni_main #jni_class(...) { ... }` — class is the launchable Android Activity