visitVariableDeclarationList method
Visit a SVariableDeclarationList.
Implementation
@override
Object? visitVariableDeclarationList(SVariableDeclarationList node) {
// We need to ensure variables are defined in the current environment.
// visitVariableDeclaration is NOT automatically called for node.variables
// by the generalizing visitor when visiting the list itself.
for (final variable in node.variables) {
if (variable.name!.name == '_') {
// Evaluate initializer for potential side effects, but don't define
variable.initializer?.accept<Object?>(this);
} else {
// Check if this is a late variable
final isLate = node.isLate;
final isFinal = node.isFinal;
final variableName = variable.name!.name;
// S3c (plan_3 §4.6 / §9.3): a slotted local dual-writes into BOTH the
// frame's slot array and the name map [_values]. Resolved reads go
// through [Environment.getSlot] (see [visitSimpleIdentifier]) — the hot
// path that previously paid the per-level name hash — while name-based
// resolution paths the interpreter still uses for the SAME local (the
// function-invocation callee lookup `environment.get(name)` for
// tear-offs / bridged-static / extension-call values) keep working
// against [_values]. Full demotion out of [_values] is deferred until
// every name-read site is audited (plan_3 §11). The slot index lives on
// the mirror node itself ([SVariableDeclaration.declSlot]) — no Expando,
// so it survives serialization.
final declSlot = variable.declSlot;
void def(Object? v) {
environment.define(variableName, v);
if (declSlot != null) {
environment.defineSlot(declSlot, variableName, v);
}
}
// SCE103: the fourth and last binding site. `int x = 'two';` bound the
// String; so did every later `x = 'two';`, and so did the for-each
// IDENTIFIER form `for (x in xs)`, which is an assignment to an
// already-declared variable and therefore the same job.
//
// Resolved ONCE per declaration and handed to the environment, because
// an assignment does not carry the annotation — the declaration is the
// only place the written type exists. `resolveBinding` returns null for
// everything it cannot answer confidently (unannotated, `dynamic`, a
// type parameter, an annotation that will not resolve), and a null
// binding is never recorded, so an untyped local costs one map probe at
// declaration and nothing afterwards.
final declaredBinding = InterpretedFunction.resolveBinding(
environment,
node.type,
describedAs: variableName,
);
// LATE IS OUT OF SCOPE, deliberately. A late declaration stores a
// `LateVariable` wrapper rather than the value, so the check here would
// reject the wrapper, and the moment the initializer actually runs is
// not an assignment this can see. Recording nothing leaves late locals
// exactly as permissive as they are today rather than half-checked.
if (declaredBinding != null && !isLate) {
environment.declareType(variableName, declaredBinding);
}
if (isLate) {
// Handle late variable
if (variable.initializer != null) {
// Late variable with lazy initializer
final lateVar = LateVariable(variableName, () {
// Create a closure that will evaluate the initializer when accessed
return variable.initializer!.accept<Object?>(this);
}, isFinal: isFinal);
def(lateVar);
Logger.debug(
"[VariableDeclList] Defined late variable '$variableName' with lazy initializer.",
);
} else {
// Late variable without initializer
final lateVar = LateVariable(variableName, null, isFinal: isFinal);
def(lateVar);
Logger.debug(
"[VariableDeclList] Defined late variable '$variableName' without initializer.",
);
}
} else {
// Regular (non-late) variable handling
Object? initValue;
Object? result; // Value returned by accept() (could be suspension)
if (variable.initializer != null) {
result = variable.initializer!.accept<Object?>(this);
if (result is AsyncSuspensionRequest) {
// Async initializer: Define as null for now, result holds suspension
Logger.debug(
"[VariableDeclList] Async init for '$variableName'. Defined as null.",
);
def(null);
// Propagate the suspension request.
// If there are multiple async inits, the LAST suspension request wins.
} else {
// Sync initializer: Use the computed value
// SCE103: checked HERE rather than inside `def`, because `def` is
// also the path for `int x;` and for an async initializer that
// has not produced its value yet — both of which define null, and
// neither of which is a write real Dart would reject. The
// declared type is still recorded for both, so the assignment
// that eventually supplies the value is held.
initValue = declaredBinding == null
? result
: declaredBinding.bind(environment, result);
if (Logger.isDebug) {
Logger.debug(
"[VariableDeclList] Sync init for '$variableName'. Defined as $initValue.",
);
}
def(initValue);
}
} else {
// No initializer: Define as null
Logger.debug(
"[VariableDeclList] No init for '$variableName'. Defined as null.",
);
def(null);
result = null; // No suspension
}
// If the result of the variable's init was a suspension, we need to return it
// to signal the state machine.
if (result is AsyncSuspensionRequest) {
// If any variable initialization caused suspension, return that suspension immediately.
// The state machine needs to handle this before processing subsequent variables.
Logger.debug(
"[VariableDeclList] Propagating suspension from initializer of '$variableName'.",
);
return result;
}
}
}
}
// If no suspension occurred (or only sync initializers for all variables)
return null; // Original behavior
}