From deebb52c2b229c9606408b8edc51f83de1cbee31 Mon Sep 17 00:00:00 2001 From: Paul Berry Date: Tue, 21 Nov 2023 00:32:19 +0000 Subject: [PATCH] Add support for null shorting expressions to the Wolf analysis prototype. The AST-to-IR conversion stage now handles null-shorting property accesses (both for reads and writes). This required adding the following instruction types: `eq`, `block`, and `brIf`. In order to make `eq` easier to test, support was also added for AST-to-IR conversion of testing binary expressions using `==`. The way null shorting is encoded in the IR is by issuing a `block` instruction when null shorting starts, and an `end` instruction when it terminates. Anywhere a null check appears within the null shorting expression, the null check uses a `brIf(0)` instruction to branch to the `end` in the case a `null` is found. To make it easier to keep track of when `block` and `end` instructions need to be generated, `RawIRWriter` keeps track of a count of the current nesting of control flow contsructs. In order for the interpreter to find to the appropriate `end` instruction when a branch is taken, a new scope analyzer is added. Later CLs will expand on it and use it for static analysis as well. Change-Id: I09ca34eaa900d47f7a4014cbc8db2a48a1d69e1e Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/336800 Reviewed-by: Phil Quitslund Commit-Queue: Paul Berry --- pkg/analyzer/lib/src/wolf/ir/ast_to_ir.dart | 72 +++++- pkg/analyzer/lib/src/wolf/ir/interpreter.dart | 167 ++++++++++++-- pkg/analyzer/lib/src/wolf/ir/ir.dart | 31 +++ pkg/analyzer/lib/src/wolf/ir/ir.g.dart | 69 +++++- .../lib/src/wolf/ir/scope_analyzer.dart | 163 +++++++++++++ pkg/analyzer/lib/src/wolf/ir/validator.dart | 29 ++- .../test/src/wolf/ir/ast_to_ir_test.dart | 153 +++++++++--- .../test/src/wolf/ir/scope_analyzer_test.dart | 160 +++++++++++++ pkg/analyzer/test/src/wolf/ir/test_all.dart | 2 + pkg/analyzer/test/src/wolf/ir/utils.dart | 24 +- .../test/src/wolf/ir/validator_test.dart | 218 ++++++++++++++++++ pkg/analyzer/tool/wolf/generate.dart | 3 + 12 files changed, 1031 insertions(+), 60 deletions(-) create mode 100644 pkg/analyzer/lib/src/wolf/ir/scope_analyzer.dart create mode 100644 pkg/analyzer/test/src/wolf/ir/scope_analyzer_test.dart diff --git a/pkg/analyzer/lib/src/wolf/ir/ast_to_ir.dart b/pkg/analyzer/lib/src/wolf/ir/ast_to_ir.dart index 7cc5b9ed534..65c7eb145ce 100644 --- a/pkg/analyzer/lib/src/wolf/ir/ast_to_ir.dart +++ b/pkg/analyzer/lib/src/wolf/ir/ast_to_ir.dart @@ -103,12 +103,23 @@ class _AstToIRVisitor extends ThrowingAstVisitor<_LValueTemplates> { _LValueTemplates dispatchLValue(Expression node) => node.accept(this)!; /// Visits [node], reporting progress to [eventListener]. - void dispatchNode(AstNode node) { + /// + /// If [node] has null shorting behavior, then [terminateNullShorting] + /// determines how the null shorting behavior is handled. If + /// [terminateNullShorting] is `true` (the default), then null shorting will + /// be terminated after visiting [node], by emitting an `end` instruction; + /// this means that in the case where the null short occurs, the expression + /// will evaluate to `null`. If [terminateNullShorting] is `false`, then null + /// shorting won't be terminated; this means that in the case where the null + /// short occurs, execution of the parent node will be skipped too. + void dispatchNode(AstNode node, {bool terminateNullShorting = true}) { eventListener.onEnterNode(node); + var previousNestingLevel = ir.nestingLevel; var lValueTemplates = node.accept(this); // If the node was an L-value, then its visitor didn't actually perform the // read, so do that now. lValueTemplates?.simpleRead(this); + if (terminateNullShorting) ir.endTo(previousNestingLevel); eventListener.onExitNode(); } @@ -135,6 +146,37 @@ class _AstToIRVisitor extends ThrowingAstVisitor<_LValueTemplates> { twoArguments); } + /// Performs a null check that is part of a null shorting expression. + /// + /// If the value at the top of the stack is `null`, execution will branch to + /// the end of the null shorting expression, and the null shorting expression + /// will evaluate to `null`. Otherwise, execution will proceed normally. + /// + /// [previousNestingLevel] is the value returned by [RawIRWriter.nestingLevel] + /// at the beginning of the null shorting expression. It is used to detect + /// whether null shorting has already been begun, and therefore whether a + /// `block` instruction needs to be output. + void nullShortingCheck({required int previousNestingLevel}) { + assert(previousNestingLevel <= ir.nestingLevel); + // Stack: value + ir.dup(); + // Stack: value value + ir.literal(null_); + // Stack: value value null + ir.eq(); + // Stack: value (value == null) + if (previousNestingLevel == ir.nestingLevel) { + // Null shorting hasn't begun yet for the containing expression, so start + // it now by opening a block; the block will be ended at the end of the + // null shorting expression, so it will be the branch target for null + // shorts. + ir.block(2, 1); + // Stack: BLOCK(1) value (value == null) + } + ir.brIf(0); + // Stack: BLOCK(1)? value + } + Null this_() { ir.readLocal(0); // Stack: this } @@ -157,6 +199,22 @@ class _AstToIRVisitor extends ThrowingAstVisitor<_LValueTemplates> { } } + @override + Null visitBinaryExpression(BinaryExpression node) { + var tokenType = node.operator.type; + switch (tokenType) { + case TokenType.EQ_EQ: + dispatchNode(node.leftOperand); + // Stack: lhs + dispatchNode(node.rightOperand); + // Stack: lhs rhs + ir.eq(); + // Stack: (lhs == rhs) + default: + throw UnimplementedError('TODO(paulberry): $node'); + } + } + @override Null visitBlock(Block node) { var previousLocalVariableCount = ir.localVariableCount; @@ -271,10 +329,14 @@ class _AstToIRVisitor extends ThrowingAstVisitor<_LValueTemplates> { @override _LValueTemplates visitPropertyAccess(PropertyAccess node) { - // TODO(paulberry): handle null shorting + var previousNestingLevel = ir.nestingLevel; // TODO(paulberry): handle cascades - dispatchNode(node.target!); + dispatchNode(node.target!, terminateNullShorting: false); // Stack: target + if (node.isNullAware) { + nullShortingCheck(previousNestingLevel: previousNestingLevel); + } + // Stack: BLOCK(1)? target return _PropertyAccessTemplates(node.propertyName); } @@ -395,8 +457,6 @@ class _LocalTemplates extends _LValueTemplates { /// subexpression values in order to read or write the L-value. These methods /// are abstract, and are defined in a derived class for each specific kind of /// L-value supported by Dart. -/// -// TODO(paulberry): add null shorting support. sealed class _LValueTemplates { /// Outputs the IR instructions for a simple read of the L-value. /// @@ -414,8 +474,6 @@ sealed class _LValueTemplates { } /// Instruction templates for converting a property access to IR. -/// -// TODO(paulberry): handle null shorting class _PropertyAccessTemplates extends _LValueTemplates { final SimpleIdentifier property; diff --git a/pkg/analyzer/lib/src/wolf/ir/interpreter.dart b/pkg/analyzer/lib/src/wolf/ir/interpreter.dart index 0645dfc6638..9945984a922 100644 --- a/pkg/analyzer/lib/src/wolf/ir/interpreter.dart +++ b/pkg/analyzer/lib/src/wolf/ir/interpreter.dart @@ -6,6 +6,7 @@ import 'package:analyzer/dart/element/type.dart'; import 'package:analyzer/src/wolf/ir/call_descriptor.dart'; import 'package:analyzer/src/wolf/ir/coded_ir.dart'; import 'package:analyzer/src/wolf/ir/ir.dart'; +import 'package:analyzer/src/wolf/ir/scope_analyzer.dart'; import 'package:meta/meta.dart'; /// Evaluates [ir], passing in [args], and returns the result. @@ -22,14 +23,35 @@ import 'package:meta/meta.dart'; /// that an instruction sequence behaves as it's expected to. @visibleForTesting Object? interpret(CodedIRContainer ir, List args, - {required CallHandler Function(CallDescriptor) callDispatcher}) { - return _IRInterpreter(ir, callDispatcher: callDispatcher).run(args); + {required Scopes scopes, required CallDispatcher callDispatcher}) { + return _IRInterpreter(ir, scopes: scopes, callDispatcher: callDispatcher) + .run(args); } /// Function type invoked by [interpret] to execute a `call` instruction. typedef CallHandler = Object? Function( List positionalArguments, Map namedArguments); +/// Interface used by [interpret] to query the behavior of calls to external +/// code. +abstract interface class CallDispatcher { + /// Evaluates a call to `operator==`, using virtual dispatch on [firstValue], + /// and passing [secondValue] as the parameter to `operator==`. + /// + /// In accordance with Dart semantics, this method is only called if both + /// [firstValue] and [secondValue] are non-null. + bool equals(Object firstValue, Object secondValue); + + /// Looks up the function that can be used to evaluate calls to + /// [callDescriptor]. + /// + /// The interpreter may invoke this method for any [CallDescriptor] in the + /// IR's call descriptor table (whether or not it's invoked), and it may cache + /// the results. However, it is guaranteed to call the [CallHandler] exactly + /// once for each `call` instruction that is interpreted. + CallHandler lookupCallDescriptor(CallDescriptor callDescriptor); +} + /// Interpreter representation of a heap object. /// /// This class should not be used for the types [int], [double], [String], or @@ -62,29 +84,101 @@ class SoundnessError extends Error { 'Soundness error at $address ($instructionString): $message'; } +/// An entry on the control flow stack, representing a control flow construct +/// (such as a `block`) that the interpreter is currently executing. +class _ControlFlowStackEntry { + /// The index into [_IRInterpreter.stack] before the first input to control + /// flow construct. + /// + /// This is called a "fence" because it represents the dividing line between + /// stack values that belong to the instructions inside the control flow + /// construct and stack values that belong to the instructions outside the + /// control flow construct. If a branch instruction targets the control flow + /// construct, this helps to determine which stack values should be discarded + /// (see [outputCount]). + final int stackFence; + + /// The length of [_IRInterpreter.locals] at the time the control flow + /// construct was entered. + /// + /// This is called a "fence" because it represents the dividing line between + /// locals that belong to the instructions inside the control flow construct + /// and locals that belong to the instructions outside the control flow + /// construct. If a branch instruction targets the control flow construct, + /// then locals whose index is greater than equal to this value will + /// automatically be released. + final int localFence; + + /// The number of outputs of the control flow construct. + /// + /// If a branch instruction targets the control flow construct, this is the + /// number of entries at the top of [_IRInterpreter.stack] that will remain on + /// the stack after the branch is taken. Any other stack entries belonging to + /// the instructions inside the control flow construct will be discarded (see + /// [stackFence]). + final int outputCount; + + /// The scope index (as defined by [Scopes]) corresponding to the instructions + /// that delimit the control flow construct. + final int scope; + + _ControlFlowStackEntry( + {required this.stackFence, + required this.localFence, + required this.outputCount, + required this.scope}); +} + class _IRInterpreter { + static const keepGoing = _KeepGoing(); final CodedIRContainer ir; + final Scopes scopes; + final CallDispatcher callDispatcher; final List callHandlers; final stack = []; final locals = <_LocalSlot>[]; + final controlFlowStack = <_ControlFlowStackEntry>[]; var address = 1; - _IRInterpreter(this.ir, - {required CallHandler Function(CallDescriptor) callDispatcher}) - : callHandlers = ir.mapCallDescriptors(callDispatcher); + /// The scope index (as defined by [Scopes]) corresponding to the last begin + /// instruction preceding [address]. + var mostRecentScope = 0; + + _IRInterpreter(this.ir, {required this.scopes, required this.callDispatcher}) + : callHandlers = + ir.mapCallDescriptors(callDispatcher.lookupCallDescriptor); /// Performs the necessary logic for a `br`, `brIf`, or `brIndex` instruction. /// /// [nesting] indicates which enclosing control flow construct is targeted by /// the branch (where 0 means the innermost). /// - /// The returned value is the value that should be returned to the caller. + /// The returned value is either: + /// - [keepGoing], indicating that interpretation should continue from the + /// instruction following [address], or + /// - Some other value, indicating that the code being interpreted has + /// finished executing, and this value should be returned to the caller. Object? branch(int nesting) { - if (nesting != 0) { - throw UnimplementedError('TODO(paulberry): nonzero branch nesting'); + while (nesting-- > 0) { + controlFlowStack.removeLast(); + } + if (controlFlowStack.isNotEmpty) { + var stackEntry = controlFlowStack.removeLast(); + var stackFence = stackEntry.stackFence; + var outputCount = stackEntry.outputCount; + var newStackLength = stackFence + outputCount; + stack.setRange(stackFence, newStackLength, stack, + stack.length - stackEntry.outputCount); + stack.length = stackFence + outputCount; + locals.length = stackEntry.localFence; + var scope = stackEntry.scope; + address = scopes.endAddress(scope); + mostRecentScope = scopes.lastDescendant(scope); + return keepGoing; + } else { + // Branch targets the function, so return from the code being interpreted. + return stack.last; } - // Branch targets the function, so return from the code being interpreted. - return stack.last; } Object? run(List args) { @@ -98,15 +192,37 @@ class _IRInterpreter { } stack.addAll(args); while (true) { + assert(scopes.mostRecentScope(address - 1) == mostRecentScope); switch (ir.opcodeAt(address)) { case Opcode.alloc: var count = Opcode.alloc.decodeCount(ir, address); for (var i = 0; i < count; i++) { locals.add(_LocalSlot()); } + case Opcode.block: + var inputCount = Opcode.block.decodeInputCount(ir, address); + var outputCount = Opcode.block.decodeOutputCount(ir, address); + var scope = ++mostRecentScope; + assert(scopes.beginAddress(scope) == address); + controlFlowStack.add(_ControlFlowStackEntry( + stackFence: stack.length - inputCount, + localFence: locals.length, + outputCount: outputCount, + scope: scope)); case Opcode.br: var nesting = Opcode.br.decodeNesting(ir, address); - return branch(nesting); + var result = branch(nesting); + if (!identical(result, keepGoing)) { + return result; + } + case Opcode.brIf: + var nesting = Opcode.brIf.decodeNesting(ir, address); + if (stack.removeLast() as bool) { + var result = branch(nesting); + if (!identical(result, keepGoing)) { + return result; + } + } case Opcode.call: var argumentNames = ir.decodeArgumentNames( Opcode.call.decodeArgumentNames(ir, address)); @@ -130,8 +246,27 @@ class _IRInterpreter { case Opcode.dup: stack.add(stack.last); case Opcode.end: - assert(stack.length == 1); - return stack.last; + if (controlFlowStack.isEmpty) { + assert(stack.length == 1); + return stack.last; + } else { + var stackEntry = controlFlowStack.last; + assert( + stack.length == stackEntry.stackFence + stackEntry.outputCount); + assert(locals.length == stackEntry.localFence); + // Continue with the code following the block. + controlFlowStack.removeLast(); + } + case Opcode.eq: + var secondValue = stack.removeLast(); + var firstValue = stack.removeLast(); + if (firstValue == null) { + stack.add(null == secondValue); + } else if (secondValue == null) { + stack.add(false); + } else { + stack.add(callDispatcher.equals(firstValue, secondValue)); + } case Opcode.literal: var value = Opcode.literal.decodeValue(ir, address); stack.add(ir.decodeLiteral(value)); @@ -173,6 +308,12 @@ class _IRInterpreter { message: message); } +/// Sentinel value used by [_IRInterpreter.branch] to indicate that the +/// interpreter should keep executing instructions. +class _KeepGoing { + const _KeepGoing(); +} + /// Storage for a single local variable. class _LocalSlot { /// The contents of the local variable, or [_NoValue] if the slot is empty. diff --git a/pkg/analyzer/lib/src/wolf/ir/ir.dart b/pkg/analyzer/lib/src/wolf/ir/ir.dart index 74692ba54a8..c7d4b536785 100644 --- a/pkg/analyzer/lib/src/wolf/ir/ir.dart +++ b/pkg/analyzer/lib/src/wolf/ir/ir.dart @@ -258,8 +258,12 @@ class RawIRWriter with _RawIRWriterMixin { int _localVariableCount = 0; + int _nestingLevel = 0; + int get localVariableCount => _localVariableCount; + int get nestingLevel => _nestingLevel; + int get nextInstructionAddress => _opcodes.length; @override @@ -268,6 +272,12 @@ class RawIRWriter with _RawIRWriterMixin { super.alloc(count); } + @override + void block(int inputCount, int outputCount) { + _nestingLevel++; + super.block(inputCount, outputCount); + } + ArgumentNamesRef encodeArgumentNames(List argumentNames) => // TODO(paulberry): is `putIfAbsent` the best-performing way to do this? _argumentNamesToRef.putIfAbsent(argumentNames, () { @@ -284,6 +294,27 @@ class RawIRWriter with _RawIRWriterMixin { return encoding; }); + @override + void end() { + _nestingLevel--; + super.end(); + } + + /// Outputs enough `end` instructions to cause [nestingLevel] to equal + /// [desiredNestingLevel]. + void endTo(int desiredNestingLevel) { + assert(desiredNestingLevel <= nestingLevel); + while (desiredNestingLevel < nestingLevel) { + end(); + } + } + + @override + void function(TypeRef type, FunctionFlags flags) { + _nestingLevel++; + super.function(type, flags); + } + @override void release(int count) { _localVariableCount -= count; diff --git a/pkg/analyzer/lib/src/wolf/ir/ir.g.dart b/pkg/analyzer/lib/src/wolf/ir/ir.g.dart index 84ae20b85a8..a4382723667 100644 --- a/pkg/analyzer/lib/src/wolf/ir/ir.g.dart +++ b/pkg/analyzer/lib/src/wolf/ir/ir.g.dart @@ -16,12 +16,24 @@ mixin _RawIRWriterMixin implements _RawIRWriterMixinInterface { _params1.add(0); } + void block(int inputCount, int outputCount) { + _opcodes.add(Opcode.block); + _params0.add(inputCount); + _params1.add(outputCount); + } + void br(int nesting) { _opcodes.add(Opcode.br); _params0.add(nesting); _params1.add(0); } + void brIf(int nesting) { + _opcodes.add(Opcode.brIf); + _params0.add(nesting); + _params1.add(0); + } + void call(CallDescriptorRef callDescriptor, ArgumentNamesRef argumentNames) { _opcodes.add(Opcode.call); _params0.add(callDescriptor.index); @@ -46,6 +58,12 @@ mixin _RawIRWriterMixin implements _RawIRWriterMixinInterface { _params1.add(0); } + void eq() { + _opcodes.add(Opcode.eq); + _params0.add(0); + _params1.add(0); + } + void function(TypeRef type, FunctionFlags flags) { _opcodes.add(Opcode.function); _params0.add(type.index); @@ -101,6 +119,9 @@ mixin IRToStringMixin implements RawIRContainerInterface { case Opcode.literal: return 'literal(${literalRefToString(Opcode.literal.decodeValue(this, address))})'; + case Opcode.eq: + return 'eq'; + case Opcode.drop: return 'drop'; @@ -110,6 +131,9 @@ mixin IRToStringMixin implements RawIRContainerInterface { case Opcode.shuffle: return 'shuffle(${Opcode.shuffle.decodePopCount(this, address)}, ${stackIndicesRefToString(Opcode.shuffle.decodeStackIndices(this, address))})'; + case Opcode.block: + return 'block(${Opcode.block.decodeInputCount(this, address)}, ${Opcode.block.decodeOutputCount(this, address)})'; + case Opcode.function: return 'function(${typeRefToString(Opcode.function.decodeType(this, address))}, ${functionFlagsToString(Opcode.function.decodeFlags(this, address))})'; @@ -119,6 +143,9 @@ mixin IRToStringMixin implements RawIRContainerInterface { case Opcode.br: return 'br(${Opcode.br.decodeNesting(this, address)})'; + case Opcode.brIf: + return 'brIf(${Opcode.brIf.decodeNesting(this, address)})'; + case Opcode.call: return 'call(${callDescriptorRefToString(Opcode.call.decodeCallDescriptor(this, address))}, ${argumentNamesRefToString(Opcode.call.decodeArgumentNames(this, address))})'; default: @@ -175,6 +202,20 @@ class _ParameterShape4 extends Opcode { class _ParameterShape5 extends Opcode { const _ParameterShape5._(super.index) : super._(); + int decodeInputCount(RawIRContainerInterface ir, int address) { + assert(ir.opcodeAt(address).index == index); + return ir._params0[address]; + } + + int decodeOutputCount(RawIRContainerInterface ir, int address) { + assert(ir.opcodeAt(address).index == index); + return ir._params1[address]; + } +} + +class _ParameterShape6 extends Opcode { + const _ParameterShape6._(super.index) : super._(); + TypeRef decodeType(RawIRContainerInterface ir, int address) { assert(ir.opcodeAt(address).index == index); return TypeRef(ir._params0[address]); @@ -186,8 +227,8 @@ class _ParameterShape5 extends Opcode { } } -class _ParameterShape6 extends Opcode { - const _ParameterShape6._(super.index) : super._(); +class _ParameterShape7 extends Opcode { + const _ParameterShape7._(super.index) : super._(); int decodeNesting(RawIRContainerInterface ir, int address) { assert(ir.opcodeAt(address).index == index); @@ -195,8 +236,8 @@ class _ParameterShape6 extends Opcode { } } -class _ParameterShape7 extends Opcode { - const _ParameterShape7._(super.index) : super._(); +class _ParameterShape8 extends Opcode { + const _ParameterShape8._(super.index) : super._(); CallDescriptorRef decodeCallDescriptor( RawIRContainerInterface ir, int address) { @@ -223,13 +264,16 @@ class Opcode { static const readLocal = _ParameterShape1._(2); static const writeLocal = _ParameterShape1._(3); static const literal = _ParameterShape2._(4); - static const drop = _ParameterShape3._(5); - static const dup = _ParameterShape3._(6); - static const shuffle = _ParameterShape4._(7); - static const function = _ParameterShape5._(8); - static const end = _ParameterShape3._(9); - static const br = _ParameterShape6._(10); - static const call = _ParameterShape7._(11); + static const eq = _ParameterShape3._(5); + static const drop = _ParameterShape3._(6); + static const dup = _ParameterShape3._(7); + static const shuffle = _ParameterShape4._(8); + static const block = _ParameterShape5._(9); + static const function = _ParameterShape6._(10); + static const end = _ParameterShape3._(11); + static const br = _ParameterShape7._(12); + static const brIf = _ParameterShape7._(13); + static const call = _ParameterShape8._(14); String describe() => opcodeNameTable[index]; @@ -239,12 +283,15 @@ class Opcode { "readLocal", "writeLocal", "literal", + "eq", "drop", "dup", "shuffle", + "block", "function", "end", "br", + "brIf", "call", ]; } diff --git a/pkg/analyzer/lib/src/wolf/ir/scope_analyzer.dart b/pkg/analyzer/lib/src/wolf/ir/scope_analyzer.dart new file mode 100644 index 00000000000..8774d888bd2 --- /dev/null +++ b/pkg/analyzer/lib/src/wolf/ir/scope_analyzer.dart @@ -0,0 +1,163 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/src/wolf/ir/ir.dart'; + +/// Analyzes the scopes in a [BaseIRContainer]. +/// +/// This function computes the nesting of begin/end scopes in [ir]. A begin/end +/// scope is the set of instructions between a "begin" instruction (a `block`, +/// `loop`, `tryCatch`, `tryFinally`, `function`, or `instanceFunction` +/// instruction) and the `end` instruction that matches it. +Scopes analyzeScopes(BaseIRContainer ir, + {ScopeAnalyzerEventListener? eventListener}) { + eventListener ??= ScopeAnalyzerEventListener(); + var scopeAnalyzer = _ScopeAnalyzer(ir, eventListener); + scopeAnalyzer.run(); + return Scopes._(scopeAnalyzer); +} + +/// Event listener used by [analyzeScopes] to report progress information. +/// +/// By itself this class does nothing; the caller of [analyzeScopes] should make +/// a derived class that overrides one or more of the `on...` methods. +base class ScopeAnalyzerEventListener { + /// Called when [scopeAnalyzer] is about to process a "begin" instruction. + void onPushScope({required int address, required int scope}) {} +} + +/// The result of scope analysis. +/// +/// See [analyzeScopes] for more information. +class Scopes { + final List _beginAddresses; + final List _endAddresses; + final List _lastDescendants; + + Scopes._(_ScopeAnalyzer analyzer) + : _beginAddresses = analyzer.beginAddresses, + _endAddresses = analyzer.endAddresses, + _lastDescendants = analyzer.lastDescendants; + + /// The number of scopes that was found. + int get scopeCount => _beginAddresses.length; + + /// The address of the "begin" instruction that opens [scope]. + /// + /// Scopes are numbered in pre-order, so a [scope] of `i` corresponds to the + /// scope opened by the `i`th begin instruction in the IR. + int beginAddress(int scope) => _beginAddresses[scope]; + + /// The address of the `end` instruction that closes [scope]. + /// + /// Scopes are numbered in pre-order, so a [scope] of `i` corresponds to the + /// scope opened by the `i`th begin instruction in the IR. + int endAddress(int scope) => _endAddresses[scope]; + + /// The scope index of the last scope transitively contained within [scope]. + /// + /// If [scope] doesn't contain any other scopes, then [scope] is returned. + int lastDescendant(int scope) => _lastDescendants[scope]; + + /// Computes the highest-numbered scope whose [beginAddress] is less than or + /// equal to [address]. + /// + /// Scopes are numbered in pre-order, so the returned scope will either be the + /// innermost scope containing [address], or one of its ancestors will be (see + /// [ancestorContainingAddress]). + int mostRecentScope(int address) { + assert(address >= 0); + // By validation, we know that the instruction sequence begins with + // `function` or `instanceFunction`, so the outermost scope covers the whole + // instruction sequence. + assert(beginAddress(0) == 0); + var low = 0; + var high = scopeCount; + while (low < high - 1) { + // Loop invariants + assert(beginAddress(low) <= address); + assert(high == scopeCount || beginAddress(high) > address); + + var mid = (low + high) ~/ 2; + if (beginAddress(mid) <= address) { + low = mid; + } else { + high = mid; + } + } + return low; + } +} + +base class _ScopeAnalyzer { + static const enableDebugPrints = false; + final BaseIRContainer ir; + final ScopeAnalyzerEventListener eventListener; + + /// Stack of scope indices for all open scopes. + final scopeIndices = []; + + /// See [Scopes.beginAddress]. + final beginAddresses = []; + + /// See [Scopes.endAddress]. + final endAddresses = []; + + /// See [Scopes.lastDescendant]. + final lastDescendants = []; + + _ScopeAnalyzer(this.ir, this.eventListener); + + bool checkState() { + if (enableDebugPrints) dumpState(); + // Scopes are numbered in pre-order, so `_scopeIndices` should be + // monotonically increasing. + for (var i = 0; i < scopeIndices.length - 1; i++) { + assert(scopeIndices[i] < scopeIndices[i + 1], + '_scopeIndices out of order: $scopeIndices'); + } + return true; + } + + void dumpState() { + print(' scopeIndices: $scopeIndices'); + print(' beginAddresses: $beginAddresses'); + print(' endAddresses: $endAddresses'); + print(' lastDescendants: $lastDescendants'); + } + + void popScope(int address) { + var scopeIndex = scopeIndices.removeLast(); + endAddresses[scopeIndex] = address; + lastDescendants[scopeIndex] = lastDescendants.length - 1; + } + + void pushScope(int address) { + var scope = beginAddresses.length; + eventListener.onPushScope(address: address, scope: scope); + scopeIndices.add(scope); + beginAddresses.add(address); + endAddresses.add(-1); + lastDescendants.add(-1); + } + + void run() { + for (var address = 0; address < ir.endAddress; address++) { + assert(checkState()); + if (enableDebugPrints) { + print('$address: ${ir.instructionToString(address)}'); + } + switch (ir.opcodeAt(address)) { + case Opcode.function: + pushScope(address); + case Opcode.block: + pushScope(address); + case Opcode.end: + popScope(address); + } + } + assert(checkState()); + assert(scopeIndices.isEmpty); + } +} diff --git a/pkg/analyzer/lib/src/wolf/ir/validator.dart b/pkg/analyzer/lib/src/wolf/ir/validator.dart index 5029c798c33..0111b3a9e54 100644 --- a/pkg/analyzer/lib/src/wolf/ir/validator.dart +++ b/pkg/analyzer/lib/src/wolf/ir/validator.dart @@ -144,12 +144,21 @@ class _Validator { /// Validates a `br` or `brIf` instruction. void branch(int nesting, {required bool conditional}) { + if (conditional) popValues(1); check(nesting >= 0, 'Negative branch nesting'); var target = controlFlowStack.length - 1 - nesting; check(target >= 0, 'Control flow stack underflow'); + for (var i = target + 1; i < controlFlowStack.length; i++) { + check(!controlFlowStack[i].isFunction, + 'Cannot branch outside of enclosing function'); + } var branchValueCount = controlFlowStack[target].branchValueCount; popValues(branchValueCount); - valueStackDepth = ValueCount.indeterminate; + if (conditional) { + pushValues(branchValueCount); + } else { + valueStackDepth = ValueCount.indeterminate; + } } /// Reports a validation error if [condition] is `false`. @@ -191,9 +200,24 @@ class _Validator { var count = Opcode.alloc.decodeCount(ir, address); check(count >= 0, 'Negative alloc count'); localCount += count; + case Opcode.block: + var inputCount = Opcode.block.decodeInputCount(ir, address); + var outputCount = Opcode.block.decodeOutputCount(ir, address); + check(inputCount >= 0, 'Negative input count'); + check(outputCount >= 0, 'Negative output count'); + popValues(inputCount); + controlFlowStack.add(_ControlFlowElement( + localCountBefore: localCount, + functionFlagsBefore: functionFlags, + valueStackDepthAfter: valueStackDepth + outputCount, + branchValueCount: outputCount)); + valueStackDepth = ValueCount(inputCount); case Opcode.br: var nesting = Opcode.br.decodeNesting(ir, address); branch(nesting, conditional: false); + case Opcode.brIf: + var nesting = Opcode.brIf.decodeNesting(ir, address); + branch(nesting, conditional: true); case Opcode.call: var argumentNames = Opcode.call.decodeArgumentNames(ir, address); popValues(ir.decodeArgumentNames(argumentNames).length); @@ -213,6 +237,9 @@ class _Validator { '${valueStackDepth._depth} superfluous value(s) remaining'); valueStackDepth = controlFlowElement.valueStackDepthAfter; functionFlags = controlFlowElement.functionFlagsBefore; + case Opcode.eq: + popValues(2); + pushValues(1); case Opcode.function: var type = Opcode.function.decodeType(ir, address); var kind = Opcode.function.decodeFlags(ir, address); diff --git a/pkg/analyzer/test/src/wolf/ir/ast_to_ir_test.dart b/pkg/analyzer/test/src/wolf/ir/ast_to_ir_test.dart index 61f80069f3b..b4671ed74e2 100644 --- a/pkg/analyzer/test/src/wolf/ir/ast_to_ir_test.dart +++ b/pkg/analyzer/test/src/wolf/ir/ast_to_ir_test.dart @@ -10,6 +10,7 @@ import 'package:analyzer/src/wolf/ir/call_descriptor.dart'; import 'package:analyzer/src/wolf/ir/coded_ir.dart'; import 'package:analyzer/src/wolf/ir/interpreter.dart'; import 'package:analyzer/src/wolf/ir/ir.dart'; +import 'package:analyzer/src/wolf/ir/scope_analyzer.dart'; import 'package:analyzer/src/wolf/ir/validator.dart'; import 'package:checks/checks.dart'; import 'package:test_reflective_loader/test_reflective_loader.dart'; @@ -27,6 +28,8 @@ main() { class AstToIRTest extends AstToIRTestBase { final _instanceGetHandlers = { 'int.isEven': unaryFunction((i) => i.isEven), + 'Iterable.first': unaryFunction((list) => list.values.first), + 'Object.hashCode': unaryFunction((o) => o.hashCode), 'String.length': unaryFunction((s) => s.length) }; @@ -38,8 +41,8 @@ class AstToIRTest extends AstToIRTestBase { ListInstance makeList(List values) => ListInstance( typeProvider.listType(typeProvider.objectQuestionType), values); - Object? runInterpreter(List args) => - interpret(ir, args, callDispatcher: _callDispatcher); + Object? runInterpreter(List args) => interpret(ir, args, + scopes: scopes, callDispatcher: _CallDispatcher(this)); test_assignmentExpression_local_simple_sideEffect() async { await assertNoErrorsInCode(''' @@ -95,6 +98,21 @@ test(int i) => i = 123; check(runInterpreter([1])).equals(123); } + test_assignmentExpression_property_nullShorting_simple() async { + await assertNoErrorsInCode(''' +test(List? l) => l?.length = 3; +'''); + analyze(findNode.singleFunctionDeclaration); + check(astNodes)[findNode.assignment('l?.length = 3')] + ..containsSubrange(astNodes[findNode.simple('l?.length')]!) + ..containsSubrange(astNodes[findNode.propertyAccess('l?.length')]!) + ..containsSubrange(astNodes[findNode.integerLiteral('3')]!); + check(runInterpreter([null])).equals(null); + var l = ['a', 'b', 'c', 'd', 'e']; + check(runInterpreter([makeList(l)])).equals(3); + check(l).deepEquals(['a', 'b', 'c']); + } + test_assignmentExpression_property_prefixedIdentifier_simple() async { await assertNoErrorsInCode(''' test(List l) => l.length = 3; @@ -138,6 +156,21 @@ extension E on List { check(l).deepEquals(['a', 'b', 'c']); } + test_binaryExpression_equal() async { + await assertNoErrorsInCode(''' +test(Object? x, Object? y) => x == y; +'''); + analyze(findNode.singleFunctionDeclaration); + check(astNodes)[findNode.binary('x == y')] + ..containsSubrange(astNodes[findNode.simple('x ==')]!) + ..containsSubrange(astNodes[findNode.simple('y;')]!); + check(runInterpreter([null, null])).equals(true); + check(runInterpreter([null, 1])).equals(false); + check(runInterpreter([1, null])).equals(false); + check(runInterpreter([1, 2])).equals(false); + check(runInterpreter([1, 1])).equals(true); + } + test_block() async { await assertNoErrorsInCode(''' test(int i) { @@ -258,6 +291,55 @@ test(int i) => (i); check(runInterpreter([123])).equals(123); } + test_parenthesizedExpression_stopsNullShorting() async { + await assertNoErrorsInCode(''' +test(List? list) => (list?.first).hashCode; +'''); + analyze(findNode.singleFunctionDeclaration); + check(runInterpreter([null])).equals(null.hashCode); + check(runInterpreter([ + makeList([123]) + ])).equals(123.hashCode); + } + + test_propertyAccess_allowsNullShorting() async { + await assertNoErrorsInCode(''' +test(List? list) => list?.first.hashCode; +'''); + analyze(findNode.singleFunctionDeclaration); + check(runInterpreter([null])).equals(null); + check(runInterpreter([ + makeList([123]) + ])).equals(123.hashCode); + } + + test_propertyAccess_nestedNullShorting() async { + await assertNoErrorsInCode(''' +test(List? list) => list?.first?.hashCode; +'''); + analyze(findNode.singleFunctionDeclaration); + check(astNodes, because: 'both null checks should use the same block')[ + findNode.singleFunctionBody] + .instructions + .withOpcode(Opcode.block) + .hasLength(1); + check(runInterpreter([null])).equals(null); + check(runInterpreter([ + makeList([123]) + ])).equals(123.hashCode); + } + + test_propertyGet_nullShorting() async { + await assertNoErrorsInCode(''' +test(String? s) => s?.length; +'''); + analyze(findNode.singleFunctionDeclaration); + check(astNodes)[findNode.propertyAccess('s?.length')] + .containsSubrange(astNodes[findNode.simple('s?.length')]!); + check(runInterpreter([null])).equals(null); + check(runInterpreter(['foo'])).equals(3); + } + test_propertyGet_prefixedIdentifier() async { await assertNoErrorsInCode(''' test(int i) => i.isEven; @@ -457,30 +539,6 @@ test() { check(runInterpreter([])).identicalTo(null); } - CallHandler _callDispatcher(CallDescriptor callDescriptor) { - CallHandler? handler; - switch (callDescriptor) { - case InstanceGetDescriptor( - getter: PropertyAccessorElement( - enclosingElement: InstanceElement(name: var typeName?) - ) - ): - handler = _instanceGetHandlers['$typeName.${callDescriptor.name}']; - case InstanceSetDescriptor( - setter: PropertyAccessorElement( - enclosingElement: InstanceElement(name: var typeName?) - ) - ): - handler = _instanceSetHandlers['$typeName.${callDescriptor.name}']; - case dynamic(:var runtimeType): - throw UnimplementedError('TODO(paulberry): $runtimeType'); - } - if (handler == null) { - throw StateError('No handler for $callDescriptor'); - } - return handler; - } - static CallHandler binaryFunction(Object? Function(T, U) f) => (positionalArguments, namedArguments) { check(positionalArguments).length.equals(2); @@ -499,6 +557,7 @@ test() { class AstToIRTestBase extends PubPackageResolutionTest { final astNodes = AstNodes(); late final CodedIRContainer ir; + late final Scopes scopes; void analyze(Declaration declaration) { switch (declaration) { @@ -516,6 +575,7 @@ class AstToIRTestBase extends PubPackageResolutionTest { 'TODO(paulberry): ${declaration.declaredElement}'); } validate(ir); + scopes = analyzeScopes(ir); } } @@ -526,6 +586,47 @@ class ListInstance extends Instance { ListInstance(super.type, this.values); } +class _CallDispatcher implements CallDispatcher { + final AstToIRTest _test; + + _CallDispatcher(this._test); + + @override + bool equals(Object firstValue, Object secondValue) { + if (firstValue is Literal) { + throw UnimplementedError('TODO(paulberry): call custom operator=='); + } + return firstValue == secondValue; + } + + @override + CallHandler lookupCallDescriptor(CallDescriptor callDescriptor) { + CallHandler? handler; + switch (callDescriptor) { + case InstanceGetDescriptor( + getter: PropertyAccessorElement( + enclosingElement: InstanceElement(name: var typeName?) + ) + ): + handler = + _test._instanceGetHandlers['$typeName.${callDescriptor.name}']; + case InstanceSetDescriptor( + setter: PropertyAccessorElement( + enclosingElement: InstanceElement(name: var typeName?) + ) + ): + handler = + _test._instanceSetHandlers['$typeName.${callDescriptor.name}']; + case dynamic(:var runtimeType): + throw UnimplementedError('TODO(paulberry): $runtimeType'); + } + if (handler == null) { + throw StateError('No handler for $callDescriptor'); + } + return handler; + } +} + extension on Subject { Subject get address => has((e) => e.address, 'address'); Subject get message => has((e) => e.message, 'message'); diff --git a/pkg/analyzer/test/src/wolf/ir/scope_analyzer_test.dart b/pkg/analyzer/test/src/wolf/ir/scope_analyzer_test.dart new file mode 100644 index 00000000000..ba42ede5b96 --- /dev/null +++ b/pkg/analyzer/test/src/wolf/ir/scope_analyzer_test.dart @@ -0,0 +1,160 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:analyzer/src/wolf/ir/ir.dart'; +import 'package:analyzer/src/wolf/ir/scope_analyzer.dart'; +import 'package:analyzer/src/wolf/ir/validator.dart'; +import 'package:checks/checks.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import 'utils.dart'; + +main() { + defineReflectiveSuite(() { + defineReflectiveTests(ScopeAnalyzerTest); + }); +} + +@reflectiveTest +class ScopeAnalyzerTest { + final labelToScope = {}; + late final TestIRContainer ir; + late final Scopes scopeAnalysisResult; + + LiteralRef get dummyLiteral => LiteralRef(0); + + void test_beginAddress() { + _analyze((ir) => ir + ..ordinaryFunction(parameterCount: 1) + ..label('block') + ..block(0, 0) + ..end() + ..end()); + check(scopeAnalysisResult.beginAddress(labelToScope['block']!)).equals(1); + } + + void test_endAddress() { + _analyze((ir) => ir + ..ordinaryFunction(parameterCount: 1) + ..label('block') + ..block(0, 0) + ..label('end') + ..end() + ..end()); + check(scopeAnalysisResult.endAddress(labelToScope['block']!)) + .equals(ir.labelToAddress('end')!); + } + + void test_lastDescendant() { + _analyze((ir) => ir + ..label('function') + ..ordinaryFunction(parameterCount: 1) + ..label('block1') + ..block(0, 0) + ..end() + ..label('block2') + ..block(0, 0) + ..label('block3') + ..block(0, 0) + ..end() + ..end() + ..end()); + check(scopeAnalysisResult.beginAddress( + scopeAnalysisResult.lastDescendant(labelToScope['function']!))) + .equals(ir.labelToAddress('block3')!); + check(scopeAnalysisResult.beginAddress( + scopeAnalysisResult.lastDescendant(labelToScope['block1']!))) + .equals(ir.labelToAddress('block1')!); + check(scopeAnalysisResult.beginAddress( + scopeAnalysisResult.lastDescendant(labelToScope['block2']!))) + .equals(ir.labelToAddress('block3')!); + check(scopeAnalysisResult.beginAddress( + scopeAnalysisResult.lastDescendant(labelToScope['block3']!))) + .equals(ir.labelToAddress('block3')!); + } + + void test_mostRecentScope_manyScopes() { + _analyze((ir) => ir + ..label('function') + ..ordinaryFunction(parameterCount: 1) + ..label('block1') + ..block(0, 0) + ..label('block2') + ..block(0, 0) + ..end() + ..label('block3') + ..block(0, 0) + ..end() + ..end() + ..label('block4') + ..block(0, 0) + ..end() + ..end()); + check(scopeAnalysisResult.mostRecentScope(ir.labelToAddress('function')!)) + .equals(labelToScope['function']!); + check(scopeAnalysisResult.mostRecentScope(ir.labelToAddress('block1')! - 1)) + .equals(labelToScope['function']!); + check(scopeAnalysisResult.mostRecentScope(ir.labelToAddress('block1')!)) + .equals(labelToScope['block1']!); + check(scopeAnalysisResult.mostRecentScope(ir.labelToAddress('block2')! - 1)) + .equals(labelToScope['block1']!); + check(scopeAnalysisResult.mostRecentScope(ir.labelToAddress('block2')!)) + .equals(labelToScope['block2']!); + check(scopeAnalysisResult.mostRecentScope(ir.labelToAddress('block3')! - 1)) + .equals(labelToScope['block2']!); + check(scopeAnalysisResult.mostRecentScope(ir.labelToAddress('block3')!)) + .equals(labelToScope['block3']!); + check(scopeAnalysisResult.mostRecentScope(ir.labelToAddress('block4')! - 1)) + .equals(labelToScope['block3']!); + check(scopeAnalysisResult.mostRecentScope(ir.labelToAddress('block4')!)) + .equals(labelToScope['block4']!); + } + + void test_mostRecentScope_oneScope() { + _analyze((ir) => ir + ..label('function') + ..ordinaryFunction(parameterCount: 1) + ..label('end') + ..end()); + check(scopeAnalysisResult.mostRecentScope(ir.labelToAddress('function')!)) + .equals(labelToScope['function']!); + check(scopeAnalysisResult.mostRecentScope(ir.labelToAddress('end')!)) + .equals(labelToScope['function']!); + } + + void test_scopeCount() { + _analyze((ir) => ir + ..ordinaryFunction(parameterCount: 1) + ..label('block') + ..block(0, 0) + ..end() + ..end()); + check(scopeAnalysisResult.scopeCount).equals(2); + } + + void _analyze(void Function(TestIRWriter) writeIR) { + var writer = TestIRWriter(); + writeIR(writer); + ir = TestIRContainer(writer); + validate(ir); + scopeAnalysisResult = analyzeScopes(ir, + eventListener: + _ScopeAnalyzerEventListener(ir: ir, labelToScope: labelToScope)); + } +} + +final class _ScopeAnalyzerEventListener extends ScopeAnalyzerEventListener { + final TestIRContainer ir; + final Map labelToScope; + + _ScopeAnalyzerEventListener({required this.ir, required this.labelToScope}); + + @override + void onPushScope({required int address, required int scope}) { + // Record the scope name. + if (ir.addressToLabel(address) case var name?) { + labelToScope[name] = scope; + } + } +} diff --git a/pkg/analyzer/test/src/wolf/ir/test_all.dart b/pkg/analyzer/test/src/wolf/ir/test_all.dart index 7c5b7c056da..94873ddad9f 100644 --- a/pkg/analyzer/test/src/wolf/ir/test_all.dart +++ b/pkg/analyzer/test/src/wolf/ir/test_all.dart @@ -5,11 +5,13 @@ import 'package:test_reflective_loader/test_reflective_loader.dart'; import 'ast_to_ir_test.dart' as ast_to_ir; +import 'scope_analyzer_test.dart' as scope_analyzer; import 'validator_test.dart' as validator; main() { defineReflectiveSuite(() { ast_to_ir.main(); + scope_analyzer.main(); validator.main(); }, name: 'ir'); } diff --git a/pkg/analyzer/test/src/wolf/ir/utils.dart b/pkg/analyzer/test/src/wolf/ir/utils.dart index e0dac62bf4c..4117c8d34cc 100644 --- a/pkg/analyzer/test/src/wolf/ir/utils.dart +++ b/pkg/analyzer/test/src/wolf/ir/utils.dart @@ -88,6 +88,11 @@ class Instruction { final int address; Instruction(this.ir, this.address); + + Opcode get opcode => ir.opcodeAt(address); + + @override + String toString() => '$address: ${ir.instructionToString(address)}'; } /// Reference to a range of instructions in a [CodedIRContainer]. @@ -226,8 +231,23 @@ extension SubjectAstNodes on Subject { /// Testing methods for [Instruction]. extension SubjectInstruction on Subject { @meta.useResult - Subject get opcode => has( - (instruction) => instruction.ir.opcodeAt(instruction.address), 'opcode'); + Subject get opcode => + has((instruction) => instruction.opcode, 'opcode'); +} + +/// Testing methods for `Iterable`. +extension SubjectInstructionIterable on Subject> { + void hasLength(int expectedLength) => context.expect( + () => ['has length $expectedLength'], + (instructions) => instructions.length == expectedLength + ? null + : Rejection(which: ['does not have length $expectedLength'])); + + @meta.useResult + Subject> withOpcode(Opcode opcode) => context.nest( + () => ['contains instructions matching ${opcode.describe()}'], + (instructions) => + Extracted.value(instructions.where((i) => i.opcode == opcode))); } /// Testing methods for [InstructionRange]. diff --git a/pkg/analyzer/test/src/wolf/ir/validator_test.dart b/pkg/analyzer/test/src/wolf/ir/validator_test.dart index c41f564c60e..6398f88be25 100644 --- a/pkg/analyzer/test/src/wolf/ir/validator_test.dart +++ b/pkg/analyzer/test/src/wolf/ir/validator_test.dart @@ -41,6 +41,48 @@ class ValidatorTest { _validate(); } + test_block_negativeInputCount() { + _analyze((ir) => ir + ..ordinaryFunction() + ..label('bad') + ..block(-1, 0) + ..end() + ..end()); + _checkInvalidMessageAt('bad').equals('Negative input count'); + } + + test_block_negativeOutputCount() { + _analyze((ir) => ir + ..ordinaryFunction() + ..label('bad') + ..block(0, -1) + ..end() + ..end()); + _checkInvalidMessageAt('bad').equals('Negative output count'); + } + + test_block_ok() { + _analyze((ir) => ir + ..ordinaryFunction(parameterCount: 2) + ..onValidate((v) => check(v.valueStackDepth).equals(ValueCount(2))) + ..block(2, 1) + ..onValidate((v) => check(v.valueStackDepth).equals(ValueCount(2))) + ..drop() + ..end() + ..end()); + _validate(); + } + + test_block_underflow() { + _analyze((ir) => ir + ..ordinaryFunction(parameterCount: 1) + ..label('bad') + ..block(2, 1) + ..end() + ..end()); + _checkInvalidMessageAt('bad').equals('Value stack underflow'); + } + test_br_controlFlowStackUnderflow() { _analyze((ir) => ir ..ordinaryFunction() @@ -50,6 +92,33 @@ class ValidatorTest { _checkInvalidMessageAt('bad').equals('Control flow stack underflow'); } + test_br_fromBlock_ok() { + _analyze((ir) => ir + ..ordinaryFunction(parameterCount: 2) + ..block(2, 1) + ..drop() + ..onValidate((v) => check(v.valueStackDepth).equals(ValueCount(1))) + ..br(0) + ..onValidate( + (v) => check(v.valueStackDepth).equals(ValueCount.indeterminate)) + ..end() + ..end()); + _validate(); + } + + test_br_fromBlock_stackUnderflow() { + _analyze((ir) => ir + ..ordinaryFunction(parameterCount: 2) + ..block(2, 1) + ..drop() + ..drop() + ..label('bad') + ..br(0) + ..end() + ..end()); + _checkInvalidMessageAt('bad').equals('Value stack underflow'); + } + test_br_fromFunction_ok() { _analyze((ir) => ir ..ordinaryFunction(parameterCount: 1) @@ -78,6 +147,90 @@ class ValidatorTest { _checkInvalidMessageAt('bad').equals('Negative branch nesting'); } + test_br_outsideOfEnclosingFunction() { + _analyze((ir) => ir + ..ordinaryFunction() + ..ordinaryFunction(parameterCount: 1) + ..label('bad') + ..br(1) + ..end() + ..end()); + _checkInvalidMessageAt('bad') + .equals('Cannot branch outside of enclosing function'); + } + + test_brIf_controlFlowStackUnderflow() { + _analyze((ir) => ir + ..ordinaryFunction(parameterCount: 2) + ..label('bad') + ..brIf(1) + ..end()); + _checkInvalidMessageAt('bad').equals('Control flow stack underflow'); + } + + test_brIf_fromBlock_ok() { + _analyze((ir) => ir + ..ordinaryFunction(parameterCount: 2) + ..block(2, 1) + ..onValidate((v) => check(v.valueStackDepth).equals(ValueCount(2))) + ..brIf(0) + ..onValidate((v) => check(v.valueStackDepth).equals(ValueCount(1))) + ..end() + ..end()); + _validate(); + } + + test_brIf_fromBlock_stackUnderflow() { + _analyze((ir) => ir + ..ordinaryFunction(parameterCount: 2) + ..block(2, 1) + ..drop() + ..label('bad') + ..brIf(0) + ..end() + ..end()); + _checkInvalidMessageAt('bad').equals('Value stack underflow'); + } + + test_brIf_fromFunction_ok() { + _analyze((ir) => ir + ..ordinaryFunction(parameterCount: 2) + ..brIf(0) + ..onValidate((v) => check(v.valueStackDepth).equals(ValueCount(1))) + ..end()); + _validate(); + } + + test_brIf_fromFunction_stackUnderflow() { + _analyze((ir) => ir + ..ordinaryFunction(parameterCount: 1) + ..label('bad') + ..brIf(0) + ..end()); + _checkInvalidMessageAt('bad').equals('Value stack underflow'); + } + + test_brIf_negativeNesting() { + _analyze((ir) => ir + ..ordinaryFunction(parameterCount: 2) + ..label('bad') + ..brIf(-1) + ..end()); + _checkInvalidMessageAt('bad').equals('Negative branch nesting'); + } + + test_brIf_outsideOfEnclosingFunction() { + _analyze((ir) => ir + ..ordinaryFunction() + ..ordinaryFunction(parameterCount: 2) + ..label('bad') + ..br(1) + ..end() + ..end()); + _checkInvalidMessageAt('bad') + .equals('Cannot branch outside of enclosing function'); + } + test_call_ok() { _analyze((ir) => ir ..ordinaryFunction(parameterCount: 2) @@ -136,6 +289,53 @@ class ValidatorTest { _checkInvalidMessageAt('bad').equals('Value stack underflow'); } + test_end_block_indeterminate() { + _analyze((ir) => ir + ..ordinaryFunction(parameterCount: 2) + ..block(2, 1) + ..br(1) + ..onValidate( + (v) => check(v.valueStackDepth).equals(ValueCount.indeterminate)) + ..end() + ..onValidate((v) => check(v.valueStackDepth).equals(ValueCount(1))) + ..end()); + _validate(); + } + + test_end_block_preservesStackValuesBelowInput() { + _analyze((ir) => ir + ..ordinaryFunction(parameterCount: 3) + ..onValidate((v) => check(v.valueStackDepth).equals(ValueCount(3))) + ..block(2, 1) + ..drop() + ..onValidate((v) => check(v.valueStackDepth).equals(ValueCount(1))) + ..end() + ..onValidate((v) => check(v.valueStackDepth).equals(ValueCount(2))) + ..drop() + ..end()); + _validate(); + } + + test_end_block_superfluousValues() { + _analyze((ir) => ir + ..ordinaryFunction(parameterCount: 2) + ..block(2, 1) + ..label('bad') + ..end() + ..end()); + _checkInvalidMessageAt('bad').equals('1 superfluous value(s) remaining'); + } + + test_end_block_underflow() { + _analyze((ir) => ir + ..ordinaryFunction() + ..block(0, 1) + ..label('bad') + ..end() + ..end()); + _checkInvalidMessageAt('bad').equals('Value stack underflow'); + } + test_end_function_indeterminate() { _analyze((ir) => ir ..ordinaryFunction(parameterCount: 1) @@ -196,6 +396,24 @@ class ValidatorTest { _checkInvalidMessageAt('bad').equals('Unreleased locals'); } + test_eq_ok() { + _analyze((ir) => ir + ..ordinaryFunction(parameterCount: 2) + ..onValidate((v) => check(v.valueStackDepth).equals(ValueCount(2))) + ..eq() + ..onValidate((v) => check(v.valueStackDepth).equals(ValueCount(1))) + ..end()); + } + + test_eq_underflow() { + _analyze((ir) => ir + ..ordinaryFunction(parameterCount: 1) + ..label('bad') + ..eq() + ..end()); + _checkInvalidMessageAt('bad').equals('Value stack underflow'); + } + test_firstInstruction_function_ok() { _analyze((ir) => ir ..ordinaryFunction() diff --git a/pkg/analyzer/tool/wolf/generate.dart b/pkg/analyzer/tool/wolf/generate.dart index 272c3e02486..74b6d044926 100644 --- a/pkg/analyzer/tool/wolf/generate.dart +++ b/pkg/analyzer/tool/wolf/generate.dart @@ -85,15 +85,18 @@ class _Instructions { _addInstruction('writeLocal', [uint('localIndex')]); // Primitive operations _addInstruction('literal', [literal('value')]); + _addInstruction('eq', []); // Stack manipulation _addInstruction('drop', []); _addInstruction('dup', []); _addInstruction( 'shuffle', [uint('popCount'), stackIndices('stackIndices')]); // Flow control + _addInstruction('block', [uint('inputCount'), uint('outputCount')]); _addInstruction('function', [type('type'), functionFlags('flags')]); _addInstruction('end', []); _addInstruction('br', [uint('nesting')]); + _addInstruction('brIf', [uint('nesting')]); // Invocations and tearoffs _addInstruction('call', [(callDescriptor('callDescriptor')), (argumentNames('argumentNames'))]);