// 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 'bytecode_serialization.dart'; enum LocalVariableEntryKind { invalid, scope, variableDeclaration, contextVariable, } abstract class LocalVariableEntry { static const KindMask = 0x0F; static const IsCapturedFlag = 1 << 4; LocalVariableEntryKind get kind; int get flags => 0; final int startPC; void writeContents(BufferedWriter writer); LocalVariableEntry(this.startPC); } class Scope extends LocalVariableEntry { int? endPC; int contextLevel; int position; int? endPosition; final List variables = []; Scope( int startPC, this.endPC, this.contextLevel, this.position, this.endPosition, ) : super(startPC); @override LocalVariableEntryKind get kind => LocalVariableEntryKind.scope; @override void writeContents(BufferedWriter writer) { final endPC = this.endPC; if (endPC == null) { throw '$this is not closed'; } writer.writePackedUInt30(endPC - startPC); writer.writeSLEB128(contextLevel); writer.writePackedUInt30(position + 1); writer.writePackedUInt30(endPosition! + 1); } Scope.readContents(BufferedReader reader, int startPC) : endPC = startPC + reader.readPackedUInt30(), contextLevel = reader.readSLEB128(), position = reader.readPackedUInt30() - 1, endPosition = reader.readPackedUInt30() - 1, super(startPC); @override String toString() => 'scope (pc $startPC-$endPC pos $position-$endPosition context-level $contextLevel)'; } class VariableDeclaration extends LocalVariableEntry { bool isCaptured; int index; int name; int type; int position; int initializedPosition; VariableDeclaration( int startPC, this.isCaptured, this.index, this.name, this.type, this.position, this.initializedPosition, ) : super(startPC); @override LocalVariableEntryKind get kind => LocalVariableEntryKind.variableDeclaration; @override int get flags => (isCaptured ? LocalVariableEntry.IsCapturedFlag : 0); @override void writeContents(BufferedWriter writer) { writer.writeSLEB128(index); writer.writePackedUInt30(name); writer.writePackedUInt30(type); writer.writePackedUInt30(position + 1); writer.writePackedUInt30(initializedPosition + 1); } VariableDeclaration.readContents( BufferedReader reader, int startPC, int flags, ) : isCaptured = (flags & LocalVariableEntry.IsCapturedFlag) != 0, index = reader.readSLEB128(), name = reader.readPackedUInt30(), type = reader.readPackedUInt30(), position = reader.readPackedUInt30() - 1, initializedPosition = reader.readPackedUInt30() - 1, super(startPC); @override String toString() => 'variable $index${isCaptured ? ' (captured)' : ''} pc $startPC, name CP#$name, type CP#$type, pos $position, init-pos $initializedPosition'; } class ContextVariable extends LocalVariableEntry { int index; ContextVariable(int startPC, this.index) : super(startPC); @override LocalVariableEntryKind get kind => LocalVariableEntryKind.contextVariable; @override void writeContents(BufferedWriter writer) { writer.writeSLEB128(index); } ContextVariable.readContents(BufferedReader reader, int startPC) : index = reader.readSLEB128(), super(startPC); @override String toString() => 'context variable $index'; } /// Keeps information about declared local variables. class LocalVariableTable extends BytecodeDeclaration { final scopes = []; final activeScopes = []; ContextVariable? contextVariable; LocalVariableTable(); void enterScope(int pc, int contextLevel, int position) { final scope = new Scope(pc, null, contextLevel, position, null); activeScopes.add(scope); scopes.add(scope); } void declareVariable( int pc, bool isCaptured, int index, int nameCpIndex, int typeCpIndex, int position, int initializedPosition, ) { final variable = new VariableDeclaration( pc, isCaptured, index, nameCpIndex, typeCpIndex, position, initializedPosition, ); activeScopes.last.variables.add(variable); } void leaveScope(int pc, int endPosition) { final scope = activeScopes.removeLast(); scope.endPC = pc; scope.endPosition = endPosition; if (scope.variables.isEmpty && activeScopes.isNotEmpty && scope.contextLevel == activeScopes.last.contextLevel) { scopes.remove(scope); } } void leaveAllScopes(int pc, int endPosition) { while (activeScopes.isNotEmpty) { leaveScope(pc, endPosition); } } void recordContextVariable(int pc, int index) { assert(contextVariable == null); contextVariable = new ContextVariable(pc, index); } bool get isEmpty => scopes.isEmpty && contextVariable == null; bool get isNotEmpty => !isEmpty; bool get hasActiveScopes => activeScopes.isNotEmpty; List getEntries() { final entries = []; final contextVariable = this.contextVariable; if (contextVariable != null) { entries.add(contextVariable); } for (Scope scope in scopes) { entries.add(scope); entries.addAll(scope.variables); } return entries; } void write(BufferedWriter writer) { final entries = getEntries(); writer.writePackedUInt30(entries.length); final encodeStartPC = new SLEB128DeltaEncoder(); for (var entry in entries) { writer.writeByte(entry.kind.index | entry.flags); encodeStartPC.write(writer, entry.startPC); entry.writeContents(writer); } } LocalVariableTable.read(BufferedReader reader) { final int numEntries = reader.readPackedUInt30(); final decodeStartPC = new SLEB128DeltaDecoder(); late Scope scope; for (int i = 0; i < numEntries; ++i) { final int kindAndFlags = reader.readByte(); final LocalVariableEntryKind kind = LocalVariableEntryKind .values[kindAndFlags & LocalVariableEntry.KindMask]; final int flags = kindAndFlags & ~LocalVariableEntry.KindMask; final int startPC = decodeStartPC.read(reader); switch (kind) { case LocalVariableEntryKind.scope: scope = new Scope.readContents(reader, startPC); scopes.add(scope); break; case LocalVariableEntryKind.variableDeclaration: scope.variables.add( new VariableDeclaration.readContents(reader, startPC, flags), ); break; case LocalVariableEntryKind.contextVariable: contextVariable = new ContextVariable.readContents(reader, startPC); break; default: throw 'Unexpected entry kind ${kind}'; } } } Map getBytecodeAnnotations() { final map = {}; for (var entry in getEntries()) { final pc = entry.startPC; if (map[pc] == null) { map[pc] = entry.toString(); } else { map[pc] = "${map[pc]}; $entry"; } } return map; } }