Files
sdk/pkg/dart2bytecode/lib/assembler.dart
T
Tess Strickland fe6ea5e39d [vm,dyn_modules] Rename DebugCheck to Nop and use after debugger() calls.
Normally, bytecodes no longer in use would be renamed to Unused<X>.
However, in the case of DebugCheck, its only use was to call the
debugger when single stepping, and since its original creation, the
interpreter has been changed to call the debugger when single stepping
on every instruction. Thus, DebugCheck instructions are effectively
no-ops, only used as a distinct PC offset for source positions, and this
CL changes their name to reflect this.

This CL also changes the bytecode generator to detect uses of debugger()
from dart:developer and to add a Nop after it, mimicking how
StreamingFlowGraphBuilder recognizes uses of debugger() and adds a
DebugStepCheck instruction afterwards. Doing this instead of just
using asm.emitSourcePosition() at the end of visitStaticInvocation
ensures that the source position isn't overwritten by the next emitted
instruction.

TEST=pkg/vm_service/test/set_sdk_library_debuggable_test

Cq-Include-Trybots: luci.dart.try:vm-dyn-linux-debug-x64-try,vm-dyn-mac-debug-arm64-try
Change-Id: Ie24bcea0b5aeb9e41d7765f25b1cd123bb2565b4
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/480203
Reviewed-by: Alexander Markov <alexmarkov@google.com>
Commit-Queue: Tess Strickland <sstrickl@google.com>
2026-02-19 03:00:23 -08:00

775 lines
20 KiB
Dart

// 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 SourcePositions;
class Label {
final bool allowsBackwardJumps;
List<int>? _jumps = <int>[];
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<int> 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 bool _emitSourcePositions;
bool isUnreachable = false;
int currentSourcePosition = TreeNode.noOffset;
int currentSourcePositionFlags = 0;
BytecodeAssembler(BytecodeOptions options)
: _emitSourcePositions = options.emitSourcePositions;
int get offset => _length;
Uint8List get bytecode => new Uint8List.view(_buffer.buffer, 0, _length);
void bind(Label label) {
final List<int> 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() {
if (_emitSourcePositions && !isUnreachable) {
sourcePositions.add(
offset,
currentSourcePosition == TreeNode.noOffset
? SourcePositions.noSourcePosition
: currentSourcePosition,
currentSourcePositionFlags);
}
}
@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) {
emitSourcePosition();
_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);
}
@pragma('vm:prefer-inline')
void emitNop() {
// Currently, the only reason the bytecode generator emits a no-op
// instruction is when the next instruction to emit is unknown (e.g.,
// at the end of a visit<X> method), but a source position must be
// emitted after the last known instruction and not overwritten by
// the next emitted instruction.
emitSourcePosition();
_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() {
emitSourcePosition();
_emitInstruction0(Opcode.kAllocateClosure);
}
@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);
}
}