[Bug] List.map throws UnimplementedError when passed a script-side closure
Test case:
import 'package:dart_eval/dart_eval.dart';
import 'package:dart_eval/dart_eval_bridge.dart';
import 'package:dart_eval/stdlib/core.dart';
void main() async {
const scriptContent = '''
Function main() {
return (items) => items.map((item) => item + ' processed').toList();
}
''';
final compiler = Compiler();
final program = compiler.compile({
'my_app': {'main.dart': scriptContent},
});
final runtime = Runtime.ofProgram(program);
try {
final callback =
runtime.executeLib('package:my_app/main.dart', 'main') as EvalCallable;
final scriptList = $List.wrap([
$String('hello'),
$String('world'),
]);
callback.call(runtime, null, [scriptList]);
} catch (e, s) {
print('TEST FAILED');
print('--- Error Details ---');
print(e);
print('--- Stack Trace ---');
print(s.toString().split('\n').take(5).join('\n'));
print('---------------------------');
}
}
gives this error:
dart_eval runtime exception: UnimplementedError #0 EvalFunction.$value (package:dart_eval/src/eval/runtime/function.dart:41:25) #1 Unbox.run (package:dart_eval/src/eval/runtime/ops/primitives.dart:356:59) #2 Runtime.bridgeCall (package:dart_eval/src/eval/runtime/runtime.dart:829:12) at anonymous closure
RUNTIME STATE Program offset: 103 Stack sample: [L0: [L0: 1, L1: 2, L2: 3], L1: [L0: $"hello", L1: $"world"], L2: 1, L3: 2, L4: 3, L5: 4, L6: EvalFunctionPtr{offset: 86, prev: [L0: [L0: 1, L1: 2, L2: 3], L1: [L0: $"hello", L1: $"world"], L2: 1, L3: 2], rPAC: 1, pAT: [Instance of 'RuntimeType'], sNA: [], sNAT: []}, *L7: null, L8: null, L9: null] Args sample: [] Call stack: [0, -1] TRACE: 97: PushArg (L3) 98: PushConstantInt (3) 99: PushArg (L4) 100: PushConstantInt (4) 101: PushArg (L5) 102: PushFunctionPtr (@86) 103: Unbox (L6) <<< EXCEPTION 104: PushArg (L1) 105: PushArg (L6) 106: InvokeDynamic (L1.C5)
--- Stack Trace --- #0 Runtime.bridgeCall (package:dart_eval/src/eval/runtime/runtime.dart:839:7) #1 EvalFunctionPtr.call (package:dart_eval/src/eval/runtime/function.dart:61:13) #2 main (package:typesetting_prototype/eval/bug_repro_min.dart:29:14) #3 _delayEntrypointInvocation.
(dart:isolate-patch/isolate_patch.dart:314:19) #4 _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:193:12)
Example that works with loops:
import 'package:dart_eval/dart_eval.dart';
import 'package:dart_eval/dart_eval_bridge.dart';
import 'package:dart_eval/stdlib/core.dart';
void main() async {
const scriptContent = '''
Function main() {
return (items) {
final results = [];
for (final item in items) {
results.add(item + ' processed');
}
return results;
};
}
''';
final compiler = Compiler();
final program = compiler.compile({
'my_app': {'main.dart': scriptContent},
});
final runtime = Runtime.ofProgram(program);
final callback = runtime.executeLib('package:my_app/main.dart', 'main') as EvalCallable;
final scriptList = $List.wrap([
$String('hello'),
$String('world'),
]);
final result = callback.call(runtime, null, [scriptList]) as $Instance;
final resultList = (result as $List).$value;
print('Callback returned: $resultList');
}
I am willing to work on a PR for this, so any hints are appreciated, Thanks!
This is because items is dynamic. If you put (List items) => items.map(...) it will work fine.
Frankly, dart_eval has no idea what to do with dynamic as it cannot tell at compile time if it's a bridge class, regular class, tearoff, etc. All of those things have completely different internal calling conventions, so it just makes a guess which is often wrong. The code for this is in argument_list.dart called compileArgumentListWithDynamic and is called by method_invocation.dart.
The only way to actually solve this is to store every instance of a dynamic call, collect all of their names (in this case "map" and "toList") and synthesize metadata about every existent function with that name (mapped to its containing instance's type ID which will be accessible at runtime via runtimeType) into the compiled Program. Then, add a new function call op (based on InvokeDynamic) that is able to read this metadata to not only call the method, but also setup all of its args correctly based on the calling convention of the target method. This is quite a large undertaking. Of course, this would also have a large performance overhead which is why we can't just do it for every method.
One other potential problem is the tree-shaker, which might optimize out these methods before they even have a chance to be compiled. If so, you would have to force the tree-shaking step to include any class with that method name. With dart_eval's current architecture, tree-shaking runs before we compile (and thus before we resolve the type of each variable), so this would have to be done effectively from scratch. That said, just determining if a variable is dynamic or not is much, much easier than determining exactly what type it is and what that type represents.