renderInterpretedToString function

String renderInterpretedToString({
  1. required RuntimeValue target,
  2. required InterpretedFunction? method,
  3. required InterpreterVisitor? visitor,
  4. required String fallback(),
})

Dispatches to a script's toString() when there is one, and falls back to fallback otherwise.

SCD72 wrote this for InterpretedInstance and SCE116 shared it with InterpretedEnumValue and InterpretedExtensionTypeInstance, which had the same defect in the same file. Every line is here for a measured reason and the reasons apply unchanged to all three; a third and fourth copy would have been three and four places for the next correction to land in.

THIS FUNCTION DOES NOT THROW FOR ANYTHING RECOVERABLE, and that is a deliberate divergence from callInterpretedToString, which is what stringify uses and must keep Dart's semantics. This is the one HOST code reaches, including an onUncaughtError hook whose first act is to log the error. A second exception raised while reporting the first is worse than an imperfect string, which is the same reasoning behind the SDK's own Error.safeToString.

StackOverflowError AND OutOfMemoryError ARE RETHROWN, and the reason is measured rather than principled. SCD72's first draft caught everything, and a pair of mutually-interpolating objects (A.toString() => 'A:$b', B.toString() => 'B:$a') stopped raising StackOverflowError and started HANGING: the overflow unwound into this catch, the fallback was returned, the caller resumed on a stack that was still full, and it overflowed again — forever. Swallowing an unrecoverable VM error turns a fast crash into a livelock, which is the one outcome worse than the exception. Those two propagate; everything a script can actually do is caught.

fallback is a callback rather than a string because computing it can itself be work — an extension type's is its representation's own toString() — and the override path must not pay for it.

Implementation

String renderInterpretedToString({
  required RuntimeValue target,
  required InterpretedFunction? method,
  required InterpreterVisitor? visitor,
  required String Function() fallback,
}) {
  if (method == null) return fallback();
  // No visitor means no way to run interpreted code. It is stored on the
  // declaring TYPE rather than taken from the ambient `D4.activeVisitor`,
  // which SCD72 measured to be null inside an `onUncaughtError` hook.
  if (visitor == null) return fallback();
  if (!_renderingToString.add(target)) return fallback();
  try {
    final result = callInterpretedToString(method, target, visitor);
    if (result is String) return result;
    if (result == null) return fallback();
    return result.toString();
  } on StackOverflowError {
    rethrow;
  } on OutOfMemoryError {
    rethrow;
  } catch (_) {
    // Everything else: a script `toString` that throws, a bridge failure deep
    // inside it, a cycle this guard did not see. All of it degrades to the
    // fallback rather than escaping into whatever the host was doing.
    return fallback();
  } finally {
    _renderingToString.remove(target);
  }
}