diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b588b5..e85706b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## 0.7.6 +- Support for 'break' inside loops +- Support for constructor block bodies +- Allow changing the value of static class fields +- Fix various errors caused by tree-shaking bridge class data that is required + to set up the runtime +- Fix error when setting values on Maps + ## 0.7.5 - Create writeback-capable List wrapper. Use `$List.view()` with a type mapper function to create a view of an underlying List that can be written to. diff --git a/README.md b/README.md index d74722b..a7862f4 100644 --- a/README.md +++ b/README.md @@ -650,7 +650,7 @@ may vary when bridging. | For-each loops | ✅ | [[1]](https://github.com/ethanblake4/dart_eval/blob/master/test/statement_test.dart#L52) | | Async for-each | ❌ | N/A | | Switch statements | ❌ | N/A | -| Labels and `break` | ❌ | N/A | +| Labels, `break` & `continue` | Partial | [[1]](https://github.com/ethanblake4/dart_eval/blob/master/test/loop_test.dart#L126), [[2]](https://github.com/ethanblake4/dart_eval/blob/master/test/loop_test.dart#L146) | | If statements | ✅ | [[1]](https://github.com/ethanblake4/dart_eval/blob/master/test/loop_test.dart#L28) | | Try-catch | ✅ | [[1]](https://github.com/ethanblake4/dart_eval/blob/master/test/exception_test.dart#L13), [[2]](https://github.com/ethanblake4/dart_eval/blob/master/test/exception_test.dart#31), [[3]](https://github.com/ethanblake4/dart_eval/blob/master/test/exception_test.dart#49), [[4]](https://github.com/ethanblake4/dart_eval/blob/master/test/exception_test.dart#71), [[5]](https://github.com/ethanblake4/dart_eval/blob/master/test/exception_test.dart#92) | | Try-catch-finally | ✅ | [[1]](https://github.com/ethanblake4/dart_eval/blob/master/test/exception_test.dart#L132), [[2]](https://github.com/ethanblake4/dart_eval/blob/master/test/exception_test.dart#147), [[3]](https://github.com/ethanblake4/dart_eval/blob/master/test/exception_test.dart#187), [[4]](https://github.com/ethanblake4/dart_eval/blob/master/test/exception_test.dart#209), [[5]](https://github.com/ethanblake4/dart_eval/blob/master/test/exception_test.dart#231) | diff --git a/lib/src/eval/compiler/collection/list.dart b/lib/src/eval/compiler/collection/list.dart index 794355e..1b147e3 100644 --- a/lib/src/eval/compiler/collection/list.dart +++ b/lib/src/eval/compiler/collection/list.dart @@ -7,6 +7,7 @@ import 'package:dart_eval/src/eval/compiler/context.dart'; import 'package:dart_eval/src/eval/compiler/errors.dart'; import 'package:dart_eval/src/eval/compiler/expression/expression.dart'; import 'package:dart_eval/src/eval/compiler/macros/loop.dart'; +import 'package:dart_eval/src/eval/compiler/model/label.dart'; import 'package:dart_eval/src/eval/compiler/statement/statement.dart'; import 'package:dart_eval/src/eval/compiler/type.dart'; import 'package:dart_eval/src/eval/compiler/variable.dart'; @@ -49,10 +50,12 @@ Variable compileListLiteral(ListLiteral l, CompilerContext ctx, ); ctx.beginAllocScope(); + ctx.labels.add(SimpleCompilerLabel()); final resultTypes = []; for (final e in elements) { resultTypes.addAll(compileListElement(e, _list, ctx, _boxListElements)); } + ctx.labels.removeLast(); ctx.endAllocScope(); if (listSpecifiedType == null) { diff --git a/lib/src/eval/compiler/compiler.dart b/lib/src/eval/compiler/compiler.dart index 036ced9..fddd8e1 100644 --- a/lib/src/eval/compiler/compiler.dart +++ b/lib/src/eval/compiler/compiler.dart @@ -283,6 +283,7 @@ class Compiler implements BridgeDeclarationRegistry, EvalPluginRegistry { /// Discover entrypoints for (final declaration in library.declarations) { if (declaration.isBridge) { + _entrypoints.add(library.uri); continue; } final d = declaration.declaration!; diff --git a/lib/src/eval/compiler/context.dart b/lib/src/eval/compiler/context.dart index c3d5378..f12fa3e 100644 --- a/lib/src/eval/compiler/context.dart +++ b/lib/src/eval/compiler/context.dart @@ -4,6 +4,7 @@ import 'package:analyzer/dart/ast/ast.dart'; import 'package:dart_eval/dart_eval_bridge.dart'; import 'package:dart_eval/src/eval/compiler/builtins.dart'; import 'package:dart_eval/src/eval/compiler/constant_pool.dart'; +import 'package:dart_eval/src/eval/compiler/model/label.dart'; import 'package:dart_eval/src/eval/compiler/model/override_spec.dart'; import 'package:dart_eval/src/eval/compiler/optimizer/prescan.dart'; import 'package:dart_eval/src/eval/compiler/source.dart'; @@ -61,6 +62,14 @@ mixin ScopeContext on Object implements AbstractScopeContext { return nestCount; } + int endAllocScopeQuiet({bool popValues = true, int popAdjust = 0}) { + final nestCount = allocNest.removeLast(); + if (popValues) { + popN(nestCount + popAdjust); + } + return nestCount; + } + void popN(int pops) { if (pops == 0) { return; @@ -189,6 +198,8 @@ class CompilerContext with ScopeContext { List scopeDoesClose = []; List typeInferenceSaveStates = []; List typeUninferenceSaveStates = []; + List labels = []; + Map> labelReferences = {}; List inTypeInferenceContext = []; final List caughtExceptions = []; PrescanContext? preScan; @@ -337,6 +348,16 @@ class CompilerContext with ScopeContext { }); } } + + void resolveLabel(CompilerLabel label) { + final references = labelReferences[label]; + if (references != null) { + for (final ref in references) { + final jump = JumpConstant.make(out.length); + rewriteOp(ref, jump, 0); + } + } + } } class ContextSaveState with ScopeContext { diff --git a/lib/src/eval/compiler/declaration/constructor.dart b/lib/src/eval/compiler/declaration/constructor.dart index 088c690..5b7cd82 100644 --- a/lib/src/eval/compiler/declaration/constructor.dart +++ b/lib/src/eval/compiler/declaration/constructor.dart @@ -262,6 +262,21 @@ void compileConstructorDeclaration( _compileUnusedFields(ctx, fields, {}, instOffset); + final body = d.body; + if (d.factoryKeyword == null && !(body is EmptyFunctionBody)) { + ctx.beginAllocScope(); + ctx.setLocal('#this', Variable(instOffset, TypeRef.$this(ctx)!)); + if (body is BlockFunctionBody) { + compileBlock( + body.block, AlwaysReturnType(CoreTypes.voidType.ref(ctx), false), ctx, + name: '$n()'); + } else if (body is ExpressionFunctionBody) { + final V = compileExpression(body.expression, ctx); + doReturn(ctx, AlwaysReturnType(CoreTypes.voidType.ref(ctx), false), V); + } + ctx.endAllocScope(); + } + if ($extends != null && extendsWhat!.declaration!.isBridge) { final decl = extendsWhat.declaration!; final bridge = decl.bridge! as BridgeClassDef; diff --git a/lib/src/eval/compiler/macros/branch.dart b/lib/src/eval/compiler/macros/branch.dart index a6e6f54..042abfd 100644 --- a/lib/src/eval/compiler/macros/branch.dart +++ b/lib/src/eval/compiler/macros/branch.dart @@ -1,5 +1,6 @@ import 'package:dart_eval/src/eval/compiler/context.dart'; import 'package:dart_eval/src/eval/compiler/macros/macro.dart'; +import 'package:dart_eval/src/eval/compiler/model/label.dart'; import 'package:dart_eval/src/eval/compiler/statement/statement.dart'; import 'package:dart_eval/src/eval/compiler/type.dart'; import 'package:dart_eval/src/eval/runtime/runtime.dart'; @@ -21,7 +22,17 @@ StatementInfo macroBranch( ctx.inferTypes(); ctx.beginAllocScope(); + final label = CompilerLabel(LabelType.branch, -1, (_ctx) { + _ctx.endAllocScopeQuiet(); + if (!resolveStateToThen) { + _ctx.resolveBranchStateDiscontinuity(_initialState); + } + _ctx.endAllocScopeQuiet(); + return -1; + }); + ctx.labels.add(label); final thenResult = thenBranch(ctx, expectedReturnType); + ctx.labels.removeLast(); ctx.endAllocScope(); ctx.uninferTypes(); @@ -41,7 +52,15 @@ StatementInfo macroBranch( if (elseBranch != null) { ctx.beginAllocScope(); + final label = CompilerLabel(LabelType.branch, -1, (_ctx) { + ctx.endAllocScope(); + ctx.resolveBranchStateDiscontinuity(_initialState); + ctx.endAllocScope(); + return -1; + }); + ctx.labels.add(label); final elseResult = elseBranch(ctx, expectedReturnType); + ctx.labels.removeLast(); ctx.endAllocScope(); ctx.resolveBranchStateDiscontinuity(_initialState); ctx.rewriteOp(rewriteOut!, JumpConstant.make(ctx.out.length), 0); diff --git a/lib/src/eval/compiler/macros/loop.dart b/lib/src/eval/compiler/macros/loop.dart index 2a770d0..483d10f 100644 --- a/lib/src/eval/compiler/macros/loop.dart +++ b/lib/src/eval/compiler/macros/loop.dart @@ -1,5 +1,6 @@ import 'package:dart_eval/src/eval/compiler/context.dart'; import 'package:dart_eval/src/eval/compiler/macros/macro.dart'; +import 'package:dart_eval/src/eval/compiler/model/label.dart'; import 'package:dart_eval/src/eval/compiler/statement/statement.dart'; import 'package:dart_eval/src/eval/compiler/type.dart'; import 'package:dart_eval/src/eval/compiler/variable.dart'; @@ -46,7 +47,27 @@ StatementInfo macroLoop( update(ctx); } + final label = CompilerLabel(LabelType.loop, loopStart, (_ctx) { + _ctx.endAllocScopeQuiet(); + + /// Box/unbox variables that were declared outside the loop and changed in + /// the loop body to match the save state + _ctx.resolveBranchStateDiscontinuity(save); + + if (conditionSaveState != null) { + ctx.restoreBoxingState(conditionSaveState); + ctx.resolveBranchStateDiscontinuity(save); + } + + ctx.endAllocScopeQuiet(); + final result = ctx.pushOp(JumpConstant.make(-1), JumpConstant.LEN); + return result; + }); + + ctx.labels.add(label); final statementResult = body(ctx, expectedReturnType); + ctx.labels.removeLast(); + if (!(statementResult.willAlwaysThrow || statementResult.willAlwaysReturn)) { if (update != null && !updateBeforeBody) { update(ctx); @@ -85,6 +106,7 @@ StatementInfo macroLoop( } ctx.endAllocScope(popAdjust: pops); + ctx.resolveLabel(label); return statementResult; } diff --git a/lib/src/eval/compiler/model/label.dart b/lib/src/eval/compiler/model/label.dart new file mode 100644 index 0000000..ef23c63 --- /dev/null +++ b/lib/src/eval/compiler/model/label.dart @@ -0,0 +1,25 @@ +import 'package:dart_eval/src/eval/compiler/context.dart'; + +class CompilerLabel { + final int offset; + final int Function(CompilerContext ctx) cleanup; + final String? name; + final LabelType type; + + const CompilerLabel(this.type, this.offset, this.cleanup, {this.name}); +} + +class SimpleCompilerLabel implements CompilerLabel { + get offset => -1; + final String? name; + get type => LabelType.block; + + const SimpleCompilerLabel({this.name}); + + get cleanup => (CompilerContext ctx) { + ctx.endAllocScopeQuiet(); + return -1; + }; +} + +enum LabelType { loop, branch, block } diff --git a/lib/src/eval/compiler/reference.dart b/lib/src/eval/compiler/reference.dart index 93df5b3..055701c 100644 --- a/lib/src/eval/compiler/reference.dart +++ b/lib/src/eval/compiler/reference.dart @@ -94,6 +94,23 @@ class IdentifierReference implements Reference { @override Variable setValue(CompilerContext ctx, Variable value, [AstNode? source]) { if (object != null) { + // If the object is a class name, access static fields + if (object!.type == CoreTypes.type.ref(ctx)) { + final classType = object!.concreteTypes[0].resolveTypeChain(ctx); + final _name = '${classType.name}.$name'; + final type = ctx.topLevelVariableInferredTypes[classType.file]![_name]!; + final gIndex = ctx.topLevelGlobalIndices[classType.file]![_name]!; + if (!value.type.isAssignableTo(ctx, type)) { + throw CompileError( + 'Cannot assign value of type ${value.type} to field "$name" of type $type', + source); + } + final _value = + type.boxed ? value.boxIfNeeded(ctx) : value.unboxIfNeeded(ctx); + ctx.pushOp( + SetGlobal.make(gIndex, _value.scopeFrameOffset), SetGlobal.LEN); + return _value; + } object = object!.boxIfNeeded(ctx); final fieldType = TypeRef.lookupFieldType(ctx, object!.type, name, forSet: true, source: source) ?? diff --git a/lib/src/eval/compiler/statement/block.dart b/lib/src/eval/compiler/statement/block.dart index 06b190d..a76d619 100644 --- a/lib/src/eval/compiler/statement/block.dart +++ b/lib/src/eval/compiler/statement/block.dart @@ -1,4 +1,5 @@ import 'package:analyzer/dart/ast/ast.dart'; +import 'package:dart_eval/src/eval/compiler/model/label.dart'; import '../context.dart'; import 'statement.dart'; @@ -13,6 +14,7 @@ StatementInfo compileBlock( var willAlwaysReturn = false; var willAlwaysThrow = false; + ctx.labels.add(SimpleCompilerLabel()); for (final s in b.statements) { final stInfo = compileStatement(s, expectedReturnType, ctx); @@ -25,6 +27,7 @@ StatementInfo compileBlock( break; } } + ctx.labels.removeLast(); ctx.endAllocScope(popValues: !willAlwaysThrow && !willAlwaysReturn); diff --git a/lib/src/eval/compiler/statement/break.dart b/lib/src/eval/compiler/statement/break.dart new file mode 100644 index 0000000..bc6e1bd --- /dev/null +++ b/lib/src/eval/compiler/statement/break.dart @@ -0,0 +1,32 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:dart_eval/src/eval/compiler/context.dart'; +import 'package:dart_eval/src/eval/compiler/errors.dart'; +import 'package:dart_eval/src/eval/compiler/model/label.dart'; +import 'package:dart_eval/src/eval/compiler/statement/statement.dart'; + +StatementInfo compileBreakStatement(BreakStatement s, CompilerContext ctx) { + if (s.label != null) { + throw CompileError('Break labels are not currently supported', s); + } + + final currentState = ctx.saveState(); + + final index = + ctx.labels.lastIndexWhere((label) => label.type == LabelType.loop); + + if (index == -1) { + throw CompileError('Cannot use \'break\' outside of a loop context', s); + } + + for (var i = ctx.labels.length - 1; i > index; i--) { + ctx.labels[i].cleanup(ctx); + } + final label = ctx.labels[index]; + final offset = label.cleanup(ctx); + if (!ctx.labelReferences.containsKey(label)) { + ctx.labelReferences[label] = {}; + } + ctx.labelReferences[label]!.add(offset); + ctx.restoreState(currentState); + return StatementInfo(-1); +} diff --git a/lib/src/eval/compiler/statement/statement.dart b/lib/src/eval/compiler/statement/statement.dart index e813406..671ded4 100644 --- a/lib/src/eval/compiler/statement/statement.dart +++ b/lib/src/eval/compiler/statement/statement.dart @@ -4,6 +4,7 @@ import 'package:dart_eval/src/eval/compiler/context.dart'; import 'package:dart_eval/src/eval/compiler/errors.dart'; import 'package:dart_eval/src/eval/compiler/expression/expression.dart'; import 'package:dart_eval/src/eval/compiler/statement/assert.dart'; +import 'package:dart_eval/src/eval/compiler/statement/break.dart'; import 'package:dart_eval/src/eval/compiler/statement/do.dart'; import 'package:dart_eval/src/eval/compiler/statement/for.dart'; import 'package:dart_eval/src/eval/compiler/statement/if.dart'; @@ -41,6 +42,8 @@ StatementInfo compileStatement( return compileTryStatement(s, ctx, expectedReturnType); } else if (s is AssertStatement) { return compileAssertStatement(s, ctx, expectedReturnType); + } else if (s is BreakStatement) { + return compileBreakStatement(s, ctx); } else { throw CompileError('Unknown statement type ${s.runtimeType}'); } diff --git a/lib/src/eval/compiler/statement/try.dart b/lib/src/eval/compiler/statement/try.dart index 0b9f6b0..f70928d 100644 --- a/lib/src/eval/compiler/statement/try.dart +++ b/lib/src/eval/compiler/statement/try.dart @@ -1,6 +1,7 @@ import 'package:analyzer/dart/ast/ast.dart'; import 'package:dart_eval/src/eval/compiler/context.dart'; import 'package:dart_eval/src/eval/compiler/macros/branch.dart'; +import 'package:dart_eval/src/eval/compiler/model/label.dart'; import 'package:dart_eval/src/eval/compiler/statement/block.dart'; import 'package:dart_eval/src/eval/compiler/statement/statement.dart'; import 'package:dart_eval/src/eval/compiler/type.dart'; @@ -31,7 +32,9 @@ StatementInfo compileTryStatement( final _initialState = ctx.saveState(); ctx.beginAllocScope(); + ctx.labels.add(SimpleCompilerLabel()); final bodyInfo = compileBlock(s.body, expectedReturnType, ctx); + ctx.labels.removeLast(); ctx.endAllocScope(); ctx.resolveBranchStateDiscontinuity(_initialState); diff --git a/lib/src/eval/runtime/exception.dart b/lib/src/eval/runtime/exception.dart index c896d61..23d0df1 100644 --- a/lib/src/eval/runtime/exception.dart +++ b/lib/src/eval/runtime/exception.dart @@ -5,8 +5,12 @@ import 'package:dart_eval/src/eval/shared/stdlib/core/num.dart'; /// Format a dart_eval stack sample for printing. String formatStackSample(List st, int size, [int? frameOffset]) { final sb = StringBuffer('['); - final _size = min(size, st.length); - for (var i = 0; i < _size; i++) { + var i = 0; + if (frameOffset != null) { + i = max(0, frameOffset - size ~/ 1.3); + } + final end = min(i + size, st.length); + for (; i < end; i++) { final s = st[i]; if (i == frameOffset) { sb.write('*'); @@ -21,7 +25,7 @@ String formatStackSample(List st, int size, [int? frameOffset]) { } else { sb.write('$s'); } - if (i < _size - 1) { + if (i < end - 1) { sb.write(', '); } } diff --git a/lib/src/eval/runtime/runtime.dart b/lib/src/eval/runtime/runtime.dart index 52ccf47..fe25b8d 100644 --- a/lib/src/eval/runtime/runtime.dart +++ b/lib/src/eval/runtime/runtime.dart @@ -315,8 +315,10 @@ class Runtime { void assertPermission(String domain, [Object? data]) { if (!checkPermission(domain, data)) { throw Exception( - "Permission '$domain' denied${data == null ? '' : ' for $data'}.\n" - "To grant permissions, use Runtime.grant()."); + "Permission '$domain' denied${data == null ? '' : " for '$data'"}.\n" + "To grant permissions, use Runtime.grant() or add the permission " + "to the permissions array of your HotSwapLoader, EvalWidget, " + "or eval() function."); } } diff --git a/lib/src/eval/shared/stdlib/core/map.dart b/lib/src/eval/shared/stdlib/core/map.dart index 46b1729..c0e38ce 100644 --- a/lib/src/eval/shared/stdlib/core/map.dart +++ b/lib/src/eval/shared/stdlib/core/map.dart @@ -121,10 +121,7 @@ class $Map implements Map, $Instance { Runtime runtime, $Value? target, List<$Value?> args) { final idx = args[0]!; final map = target!.$value as Map; - if (map.values.first is $Value) { - return map[idx]; - } - return map[idx.$value]; + return map[idx]; } static const $Function __indexSet = $Function(_indexSet); @@ -133,7 +130,7 @@ class $Map implements Map, $Instance { Runtime runtime, $Value? target, List<$Value?> args) { final idx = args[0]!; final value = args[1]!; - return (target!.$value as Map)[idx.$value] = value; + return (target!.$value as Map)[idx] = value; } static const $Function __addAll = $Function(_addAll); diff --git a/pubspec.yaml b/pubspec.yaml index d2fd1a9..995c67c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: dart_eval description: A flexible Dart bytecode compiler and interpreter written in Dart, enabling dynamic execution and code push for AOT Dart apps. -version: 0.7.5 +version: 0.7.6 homepage: https://github.com/ethanblake4/dart_eval platforms: android: diff --git a/test/class_test.dart b/test/class_test.dart index f8862dc..662ea3a 100644 --- a/test/class_test.dart +++ b/test/class_test.dart @@ -429,6 +429,48 @@ void main() { }, prints('true\nfalse\ntrue\n')); }); + test('Modifying static class field', () { + final runtime = compiler.compileWriteAndLoad({ + 'example': { + 'main.dart': ''' + class TestA { + static int value = 11; + } + + int main() { + TestA.value = 22; + return TestA.value; + } + ''' + } + }); + + expect(runtime.executeLib('package:example/main.dart', 'main'), 22); + }); + + test('Modifying field value in constructor block', () { + final runtime = compiler.compileWriteAndLoad({ + 'example': { + 'main.dart': ''' + class TestA { + int value = 11; + TestA() { + value = 22; + this.value++; + } + } + + int main() { + var a = TestA(); + return a.value; + } + ''' + } + }); + + expect(runtime.executeLib('package:example/main.dart', 'main'), 23); + }); + /* test('Super parameter multi-level indirection', () { final runtime = compiler.compileWriteAndLoad({ 'example': { diff --git a/test/collection_test.dart b/test/collection_test.dart index d7b38ea..1d6c141 100644 --- a/test/collection_test.dart +++ b/test/collection_test.dart @@ -171,5 +171,21 @@ void main() { expect(runtime.executeLib('package:eval_test/main.dart', 'main'), true); }); + + test('Add key to empty map', () { + final runtime = compiler.compileWriteAndLoad({ + 'eval_test': { + 'main.dart': ''' + bool main() { + final testMap = {}; + testMap['name'] = 'Jon'; + return testMap.isNotEmpty; + } + ''' + } + }); + + expect(runtime.executeLib('package:eval_test/main.dart', 'main'), true); + }); }); } diff --git a/test/expression_test.dart b/test/expression_test.dart index e52e47b..4d2152e 100644 --- a/test/expression_test.dart +++ b/test/expression_test.dart @@ -341,9 +341,7 @@ void main() { }, prints('null\n1\n')); }); - // - - test('idk', () { + test('Named params and ternary', () { final runtime = Compiler().compileWriteAndLoad({ 'example': { 'main.dart': ''' diff --git a/test/loop_test.dart b/test/loop_test.dart index 568a788..9891642 100644 --- a/test/loop_test.dart +++ b/test/loop_test.dart @@ -122,5 +122,51 @@ void main() { expect( runtime.executeLib('package:example/main.dart', 'main'), $int(555)); }); + + test('For loop with break', () { + final runtime = compiler.compileWriteAndLoad({ + 'example': { + 'main.dart': ''' + num main() { + var i = 0; + for (; i < 555; i++) { + print(i); + if (i == 5) { + break; + } + } + return i; + } + ''', + } + }); + expect(runtime.executeLib('package:example/main.dart', 'main'), $int(5)); + }); + + test('Nested for loop with break', () { + final runtime = compiler.compileWriteAndLoad({ + 'example': { + 'main.dart': ''' + num main() { + var i = 0; + var j = 0; + for (; i < 555; i++) { + for (; j < 555; j++) { + if (j == 100) { + break; + } + } + if (i == 100) { + break; + } + } + return i * 1000 + j; + } + ''', + } + }); + expect(runtime.executeLib('package:example/main.dart', 'main'), + $int(100100)); + }); }); }