// Copyright (c) 2024, 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 'dart:typed_data' show Uint8List; import 'package:kernel/ast.dart' show TreeNode; import 'dbc.dart'; import 'exceptions.dart' show ExceptionsTable; import 'local_variable_table.dart' show LocalVariableTable; import 'options.dart'; import 'source_positions.dart' show RecordedCoverageArray, RecordedCoverageType, SourcePositions; class Label { final bool allowsBackwardJumps; List? _jumps = []; int offset = -1; Label({this.allowsBackwardJumps = false}); bool get isBound => offset >= 0; int jumpOperand(int jumpOffset) { if (isBound) { if (offset <= jumpOffset && !allowsBackwardJumps) { throw 'Backward jump to this label is not allowed'; } // Jump instruction takes a relative offset. return offset - jumpOffset; } _jumps!.add(jumpOffset); return 0; } List bind(int offset) { assert(!isBound); this.offset = offset; final jumps = _jumps!; _jumps = null; return jumps; } } class BytecodeAssembler { static const int kByteMask = 0xFF; static const int kUint32Mask = 0xFFFFFFFF; static const int kMinInt8 = -0x80; static const int kMaxInt8 = 0x7F; static const int kMinInt24 = -0x800000; static const int kMaxInt24 = 0x7FFFFF; static const int kMinInt32 = -0x80000000; static const int kMaxInt32 = 0x7FFFFFFF; static const int kInitialCapacity = 32; int _length = 0; Uint8List _buffer = new Uint8List(kInitialCapacity); final ExceptionsTable exceptionsTable = new ExceptionsTable(); final LocalVariableTable localVariableTable = new LocalVariableTable(); final SourcePositions sourcePositions = new SourcePositions(); final RecordedCoverageArray recordedCoverageArray = new RecordedCoverageArray(); final bool _emitSourcePositions; final bool _recordCoverage; bool isUnreachable = false; int currentSourcePosition = TreeNode.noOffset; int currentSourcePositionFlags = 0; BytecodeAssembler(BytecodeOptions options) : _emitSourcePositions = options.emitSourcePositions, _recordCoverage = options.recordCoverage; int get offset => _length; Uint8List get bytecode => new Uint8List.view(_buffer.buffer, 0, _length); void bind(Label label) { final List jumps = label.bind(offset); for (int jumpOffset in jumps) { _patchJump(jumpOffset, label.jumpOperand(jumpOffset)); } if (jumps.isNotEmpty || label.allowsBackwardJumps) { isUnreachable = false; } } @pragma('vm:prefer-inline') void _emitSourcePosition() { final position = currentSourcePosition == TreeNode.noOffset ? SourcePositions.noSourcePosition : currentSourcePosition; if (_emitSourcePositions && !isUnreachable) { bool added = sourcePositions.add( offset, position, currentSourcePositionFlags, ); if (!added) { // There's already an entry for the current PC, so emit a Nop instruction // to provide a new pc offset for the requested source position. _emitNop(); added = sourcePositions.add( offset, position, currentSourcePositionFlags, ); assert(added); } } } @pragma('vm:prefer-inline') void emitSourcePosition() { if (currentSourcePosition != TreeNode.noOffset) { _emitSourcePosition(); } } // TreeNode.noOffset (-1) source position on calls is used to mark synthetic // calls without corresponding source position. Debugger uses the absence of // source position to distinguish these calls and avoid stopping at them // while single stepping. @pragma('vm:prefer-inline') void emitSourcePositionForCall() => _emitSourcePosition(); void _grow() { final newSize = _buffer.length << 1; final newBuffer = new Uint8List(newSize); newBuffer.setRange(0, _buffer.length, _buffer); _buffer = newBuffer; } void _growAndEmitBytes( int b0, [ int? b1, int? b2, int? b3, int? b4, int? b5, ]) { _grow(); assert(_length + 6 < _buffer.length); _buffer[_length] = b0; ++_length; if (b1 != null) { _buffer[_length] = b1; ++_length; if (b2 != null) { _buffer[_length] = b2; ++_length; if (b3 != null) { _buffer[_length] = b3; ++_length; if (b4 != null) { _buffer[_length] = b4; ++_length; if (b5 != null) { _buffer[_length] = b5; ++_length; } } } } } } @pragma('vm:prefer-inline') void _emitByte(int abyte) { assert(_isUint8(abyte)); if (_length < _buffer.length) { _buffer[_length] = abyte; ++_length; } else { _growAndEmitBytes(abyte); } } @pragma('vm:prefer-inline') void _emitBytes2(int b0, int b1) { assert(_isUint8(b0) && _isUint8(b1)); if (_length + 1 < _buffer.length) { _buffer[_length] = b0; _buffer[_length + 1] = b1; _length += 2; } else { _growAndEmitBytes(b0, b1); } } @pragma('vm:prefer-inline') void _emitBytes3(int b0, int b1, int b2) { assert(_isUint8(b0) && _isUint8(b1) && _isUint8(b2)); if (_length + 2 < _buffer.length) { _buffer[_length] = b0; _buffer[_length + 1] = b1; _buffer[_length + 2] = b2; _length += 3; } else { _growAndEmitBytes(b0, b1, b2); } } @pragma('vm:prefer-inline') void _emitBytes4(int b0, int b1, int b2, int b3) { assert(_isUint8(b0) && _isUint8(b1) && _isUint8(b2) && _isUint8(b3)); if (_length + 3 < _buffer.length) { _buffer[_length] = b0; _buffer[_length + 1] = b1; _buffer[_length + 2] = b2; _buffer[_length + 3] = b3; _length += 4; } else { _growAndEmitBytes(b0, b1, b2, b3); } } void _emitBytes5(int b0, int b1, int b2, int b3, int b4) { assert( _isUint8(b0) && _isUint8(b1) && _isUint8(b2) && _isUint8(b3) && _isUint8(b4), ); if (_length + 4 < _buffer.length) { _buffer[_length] = b0; _buffer[_length + 1] = b1; _buffer[_length + 2] = b2; _buffer[_length + 3] = b3; _buffer[_length + 4] = b4; _length += 5; } else { _growAndEmitBytes(b0, b1, b2, b3, b4); } } void _emitBytes6(int b0, int b1, int b2, int b3, int b4, int b5) { assert( _isUint8(b0) && _isUint8(b1) && _isUint8(b2) && _isUint8(b3) && _isUint8(b4) && _isUint8(b5), ); if (_length + 5 < _buffer.length) { _buffer[_length] = b0; _buffer[_length + 1] = b1; _buffer[_length + 2] = b2; _buffer[_length + 3] = b3; _buffer[_length + 4] = b4; _buffer[_length + 5] = b5; _length += 6; } else { _growAndEmitBytes(b0, b1, b2, b3, b4, b5); } } int _byteAt(int pos) { return _buffer[pos]; } void _setByteAt(int pos, int value) { assert(_isUint8(value)); _buffer[pos] = value; } @pragma('vm:prefer-inline') int _byte0(int v) => v & kByteMask; @pragma('vm:prefer-inline') int _byte1(int v) => (v >> 8) & kByteMask; @pragma('vm:prefer-inline') int _byte2(int v) => (v >> 16) & kByteMask; @pragma('vm:prefer-inline') int _byte3(int v) => (v >> 24) & kByteMask; @pragma('vm:prefer-inline') bool _isInt8(int v) => (kMinInt8 <= v) && (v <= kMaxInt8); @pragma('vm:prefer-inline') bool _isInt24(int v) => (kMinInt24 <= v) && (v <= kMaxInt24); @pragma('vm:prefer-inline') bool _isInt32(int v) => (kMinInt32 <= v) && (v <= kMaxInt32); @pragma('vm:prefer-inline') bool _isUint8(int v) => (v & kByteMask) == v; @pragma('vm:prefer-inline') bool _isUint32(int v) => (v & kUint32Mask) == v; @pragma('vm:prefer-inline') void _emitInstruction0(Opcode opcode) { if (isUnreachable) { return; } _emitByte(opcode.index); } @pragma('vm:prefer-inline') void _emitInstructionA(Opcode opcode, int ra) { if (isUnreachable) { return; } _emitBytes2(opcode.index, ra); } @pragma('vm:prefer-inline') void _emitInstructionD(Opcode opcode, int rd) { if (isUnreachable) { return; } if (_isUint8(rd)) { _emitBytes2(opcode.index, rd); } else { assert(_isUint32(rd)); _emitBytes5( opcode.index + kWideModifier, _byte0(rd), _byte1(rd), _byte2(rd), _byte3(rd), ); } } @pragma('vm:prefer-inline') void _emitInstructionX(Opcode opcode, int rx) { if (isUnreachable) { return; } if (_isInt8(rx)) { _emitBytes2(opcode.index, rx & kByteMask); } else { assert(_isInt32(rx)); _emitBytes5( opcode.index + kWideModifier, _byte0(rx), _byte1(rx), _byte2(rx), _byte3(rx), ); } } @pragma('vm:prefer-inline') void _emitInstructionAE(Opcode opcode, int ra, int re) { if (isUnreachable) { return; } if (_isUint8(re)) { _emitBytes3(opcode.index, ra, re); } else { assert(_isUint32(re)); _emitBytes6( opcode.index + kWideModifier, ra, _byte0(re), _byte1(re), _byte2(re), _byte3(re), ); } } @pragma('vm:prefer-inline') void _emitInstructionAY(Opcode opcode, int ra, int ry) { if (isUnreachable) { return; } if (_isInt8(ry)) { _emitBytes3(opcode.index, ra, ry & kByteMask); } else { assert(_isInt32(ry)); _emitBytes6( opcode.index + kWideModifier, ra, _byte0(ry), _byte1(ry), _byte2(ry), _byte3(ry), ); } } @pragma('vm:prefer-inline') void _emitInstructionDF(Opcode opcode, int rd, int rf) { if (isUnreachable) { return; } if (_isUint8(rd)) { _emitBytes3(opcode.index, rd, rf); } else { assert(_isUint32(rd)); _emitBytes6( opcode.index + kWideModifier, _byte0(rd), _byte1(rd), _byte2(rd), _byte3(rd), rf, ); } } @pragma('vm:prefer-inline') void _emitInstructionABC(Opcode opcode, int ra, int rb, int rc) { if (isUnreachable) { return; } _emitBytes4(opcode.index, ra, rb, rc); } @pragma('vm:prefer-inline') void emitSpecializedBytecode(Opcode opcode) { assert(BytecodeFormats[opcode]!.encoding == Encoding.k0); emitSourcePosition(); _emitInstruction0(opcode); } @pragma('vm:prefer-inline') void _emitJumpInstruction(Opcode opcode, Label label) { assert(isJump(opcode)); if (isUnreachable) { return; } final int target = label.jumpOperand(offset); // Use compact representation only for backwards jumps. // TODO(alexmarkov): generate compact forward jumps as well. if (label.isBound && _isInt8(target)) { _emitBytes2(opcode.index, target & kByteMask); } else { assert(_isInt24(target)); _emitBytes4( opcode.index + kWideModifier, _byte0(target), _byte1(target), _byte2(target), ); } } void _patchJump(int pos, int rt) { final Opcode opcode = Opcode.values[_byteAt(pos) - kWideModifier]; assert(hasWideVariant(opcode)); assert(isJump(opcode)); assert(_isInt24(rt)); _setByteAt(pos + 1, _byte0(rt)); _setByteAt(pos + 2, _byte1(rt)); _setByteAt(pos + 3, _byte2(rt)); } void emitTrap() { _emitInstruction0(Opcode.kTrap); isUnreachable = true; } @pragma('vm:prefer-inline') void emitDrop1() { _emitInstruction0(Opcode.kDrop1); } @pragma('vm:prefer-inline') void emitJump(Label label) { _emitJumpInstruction(Opcode.kJump, label); isUnreachable = true; } @pragma('vm:prefer-inline') void emitJumpIfNoAsserts(Label label) { _emitJumpInstruction(Opcode.kJumpIfNoAsserts, label); } @pragma('vm:prefer-inline') void emitJumpIfNotZeroTypeArgs(Label label) { _emitJumpInstruction(Opcode.kJumpIfNotZeroTypeArgs, label); } @pragma('vm:prefer-inline') void emitJumpIfEqStrict(Label label) { _emitJumpInstruction(Opcode.kJumpIfEqStrict, label); } @pragma('vm:prefer-inline') void emitJumpIfNeStrict(Label label) { _emitJumpInstruction(Opcode.kJumpIfNeStrict, label); } @pragma('vm:prefer-inline') void emitJumpIfTrue(Label label) { _emitJumpInstruction(Opcode.kJumpIfTrue, label); } @pragma('vm:prefer-inline') void emitJumpIfFalse(Label label) { _emitJumpInstruction(Opcode.kJumpIfFalse, label); } @pragma('vm:prefer-inline') void emitJumpIfNull(Label label) { _emitJumpInstruction(Opcode.kJumpIfNull, label); } @pragma('vm:prefer-inline') void emitJumpIfNotNull(Label label) { _emitJumpInstruction(Opcode.kJumpIfNotNull, label); } @pragma('vm:prefer-inline') void emitJumpIfUnchecked(Label label) { _emitJumpInstruction(Opcode.kJumpIfUnchecked, label); } @pragma('vm:prefer-inline') void emitSuspend(Label label) { _emitJumpInstruction(Opcode.kSuspend, label); } @pragma('vm:prefer-inline') void emitReturnTOS() { emitSourcePosition(); _emitInstruction0(Opcode.kReturnTOS); isUnreachable = true; } @pragma('vm:prefer-inline') void emitPush(int rx) { _emitInstructionX(Opcode.kPush, rx); } @pragma('vm:prefer-inline') void emitLoadConstant(int ra, int re) { _emitInstructionAE(Opcode.kLoadConstant, ra, re); } @pragma('vm:prefer-inline') void emitPushConstant(int rd) { _emitInstructionD(Opcode.kPushConstant, rd); } @pragma('vm:prefer-inline') void emitPushNull() { _emitInstruction0(Opcode.kPushNull); } @pragma('vm:prefer-inline') void emitPushTrue() { _emitInstruction0(Opcode.kPushTrue); } @pragma('vm:prefer-inline') void emitPushFalse() { _emitInstruction0(Opcode.kPushFalse); } @pragma('vm:prefer-inline') void emitPushInt(int rx) { _emitInstructionX(Opcode.kPushInt, rx); } @pragma('vm:prefer-inline') void emitStoreLocal(int rx) { _emitInstructionX(Opcode.kStoreLocal, rx); } @pragma('vm:prefer-inline') void emitPopLocal(int rx) { _emitInstructionX(Opcode.kPopLocal, rx); } @pragma('vm:prefer-inline') void emitDirectCall(int rd, int rf) { emitSourcePositionForCall(); _emitInstructionDF(Opcode.kDirectCall, rd, rf); } @pragma('vm:prefer-inline') void emitUncheckedDirectCall(int rd, int rf) { emitSourcePositionForCall(); _emitInstructionDF(Opcode.kUncheckedDirectCall, rd, rf); } @pragma('vm:prefer-inline') void emitInterfaceCall(int rd, int rf) { emitSourcePositionForCall(); _emitInstructionDF(Opcode.kInterfaceCall, rd, rf); } @pragma('vm:prefer-inline') void emitInstantiatedInterfaceCall(int rd, int rf) { emitSourcePositionForCall(); _emitInstructionDF(Opcode.kInstantiatedInterfaceCall, rd, rf); } @pragma('vm:prefer-inline') void emitUncheckedClosureCall(int rd, int rf) { emitSourcePositionForCall(); _emitInstructionDF(Opcode.kUncheckedClosureCall, rd, rf); } @pragma('vm:prefer-inline') void emitUncheckedInterfaceCall(int rd, int rf) { emitSourcePositionForCall(); _emitInstructionDF(Opcode.kUncheckedInterfaceCall, rd, rf); } @pragma('vm:prefer-inline') void emitDynamicCall(int rd, int rf) { emitSourcePositionForCall(); _emitInstructionDF(Opcode.kDynamicCall, rd, rf); } @pragma('vm:prefer-inline') void emitExternalCall(int rd) { _emitInstructionD(Opcode.kExternalCall, rd); } @pragma('vm:prefer-inline') void emitFfiCall(int rd) { _emitInstructionD(Opcode.kFfiCall, rd); } @pragma('vm:prefer-inline') void emitLoadStatic(int rd) { _emitInstructionD(Opcode.kLoadStatic, rd); } @pragma('vm:prefer-inline') void emitStoreStaticTOS(int rd) { emitSourcePosition(); _emitInstructionD(Opcode.kStoreStaticTOS, rd); } @pragma('vm:prefer-inline') void emitCreateArrayTOS() { _emitInstruction0(Opcode.kCreateArrayTOS); } @pragma('vm:prefer-inline') void emitAllocate(int rd) { emitSourcePosition(); _emitInstructionD(Opcode.kAllocate, rd); } @pragma('vm:prefer-inline') void emitAllocateT() { emitSourcePosition(); _emitInstruction0(Opcode.kAllocateT); } @pragma('vm:prefer-inline') void emitStoreIndexedTOS() { _emitInstruction0(Opcode.kStoreIndexedTOS); } @pragma('vm:prefer-inline') void emitStoreFieldTOS(int rd) { _emitInstructionD(Opcode.kStoreFieldTOS, rd); } @pragma('vm:prefer-inline') void emitStoreContextParent() { _emitInstruction0(Opcode.kStoreContextParent); } @pragma('vm:prefer-inline') void emitStoreContextVar(int ra, int re) { _emitInstructionAE(Opcode.kStoreContextVar, ra, re); } @pragma('vm:prefer-inline') void emitLoadFieldTOS(int rd) { _emitInstructionD(Opcode.kLoadFieldTOS, rd); } @pragma('vm:prefer-inline') void emitLoadTypeArgumentsField(int rd) { _emitInstructionD(Opcode.kLoadTypeArgumentsField, rd); } @pragma('vm:prefer-inline') void emitLoadContextParent() { _emitInstruction0(Opcode.kLoadContextParent); } @pragma('vm:prefer-inline') void emitLoadContextVar(int ra, int re) { _emitInstructionAE(Opcode.kLoadContextVar, ra, re); } @pragma('vm:prefer-inline') void emitBooleanNegateTOS() { _emitInstruction0(Opcode.kBooleanNegateTOS); } @pragma('vm:prefer-inline') void emitThrow(int ra) { emitSourcePosition(); _emitInstructionA(Opcode.kThrow, ra); isUnreachable = true; } @pragma('vm:prefer-inline') void emitEntry(int rd) { _emitInstructionD(Opcode.kEntry, rd); } @pragma('vm:prefer-inline') void emitFrame(int rd) { _emitInstructionD(Opcode.kFrame, rd); } @pragma('vm:prefer-inline') void emitSetFrame(int ra) { _emitInstructionA(Opcode.kSetFrame, ra); } @pragma('vm:prefer-inline') void emitAllocateContext(int ra, int re) { _emitInstructionAE(Opcode.kAllocateContext, ra, re); } @pragma('vm:prefer-inline') void emitCloneContext(int ra, int re) { _emitInstructionAE(Opcode.kCloneContext, ra, re); } @pragma('vm:prefer-inline') void emitMoveSpecial(SpecialIndex ra, int ry) { _emitInstructionAY(Opcode.kMoveSpecial, ra.index, ry); } @pragma('vm:prefer-inline') void emitInstantiateType(int rd) { emitSourcePosition(); _emitInstructionD(Opcode.kInstantiateType, rd); } @pragma('vm:prefer-inline') void emitInstantiateTypeArgumentsTOS(int ra, int re) { emitSourcePosition(); _emitInstructionAE(Opcode.kInstantiateTypeArgumentsTOS, ra, re); } @pragma('vm:prefer-inline') void emitAssertAssignable(int ra, int re) { emitSourcePosition(); _emitInstructionAE(Opcode.kAssertAssignable, ra, re); } @pragma('vm:prefer-inline') void emitAssertSubtype() { emitSourcePosition(); _emitInstruction0(Opcode.kAssertSubtype); } @pragma('vm:prefer-inline') void emitCheckStack(int ra) { emitSourcePosition(); _emitInstructionA(Opcode.kCheckStack, ra); } // Nops are never created by the bytecode generator, but rather by // the assembler in order to provide unique pc offsets for source // positions. @pragma('vm:prefer-inline') void _emitNop() { _emitInstruction0(Opcode.kNop); } @pragma('vm:prefer-inline') void emitCheckFunctionTypeArgs(int ra, int re) { emitSourcePosition(); _emitInstructionAE(Opcode.kCheckFunctionTypeArgs, ra, re); } @pragma('vm:prefer-inline') void emitEntryOptional(int ra, int rb, int rc) { _emitInstructionABC(Opcode.kEntryOptional, ra, rb, rc); } @pragma('vm:prefer-inline') void emitEntrySuspendable(int ra, int rb, int rc) { _emitInstructionABC(Opcode.kEntrySuspendable, ra, rb, rc); } @pragma('vm:prefer-inline') void emitAllocateClosure(int rd) { emitSourcePosition(); _emitInstructionD(Opcode.kAllocateClosure, rd); } @pragma('vm:prefer-inline') void emitLoadClosureElement(int rd) { emitSourcePosition(); _emitInstructionD(Opcode.kLoadClosureElement, rd); } @pragma('vm:prefer-inline') void emitStoreClosureElement(int rd) { emitSourcePosition(); _emitInstructionD(Opcode.kStoreClosureElement, rd); } @pragma('vm:prefer-inline') void emitNullCheck(int rd) { emitSourcePosition(); _emitInstructionD(Opcode.kNullCheck, rd); } @pragma('vm:prefer-inline') void emitInitLateField(int rd) { emitSourcePosition(); _emitInstructionD(Opcode.kInitLateField, rd); } @pragma('vm:prefer-inline') void emitPushUninitializedSentinel() { _emitInstruction0(Opcode.kPushUninitializedSentinel); } @pragma('vm:prefer-inline') void emitJumpIfInitialized(Label label) { _emitJumpInstruction(Opcode.kJumpIfInitialized, label); } @pragma('vm:prefer-inline') void emitAllocateRecord(int rd) { _emitInstructionD(Opcode.kAllocateRecord, rd); } @pragma('vm:prefer-inline') void emitLoadRecordField(int rd) { _emitInstructionD(Opcode.kLoadRecordField, rd); } @pragma('vm:prefer-inline') void recordCoverage(RecordedCoverageType type, int fileOffset) { if (!_recordCoverage) return; final index = recordedCoverageArray.add(type, fileOffset); _emitRecordCoverage(type.index, index); } @pragma('vm:prefer-inline') void _emitRecordCoverage(int ra, int re) { _emitInstructionAE(Opcode.kRecordCoverage, ra, re); } }