lookup method
Non-throwing variant of get: resolves name across the lexical chain
(locals, bridged classes/enums, prefixed imports) and returns kNotFound
instead of throwing when nothing matches.
Why this exists: the lexical → implicit-this fallback in
InterpreterVisitor.visitSimpleIdentifier runs on every bare-identifier
read. For an instance-member reference (a field/getter named without
this.) the lexical probe necessarily misses, and the old throwing get
turned that routine miss into a thrown+caught RuntimeD4rtException with a
captured stack trace — thousands per second under an animation loop,
dominating both CPU and allocation. Returning a sentinel keeps the miss on
the normal return path. (The rare malformed-prefix errors below still
throw — they are genuine errors, not "not found".)
Implementation
Object? lookup(String name) {
// Env.get is the single hottest interpreter path (perf plan round 2, T1).
// Two structural choices keep it cheap:
// 1. Walk the enclosing chain ITERATIVELY rather than via `get` recursion,
// so each scope level costs one loop turn instead of a virtual call
// plus re-entry of the per-level guards below.
// 2. Probe `_values` ONCE on the local-hit path: read the value first and
// only fall back to `containsKey` when the read is null (to tell a key
// mapped to null from an absent key). The common non-null hit pays a
// single hash instead of `containsKey` + `[]`.
Environment? env = this;
while (env != null) {
if (Logger.isDebug) {
Logger.debug(
'[Env.get] Attempting to get "$name" in env: ${env.hashCode}',
); // Log attempt + env hash
}
// Prefixed-import resolution only applies when this environment actually
// holds prefixed imports — true only at import scopes (typically the
// global env), never at the function/block/loop scopes walked on the hot
// path. The `isNotEmpty` length-check is O(1) and lets us skip a
// `containsKey` hash plus a `name.contains('.')` scan on every lookup.
if (env._prefixedImports.isNotEmpty) {
// Check first if the name directly corresponds to a prefixed import.
if (env._prefixedImports.containsKey(name)) {
if (Logger.isDebug) {
Logger.debug(
"[Env.get] Name '$name' corresponds to a prefixed import. Returning the prefixed environment.",
);
}
return env._prefixedImports[name]; // Return the Environment itself.
}
// Check if it's a prefixed access (ex: math.pi)
if (name.contains('.')) {
final parts = name.split('.');
if (parts.length == 2) {
final prefix = parts[0];
final identifier = parts[1];
if (env._prefixedImports.containsKey(prefix)) {
if (Logger.isDebug) {
Logger.debug(
"[Env.get] Prefixed access for '$name'. Searching for '$identifier' in the prefixed environment '$prefix'.",
);
}
// Recursive call on the stored environment for the prefix.
// No need to check _enclosing here, the prefixed environment will do that.
try {
return env._prefixedImports[prefix]!.get(identifier);
} on RuntimeD4rtException catch (e) {
// If the identifier is not found in the prefixed environment, we want the original error to be propagated.
// Or, according to the desired semantics, we could raise a new error indicating that 'identifier' was not found IN 'prefix'.
throw RuntimeD4rtException(
"Undefined name '$identifier' in imported prefix '$prefix'. Original error: ${e.message}",
);
}
} else {
// The prefix itself is not found as a prefixed import.
// We could fall into the normal search if 'prefix.identifier' is a valid variable name.
// However, in Dart, an identifier cannot contain a '.' except for access.
// So, if the prefix is not in _prefixedImports, it's an error.
if (Logger.isDebug) {
Logger.debug(
"[Env.get] Prefix '$prefix' for '$name' not found in prefixed imports.",
);
}
}
} else {
// Handle the case of multiple points, for example a.b.c. For now, we only support prefix.identifier.
Logger.warn(
"[Env.get] Name '$name' contains multiple points, not supported for simple prefixed access.",
);
// Falling into the normal search could be an option, but let's raise an error for now
// because it probably indicates an unexpected usage or an invalid variable name.
throw RuntimeD4rtException(
"Complex prefixed access not supported: $name. Use the form prefix.identifier.",
);
}
}
}
// Normal search. Single probe: read once, only re-check `containsKey`
// when the read returned null (key-mapped-to-null vs absent).
final localValue = env._values[name];
if (localValue != null || env._values.containsKey(name)) {
if (Logger.isDebug) {
Logger.debug(
'[Env.get] Found \'$name\' locally in env: ${env.hashCode}',
);
}
// Unwrap GlobalGetter for lazy evaluation
if (localValue is GlobalGetter) {
return localValue();
}
return localValue;
}
// Bridged classes/enums are registered only at import/global scopes, so
// these maps are empty at the function/block/loop scopes traversed on the
// hot chain walk. The O(1) `isNotEmpty` guard skips a `containsKey` hash
// at every such intermediate level.
if (env._bridgedClasses.isNotEmpty &&
env._bridgedClasses.containsKey(name)) {
if (Logger.isDebug) {
Logger.debug(
" [Env.get] Found bridged class '$name' locally in env: ${env.hashCode}",
);
}
return env._bridgedClasses[name];
}
// Check for bridged enums
if (env._bridgedEnums.isNotEmpty && env._bridgedEnums.containsKey(name)) {
if (Logger.isDebug) {
Logger.debug(
" [Env.get] Found bridged enum '$name' locally in env: ${env.hashCode}",
);
}
return env._bridgedEnums[name];
}
// Move up the lexical scope chain (iterative — no recursion).
final parent = env._enclosing;
if (Logger.isDebug && parent != null) {
Logger.debug(
'[Env.get] Looking for \'$name\' in parent env: ${parent.hashCode}',
);
}
env = parent;
}
if (Logger.isDebug) {
Logger.debug(
'[Env.get] \'$name\' not found in env chain starting from: $hashCode (no parent)',
); // Log chain end
}
return kNotFound;
}