visitPropertyAccess method
Visit a SPropertyAccess.
Implementation
@override
Object? visitPropertyAccess(SPropertyAccess node) {
final target = node.target?.accept<Object?>(this);
if (target is AsyncSuspensionRequest) {
// Propagate suspension so the state machine resumes this node after resolution
return target;
}
// Determine if this is a conditional access by checking the operator field
final isNullAware = node.operator == '?.';
final propertyName = node.propertyName!.name;
// Null safety support: if the target is null and the access is null-aware, return null
if (target == null) {
if (isNullAware) {
return null;
}
// C21 — Dart null-shorting: when an inner selector in this chain uses
// `?.` (e.g. `a?.b.c.d` where `a == null`), every subsequent selector
// up to the chain's termination point must also yield null instead of
// throwing. The chain terminates at parentheses or non-selector
// expressions; the helper walks the syntactic target chain and stops
// there. Without this fix, `a?.b.c` throws "Cannot access property 'c'
// on null" because the outer `.c` only sees `operator == '.'`.
if (_chainHasNullAwareSelector(node.target)) {
return null;
}
// G-DOV-10/11 FIX: Try extension lookup on nullable types before throwing
final extensionMember = environment.findExtensionMember(
target,
propertyName,
visitor: this,
);
if (extensionMember != null) {
if (extensionMember is InterpretedFunction &&
extensionMember.isGetter) {
// Execute extension getter with 'this' bound to null
final extensionEnv = Environment(enclosing: environment);
extensionEnv.define('this', null);
final prevEnv = environment;
environment = extensionEnv;
try {
return extensionMember.call(this, [], {});
} finally {
environment = prevEnv;
}
}
return extensionMember;
}
throw RuntimeD4rtException(
"Cannot access property '$propertyName' on null. Use '?.' for null-aware access.",
);
}
Logger.debug(
"[SPropertyAccess: ${node.toString()}] Target type: ${target.runtimeType}, Target value: ${target.toString()}",
);
// GEN-100c: Handle prefixed import access (e.g., prefix.SomeClass.staticMember)
// When the AST produces SPropertyAccess with a prefix target that resolved to an Environment,
// delegate to that environment for member lookup.
if (target is Environment) {
Logger.debug(
"[SPropertyAccess] Target resolved to Environment (prefixed import). Looking up '$propertyName'.",
);
try {
final member = target.get(propertyName);
if (member is InterpretedFunction && member.isGetter) {
return member.call(this, [], {});
}
return member;
} on RuntimeD4rtException catch (e) {
throw RuntimeD4rtException(
"Undefined member '$propertyName' in prefixed import: ${e.message}",
);
}
}
if (target is InterpretedInstance) {
// Standard Instance Access: Try direct first, then extension
try {
final member = target.get(
propertyName,
visitor: this,
); // .get() handles inheritance
if (member is InterpretedFunction && member.isGetter) {
return member.call(
this,
[],
{},
); // .get already returned bound getter
} else {
return member; // field value or bound method
}
} on RuntimeD4rtException catch (e) {
// Try Extension Lookup Before Error
if (e.message.contains("Undefined property '$propertyName'")) {
Logger.debug(
"[SPropertyAccess] Direct access failed for '$propertyName'. Trying extension lookup on ${target.runtimeType}.",
);
try {
final extensionMember = environment.findExtensionMember(
target,
propertyName,
);
if (extensionMember is ExtensionMemberCallable) {
if (extensionMember.isGetter) {
Logger.debug(
"[SPropertyAccess] Found extension getter '$propertyName'. Calling...",
);
// Getters are called with the instance as the first (and only) positional argument
final extensionPositionalArgs = [target];
return extensionMember.call(this, extensionPositionalArgs, {});
} else if (!extensionMember.isOperator &&
!extensionMember.isSetter) {
// Return the extension method itself (it's not bound yet)
Logger.debug(
"[SPropertyAccess] Found extension method '$propertyName'. Returning callable.",
);
return extensionMember;
}
}
// No suitable extension found, fall through to rethrow original error
Logger.debug(
"[SPropertyAccess] No suitable extension member found for '$propertyName'.",
);
} on RuntimeD4rtException catch (findError) {
// Error during extension lookup itself
Logger.debug(
"[SPropertyAccess] Error during extension lookup for '$propertyName': ${findError.message}",
);
// Fall through to rethrow original error
}
}
// Rethrow original error if it wasn't "Undefined property"or if extension lookup failed
throw RuntimeD4rtException(
"${e.message} (accessing property via SPropertyAccess '$propertyName')",
);
}
} else if (target is InterpretedEnumValue) {
try {
// Get should execute the getter or return the field/bound method
// Pass the visitor to potentially execute getters
final member = target.get(propertyName, this);
// Check if the result from get was already the final value (field or executed getter)
// or if it's a bound method that shouldn't be called here.
if (member is Callable) {
// Property access should generally not return a raw callable method.
// If get() returned a bound method, it means the propertyName matched a method name,
// which is not what property access typically expects.
// However, Dart allows accessing methods like properties to get a tear-off.
// So, we return the bound method (Callable) here.
Logger.debug(
"[SPropertyAccess] Accessed enum method '$propertyName' on $target as tear-off. Returning bound method.",
);
return member;
} else {
// Must be a field value or the result of an executed getter.
Logger.debug(
"[SPropertyAccess] Accessed enum field/getter '$propertyName' on $target. Value: $member",
);
return member;
}
} on RuntimeD4rtException catch (e) {
// Try Extension Getter if Direct Fails (similar to InterpretedInstance)
if (e.message.contains("Undefined property '$propertyName'")) {
Logger.debug(
"[SPropertyAccess] Direct access failed for '$propertyName' on enum $target. Trying extension lookup...",
);
try {
final extensionMember = environment.findExtensionMember(
target,
propertyName,
);
if (extensionMember is ExtensionMemberCallable) {
if (extensionMember.isGetter) {
Logger.debug(
"[SPropertyAccess] Found extension getter '$propertyName' for enum. Calling...",
);
final extensionPositionalArgs = [target];
return extensionMember.call(this, extensionPositionalArgs, {});
} else if (!extensionMember.isOperator &&
!extensionMember.isSetter) {
Logger.debug(
"[SPropertyAccess] Found extension method '$propertyName' for enum. Returning tear-off.",
);
return extensionMember;
}
}
Logger.debug(
"[SPropertyAccess] No suitable extension member found for '$propertyName' on enum.",
);
} on RuntimeD4rtException catch (findError) {
Logger.debug(
"[SPropertyAccess] Error during extension lookup for '$propertyName' on enum: ${findError.message}",
);
}
}
// Rethrow original error or error from extension lookup
throw RuntimeD4rtException(
"${e.message} (accessing property via SPropertyAccess '$propertyName' on enum value '$target')",
);
}
} else if (target is InterpretedEnum) {
// Accessing static member on the enum itself
InterpretedFunction? staticGetter = target.staticGetters[propertyName];
if (staticGetter != null) {
// Call the static getter
return staticGetter.call(this, [], {});
}
Object? staticField = target.staticFields[propertyName];
if (target.staticFields.containsKey(propertyName)) {
// Return static field value (could be null)
return staticField;
}
InterpretedFunction? staticMethod = target.staticMethods[propertyName];
if (staticMethod != null) {
// Return the static method itself (tear-off)
return staticMethod;
}
// Check mixins for static members (reverse order)
for (final mixin in target.mixins.reversed) {
final mixinStaticGetter = mixin.findStaticGetter(propertyName);
if (mixinStaticGetter != null) {
Logger.debug(
"[SPropertyAccess] Found static getter '$propertyName' from mixin '${mixin.name}' for enum '${target.name}'",
);
return mixinStaticGetter.call(this, [], {});
}
final mixinStaticMethod = mixin.findStaticMethod(propertyName);
if (mixinStaticMethod != null) {
Logger.debug(
"[SPropertyAccess] Found static method '$propertyName' from mixin '${mixin.name}' for enum '${target.name}'",
);
return mixinStaticMethod;
}
// Check static fields - use try/catch since getStaticField throws if not found
try {
final mixinStaticField = mixin.getStaticField(propertyName);
Logger.debug(
"[SPropertyAccess] Found static field '$propertyName' from mixin '${mixin.name}' for enum '${target.name}'",
);
return mixinStaticField;
} on RuntimeD4rtException {
// Continue to next mixin
}
}
// Check for built-in 'values'
if (propertyName == 'values') {
return target.valuesList;
}
// Not found
throw RuntimeD4rtException(
"Undefined static property '$propertyName' on enum '${target.name}'.",
);
} else if (target is InterpretedClass) {
// Static Access (no change)
try {
// Check static fields first (no inheritance for static fields in Dart)
return target.getStaticField(propertyName);
} on RuntimeD4rtException catch (_) {
// If not a field, check static methods/getters
InterpretedFunction? staticMember = target.findStaticGetter(
propertyName,
);
staticMember ??= target.findStaticMethod(propertyName);
if (staticMember != null) {
if (staticMember.isGetter) {
return staticMember.call(this, [], {}); // Call static getter
} else {
// Return static method function itself (not bound)
return staticMember;
}
} else {
// Cluster C32/C33: class-as-value (Type literal) semantics. When
// a script binds a class identifier to a `Type` variable and
// reads `Object` getters off it (`hashCode`, `runtimeType`), the
// expected behaviour is `Object.hashCode` / `Object.runtimeType`
// on the Type instance, not a static-member lookup on the class.
// Fall back to the underlying Dart object's getters before
// reporting an undefined-static error.
if (propertyName == 'hashCode') {
return target.hashCode;
}
if (propertyName == 'runtimeType') {
return target.runtimeType;
}
throw RuntimeD4rtException(
"Undefined static member '$propertyName' on class '${target.name}'.",
);
}
}
} else if (target is BoundSuper) {
// Super Property Access
final instance = target.instance;
final startClass = target.startLookupClass;
InterpretedClass? currentClass =
startClass; // Start search from superclass
while (currentClass != null) {
// Check instance field in the bound instance's fields
// Use the new public getter on the instance to access the field value
try {
final fieldValue = instance.getField(propertyName);
// Field found on the instance, return its value regardless of where we are in the super hierarchy lookup
return fieldValue;
} on RuntimeD4rtException {
// Field doesn't exist directly on the instance, continue searching for getters/methods
}
// Check instance getter in the current class in hierarchy
final getter = currentClass.findInstanceGetter(propertyName);
if (getter != null) {
// Bind getter to the original instance and call
return getter.bind(instance).call(this, [], {});
}
// Check instance method (less common for property access, but possible)
final method = currentClass.findInstanceMethod(propertyName);
if (method != null) {
// Bind method to the original instance and return the bound method
return method.bind(instance);
}
// Move up the hierarchy
currentClass = currentClass.superclass;
}
// Not found in superclass hierarchy
throw RuntimeD4rtException(
"Undefined property '$propertyName' accessed via 'super' on instance of '${instance.klass.name}'.",
);
} else if (target is BridgedClass) {
final bridgedClass = target;
Logger.debug(
"[SPropertyAccess] Static access on BridgedClass: ${bridgedClass.name}.$propertyName",
);
final staticGetter = bridgedClass.findStaticGetterAdapter(propertyName);
if (staticGetter != null) {
Logger.debug("[SPropertyAccess] Found static getter adapter.");
return wrapNativeReturnValue(
staticGetter(this),
); // Call static getter adapter
}
final staticMethod = bridgedClass.findStaticMethodAdapter(propertyName);
if (staticMethod != null) {
Logger.debug("[SPropertyAccess] Found static method adapter.");
throw UnimplementedD4rtException(
"Returning bridged static methods as values from SPropertyAccess is not yet supported.",
);
// return BridgedStaticMethodCallable(bridgedClass, staticMethod, propertyName);
} else {
// Cluster C32: class-as-value (Type literal) semantics. A script
// that does `final Type t = SomeBridgedClass; t.hashCode;` expects
// `Object.hashCode` on the Type instance, not a static-member
// lookup on the bridged class. Fall back to the underlying Dart
// object's getters before reporting an undefined-static error.
if (propertyName == 'hashCode') {
return bridgedClass.hashCode;
}
if (propertyName == 'runtimeType') {
return bridgedClass.runtimeType;
}
throw RuntimeD4rtException(
"Undefined static member '$propertyName' on bridged class '${bridgedClass.name}'.",
);
}
} else if (toBridgedInstance(target).$2) {
final bridgedInstance = toBridgedInstance(target).$1!;
Logger.debug(
"[SPropertyAccess] Access on BridgedInstance: ${bridgedInstance.bridgedClass.name}.$propertyName",
);
// GEN-075: Use nativeObject for Object properties
switch (propertyName) {
case 'runtimeType':
return bridgedInstance.nativeObject.runtimeType;
case 'hashCode':
return bridgedInstance.nativeObject.hashCode;
default:
}
final getterAdapter = bridgedInstance.bridgedClass
.findInstanceGetterAdapter(propertyName);
if (getterAdapter != null) {
Logger.debug("[SPropertyAccess] Found instance getter adapter.");
return getterAdapter(
this,
bridgedInstance.nativeObject,
); // Call instance getter adapter
}
final methodAdapter = bridgedInstance.bridgedClass
.findInstanceMethodAdapter(propertyName);
if (methodAdapter != null) {
Logger.debug(
"[SPropertyAccess] Found instance method adapter. Binding...",
);
// Return a callable bound to the instance
return BridgedMethodCallable(
bridgedInstance,
methodAdapter,
propertyName,
);
}
// Cluster-12 (priority 3): Walk the registered supertype chain when
// the leaf bridge has no matching getter/method. See
// [InterpreterVisitorExtension.lookupOnBridgedSupertypes].
final supertypeMatch =
lookupOnBridgedSupertypes(bridgedInstance, propertyName);
if (supertypeMatch.$2) {
Logger.debug(
"[SPropertyAccess] Resolved '$propertyName' via supertype walk on '${bridgedInstance.bridgedClass.name}'.",
);
return supertypeMatch.$1;
}
// Try extension lookup before throwing error
Logger.debug(
"[SPropertyAccess] Direct access failed for '$propertyName' on BridgedInstance. Trying extension lookup...",
);
final extensionMember = environment.findExtensionMember(
bridgedInstance,
propertyName,
);
if (extensionMember is ExtensionMemberCallable) {
if (extensionMember.isGetter) {
Logger.debug(
"[SPropertyAccess] Found extension getter '$propertyName' for BridgedInstance. Calling...",
);
// Getters are called with the native object as the first positional argument
final extensionPositionalArgs = [bridgedInstance.nativeObject];
return extensionMember.call(this, extensionPositionalArgs, {});
} else if (!extensionMember.isOperator && !extensionMember.isSetter) {
Logger.debug(
"[SPropertyAccess] Found extension method '$propertyName' for BridgedInstance. Returning tear-off.",
);
return extensionMember;
}
}
// Fix I: Check if the nativeObject is actually an Enum
if (bridgedInstance.nativeObject is Enum) {
final enumObj = bridgedInstance.nativeObject as Enum;
// Fast path: built-in enum members. Keep this BEFORE the
// BridgedEnumValue lookup — the lookup is O(N*M) over all registered
// bridged enums and would dominate hot paths like `.name`/`.index`.
switch (propertyName) {
case 'name':
return enumObj.name;
case 'index':
return enumObj.index;
case 'hashCode':
return enumObj.hashCode;
case 'runtimeType':
return enumObj.runtimeType;
case 'toString':
return NativeFunction(
(visitor, args, namedArgs, typeArgs) => enumObj.toString(),
arity: 0,
name: 'toString',
);
}
// Cluster-26 (Key.label dispatch): If the enum has a registered
// BridgedEnumValue, dispatch through it so custom getters (e.g.
// KeyEventType.label) registered on the BridgedEnumDefinition resolve.
// The G-DCLI-05 prefix match in toBridgedClass can otherwise wrap a
// native enum (KeyEventType) under an unrelated BridgedClass (Key).
// Walks the current scope chain (which extends from the per-module
// env where bridges register the BridgedEnum). Falls back to the
// global env for runners that pre-populate enums there.
final bridgedEnumValue =
environment.getBridgedEnumValue(enumObj) ??
globalEnvironment.getBridgedEnumValue(enumObj);
if (bridgedEnumValue != null) {
try {
return bridgedEnumValue.get(propertyName);
} on RuntimeD4rtException {
// Fall through to "Undefined property" error below.
}
}
}
// D2: If the bridged instance wraps a D4InterpretedProxy (a native
// proxy that holds a back-reference to the originating
// [InterpretedInstance]), retry the property access on the wrapped
// instance. Used for proxies like D4rtCustomPainter that route
// bridged-callback dispatch to script-defined fields/getters
// (e.g. `progress`) declared on the script's CustomPainter subclass.
final native = bridgedInstance.nativeObject;
if (native is D4InterpretedProxy) {
final inner = native.d4rtInstance;
if (inner is InterpretedInstance) {
try {
return inner.get(propertyName, visitor: this);
} catch (_) {
// Fall through to the "Undefined property" error below.
}
}
}
throw RuntimeD4rtException(
"Undefined property or method '$propertyName' on bridged instance of '${bridgedInstance.bridgedClass.name}'.",
);
} else if (target is InterpretedRecord) {
// Accessing field of a record
final record = target;
Logger.debug(
"[SPropertyAccess] Access on InterpretedRecord: .$propertyName",
);
// Check if it's a positional field access (\$1, \$2, ...)
if (propertyName.startsWith('\$') && propertyName.length > 1) {
try {
final index = int.parse(propertyName.substring(1)) - 1;
if (index >= 0 && index < record.positionalFields.length) {
return record.positionalFields[index];
} else {
throw RuntimeD4rtException(
"Record positional field index \$$index out of bounds (0..${record.positionalFields.length - 1}).",
);
}
} catch (e) {
// Handle parse errors or other issues
throw RuntimeD4rtException(
"Invalid positional record field accessor '$propertyName'.",
);
}
} else {
// Check if it's a named field access
if (record.namedFields.containsKey(propertyName)) {
return record.namedFields[propertyName];
} else {
throw RuntimeD4rtException(
"Record has no field named '$propertyName'. Available fields: ${record.namedFields.keys.join(', ')}",
);
}
}
} else if (target is Record) {
// E6: native Dart Record (e.g., produced by `Iterable.indexed`).
// Route positional access via dynamic dispatch — see
// _accessNativeRecordField for the limitations.
return _accessNativeRecordField(target, propertyName);
} else if (target is BridgedEnumValue) {
return target.get(propertyName);
} else if (target is BridgedEnum) {
Logger.debug(
"[SPropertyAccess] Accessing value on BridgedEnum: ${target.name}.$propertyName",
);
final enumValue = target.getValue(propertyName);
if (enumValue != null) {
return enumValue; // Return the BridgedEnumValue
} else {
throw RuntimeD4rtException(
"Undefined enum value '$propertyName' on bridged enum '${target.name}'.",
);
}
} else if (target is BoundBridgedSuper) {
final instance = target.instance; // The interpreted 'this' instance
final bridgedSuper = target.startLookupClass;
// RC-6: Use nativeProxy as fallback when bridgedSuperObject is null.
// This supports abstract class adapters (like _InterpretedState for State).
final nativeSuperObject =
instance.bridgedSuperObject ?? instance.nativeProxy;
if (nativeSuperObject == null) {
throw RuntimeD4rtException(
"Internal error: Cannot access super property '$propertyName' on bridged superclass '${bridgedSuper.name}' because the native super object is missing.",
);
}
// Try the bridged getter
final getterAdapter = bridgedSuper.findInstanceGetterAdapter(
propertyName,
);
if (getterAdapter != null) {
try {
return getterAdapter(this, nativeSuperObject);
} catch (e, s) {
Logger.error(
"Native exception during super access to bridged getter '${bridgedSuper.name}.$propertyName': $e\n$s",
);
throw RuntimeD4rtException(
"Native error during super access to bridged getter '$propertyName': $e",
originalException: e,
);
}
}
// Try the bridged method (for tear-off)
final methodAdapter = bridgedSuper.findInstanceMethodAdapter(
propertyName,
);
if (methodAdapter != null) {
// Return a callable bound to the native object
return BridgedSuperMethodCallable(
nativeSuperObject,
methodAdapter,
propertyName,
bridgedSuper.name,
);
}
// Not found
throw RuntimeD4rtException(
"Undefined property or method '$propertyName' accessed via 'super' on bridged superclass '${bridgedSuper.name}'.",
);
} else if (target is Callable) {
// ENG-006: Handle property access on function objects (closures, NativeFunctions)
Logger.debug(
"[SPropertyAccess] Access on Callable: ${target.runtimeType}.$propertyName",
);
switch (propertyName) {
case 'runtimeType':
return target.runtimeType;
case 'hashCode':
return target.hashCode;
case 'toString':
return NativeFunction(
(visitor, args, namedArgs, typeArgs) => target.toString(),
arity: 0,
name: 'toString',
);
case 'call':
return target; // Tear-off
default:
throw RuntimeD4rtException(
"Undefined property '$propertyName' on function object (${target.runtimeType}).",
);
}
} else {
// Check if target is a native enum that has been bridged
final bridgedEnumValue = environment.getBridgedEnumValue(target);
if (bridgedEnumValue != null) {
Logger.debug(
"[SPropertyAccess] Found bridged enum value for native enum ${target.runtimeType}",
);
try {
return bridgedEnumValue.get(propertyName);
} catch (e) {
throw RuntimeD4rtException(
"Undefined property '$propertyName' on bridged enum value '${bridgedEnumValue.name}'.",
);
}
}
// Fix I: Generic Enum property access for raw Enum targets
if (target is Enum) {
switch (propertyName) {
case 'name':
return (target).name;
case 'index':
return (target).index;
case 'hashCode':
return target.hashCode;
case 'runtimeType':
return target.runtimeType;
case 'toString':
return NativeFunction(
(visitor, args, namedArgs, typeArgs) => target.toString(),
arity: 0,
name: 'toString',
);
}
}
Logger.debug(
"[SPropertyAccess] Looking for extension getter '$propertyName' for target type ${target.runtimeType}.",
);
final extensionCallable = environment.findExtensionMember(
target,
propertyName,
);
if (extensionCallable is ExtensionMemberCallable &&
extensionCallable.isGetter) {
Logger.debug(
"[SPropertyAccess] Found extension getter '$propertyName'. Calling...",
);
// Prepend the target instance to the positional arguments for the extension call
final extensionPositionalArgs = [
target,
]; // Getters take no explicit args
try {
// Call the extension getter
return extensionCallable.call(this, extensionPositionalArgs, {});
} on ReturnException catch (e) {
return e.value;
} catch (e) {
throw RuntimeD4rtException(
"Error executing extension getter '$propertyName': $e",
);
}
} else {
// GEN-C3c: Universal Object-member fallback for arbitrary native
// targets. Every Dart Object has `toString`, `hashCode`, and
// `runtimeType`; these must always resolve regardless of whether the
// runtime type is bridged or not. Mirrors the BridgedInstance branch
// (GEN-075) above and the Callable branch at ENG-006. `target` is
// non-null here — the early-return at the top of visitPropertyAccess
// handles the null-receiver case.
switch (propertyName) {
case 'hashCode':
return target.hashCode;
case 'runtimeType':
return target.runtimeType;
case 'toString':
return NativeFunction(
(visitor, args, namedArgs, typeArgs) => target.toString(),
arity: 0,
name: 'toString',
);
}
// No extension getter found either, rethrow the original stdlib error
Logger.debug(
"[SPropertyAccess] Extension getter '$propertyName' not found. Rethrowing original error.",
);
throw RuntimeD4rtException(
"Undefined property or method '$propertyName' on ${target.runtimeType}.",
);
}
}
}