blob: 27356baec6f61940a1c7fdb310e89b72a42540d0 [file]
// Copyright (c) 2026, 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';
import 'package:cfg/ir/constant_value.dart';
import 'package:cfg/ir/instructions.dart';
import 'package:cfg/ir/ir_to_text.dart';
import 'package:cfg/ir/visitor.dart';
import 'package:cfg/passes/pass.dart';
import 'package:cfg/utils/bit_vector.dart';
import 'package:native_compiler/back_end/assembler.dart';
import 'package:native_compiler/back_end/back_end_state.dart';
import 'package:native_compiler/back_end/code.dart';
import 'package:native_compiler/back_end/code_metadata.dart';
import 'package:native_compiler/back_end/locations.dart';
import 'package:native_compiler/back_end/safepoint.dart';
import 'package:native_compiler/back_end/stack_frame.dart';
import 'package:native_compiler/runtime/object_layout.dart';
import 'package:native_compiler/runtime/vm_defs.dart';
/// Base class for architecture-specific code generator.
///
/// Contains common algorithms and code generation snippets which can be
/// used on multiple architectures.
abstract base class CodeGenerator extends Pass
implements InstructionVisitor<void> {
final BackEndState backEndState;
late final Assembler _asm;
/// Index of the current block in [codeGenBlockOrder].
int _currentBlockIndex = -1;
/// Instruction being generated.
Instruction? _currentInstruction;
/// Maps block preorder number to a preorder number of
/// the first non-empty block.
late final Int32List _firstNonEmptyBlock = _computeFirstNonEmptyBlock();
/// Maps preorder number of a block to
/// a [Label] corresponding to the block entry.
late final List<Label> _blockLabels = List<Label>.generate(
graph.preorder.length,
(i) => Label(),
);
/// Slow paths generated after all blocks.
final List<SlowPath> _slowPaths = [];
/// Metadata describing exception handlers in the generated code.
late final ExceptionHandlers _exceptionHandlers;
/// Metadata describing call sites in the generated code.
late final PcDescriptors _pcDescriptors;
/// Metadata describing moves between exception site and exception handler.
CatchEntryMoves? _catchEntryMoves;
/// Metadata describing source positions in the generated code.
late final CodeSourceMap _codeSourceMap;
/// Metadata describing stack maps for the safepoints in the generated code.
late final CompressedStackMaps _compressedStackMaps;
CodeGenerator(this.backEndState) : super('CodeGen');
VMOffsets get vmOffsets => backEndState.vmOffsets;
ObjectLayout get objectLayout => backEndState.objectLayout;
List<Block> get codeGenBlockOrder => backEndState.codeGenBlockOrder;
StackFrame get stackFrame => backEndState.stackFrame;
Location loc(OperandId operandId) =>
backEndState.operandLocations[operandId]!.physicalLocation;
Location inputLoc(Instruction instr, int inputIndex) =>
loc(OperandId.input(instr.id, inputIndex));
Register inputReg(Instruction instr, int inputIndex) =>
inputLoc(instr, inputIndex) as Register;
Register temporaryReg(Instruction instr, int tempIndex) =>
loc(OperandId.temp(instr.id, tempIndex)) as Register;
Register outputReg(Instruction instr) =>
loc(OperandId.result(instr.id)) as Register;
FPRegister inputFPReg(Instruction instr, int inputIndex) =>
inputLoc(instr, inputIndex) as FPRegister;
FPRegister tempFPReg(Instruction instr, int tempIndex) =>
loc(OperandId.temp(instr.id, tempIndex)) as FPRegister;
FPRegister outputFPReg(Instruction instr) =>
loc(OperandId.result(instr.id)) as FPRegister;
Block firstNonEmptyBlocks(Block block) =>
graph.preorder[_firstNonEmptyBlock[block.preorderNumber]];
Label blockLabel(Block block) => _blockLabels[block.preorderNumber];
bool canFallThroughTo(Block block) {
assert(_currentBlockIndex >= 0);
if (_currentBlockIndex == codeGenBlockOrder.length - 1) {
// The last block cannot fall through.
return false;
}
final nextBlock = codeGenBlockOrder[_currentBlockIndex + 1];
return _firstNonEmptyBlock[block.preorderNumber] ==
_firstNonEmptyBlock[nextBlock.preorderNumber];
}
Safepoint _getCurrentSafepoint(CallSiteKind kind) {
var safepoint = backEndState.safepoints[_currentInstruction!.id];
if (safepoint == null) {
if (kind == .fatalError) {
safepoint = backEndState.safepoints[_currentInstruction!.id] =
Safepoint();
} else {
throw 'No safepoint for ${IrToText.instruction(_currentInstruction!)}';
}
}
return safepoint;
}
void addCallSiteMetadata(CallSiteKind kind) {
final pcOffset = _asm.currentPcOffset;
final exceptionHandler = _currentInstruction!.block!.exceptionHandler;
final exceptionHandlerIndex = (exceptionHandler != null)
? _exceptionHandlers.getHandler(exceptionHandler).index
: -1;
_pcDescriptors.add(
CallSite(
pcOffset,
exceptionHandlerIndex,
_currentInstruction!.sourcePosition,
),
);
if (exceptionHandler != null) {
(_catchEntryMoves ??= CatchEntryMoves()).add(
ExceptionSite(
pcOffset,
// TODO: add moves
),
);
}
_codeSourceMap.add(
CodeSourcePosition(pcOffset, _currentInstruction!.sourcePosition),
);
_compressedStackMaps.add(
pcOffset,
_getCurrentSafepoint(kind),
stackFrame.frameSizeInSlots,
);
}
/// Record [numArgs] outgoing arguments as object pointers
/// at the current safepoint.
void recordOutgoingArgumentsAtSafepoint(CallSiteKind kind, int numArgs) {
assert((0 <= numArgs) && (numArgs <= stackFrame.maxArgumentsStackSlots));
final safepoint = _getCurrentSafepoint(kind);
final frameSize = stackFrame.frameSizeInSlots;
/// TODO: pass arguments on registers.
/// TODO: unboxed arguments.
safepoint.addLiveStackSlots(
frameSize - numArgs,
numArgs,
isObjectPointer: true,
);
}
@override
void run() {
final blocks = codeGenBlockOrder;
assert(blocks.first is EntryBlock);
assert(blocks.length == graph.preorder.length);
final asyncMarker = graph.function.asyncMarker;
_exceptionHandlers = ExceptionHandlers(
hasAsyncHandler: asyncMarker == .Async || asyncMarker == .AsyncStar,
);
_pcDescriptors = PcDescriptors();
_codeSourceMap = CodeSourceMap();
_compressedStackMaps = CompressedStackMaps();
_asm = createAssembler();
enterFrame();
for (int i = 0, n = blocks.length; i < n; ++i) {
_currentBlockIndex = i;
final block = currentBlock = blocks[i];
generateBlock(block);
}
_currentBlockIndex = -1;
for (final slowPath in _slowPaths) {
currentInstruction = _currentInstruction = slowPath.instruction;
_asm.bind(slowPath.entry);
slowPath.generator();
}
currentInstruction = _currentInstruction = null;
backEndState.consumeGeneratedCode(
Code(
graph.function.toString(),
graph.function,
_asm.bytes,
_asm.objectPool,
_exceptionHandlers,
_pcDescriptors,
_catchEntryMoves,
_codeSourceMap,
_compressedStackMaps,
),
);
}
Assembler createAssembler();
void enterFrame();
void generateBlock(Block block) {
currentInstruction = _currentInstruction = block;
_asm.bind(blockLabel(block));
block.accept(this);
for (final instr in block) {
currentInstruction = _currentInstruction = instr;
instr.accept(this);
}
currentInstruction = _currentInstruction = null;
}
/// Returns true if no code should be generated for this block.
bool isEmptyBlock(Block block) => block is! CatchBlock && block.next is Goto;
/// For each block, calculate the first non-empty block
/// which cannot be skipped while generating code. Handles cycles.
Int32List _computeFirstNonEmptyBlock() {
final numBlocks = graph.preorder.length;
final firstNonEmpty = Int32List(numBlocks);
final emptyBlocks = <Block>[];
for (var i = 1; i < numBlocks; ++i) {
if (firstNonEmpty[i] != 0) {
continue;
}
firstNonEmpty[i] = i;
Block block = graph.preorder[i];
if (!isEmptyBlock(block)) continue;
// Collect empty blocks until reaching an non-empty block or
// an empty block which has been seen already.
for (;;) {
emptyBlocks.add(block);
block = block.successors.single;
if (firstNonEmpty[block.preorderNumber] != 0) {
break;
}
firstNonEmpty[block.preorderNumber] = block.preorderNumber;
if (!isEmptyBlock(block)) {
break;
}
}
final nonEmpty = firstNonEmpty[block.preorderNumber];
for (final block in emptyBlocks) {
firstNonEmpty[block.preorderNumber] = nonEmpty;
}
emptyBlocks.clear();
}
return firstNonEmpty;
}
Label addSlowPath(void Function() generator) {
final entry = Label();
_slowPaths.add(SlowPath(_currentInstruction!, entry, generator));
return entry;
}
@override
void visitEntryBlock(EntryBlock instr) {}
@override
void visitJoinBlock(JoinBlock instr) {}
@override
void visitTargetBlock(TargetBlock instr) {}
@override
void visitCatchBlock(CatchBlock instr) {
_exceptionHandlers.getHandler(instr).pcOffset = _asm.currentPcOffset;
}
@override
void visitGoto(Goto instr) {
final target = instr.target;
if (!canFallThroughTo(target)) {
_asm.jump(blockLabel(target));
}
}
@override
void visitTryEntry(TryEntry instr) {
final target = instr.tryBody;
if (!canFallThroughTo(target)) {
_asm.jump(blockLabel(target));
}
}
@override
void visitPhi(Phi instr) {
// Generated via ParallelMove instructions inserted by register allocator.
}
/// Ensure that all destinations are distinct and
/// stack locations are not used both as a source and destination.
static bool _verifyParallelMoveDestinations(ParallelMove instr) {
final destinations = <Location>{};
for (final move in instr.moves) {
final to = switch (move) {
Move() => move.to.physicalLocation,
LoadConstant() => move.to.physicalLocation,
_ => throw 'Unexpected move ${move.runtimeType} $move',
};
if (!destinations.add(to)) {
throw 'Non-unique destination location $to in ${IrToText.instruction(instr)}';
}
}
for (final move in instr.moves) {
if (move is Move) {
final from = move.from.physicalLocation;
final to = move.to.physicalLocation;
if (from != to &&
from is StackLocation &&
destinations.contains(from)) {
throw 'Stack location $from is used both as a source and destination in ${IrToText.instruction(instr)}';
}
}
}
return true;
}
@override
void visitParallelMove(ParallelMove instr) {
// TODO: merge subsequent ParallelMove instructions.
assert(_verifyParallelMoveDestinations(instr));
final moves = <Move>[];
for (final move in instr.moves) {
if (move is Move) {
final from = move.from.physicalLocation;
final to = move.to.physicalLocation;
if (from != to) {
if (from is StackLocation && to is StackLocation) {
// Moves into spill slots cannot participate in cycles.
// Generate them eagerly as they require a temporary register
// which can be occupied while breaking a cycle.
final temp = getMoveTempRegister(RegisterClass.cpu);
generateMove(from, temp);
generateMove(temp, to);
} else {
moves.add(Move(from, to));
}
}
}
}
// The algorithm is described in Laurence Rideau, Bernard Paul Serpette, Xavier Leroy (2008)
// "Tilting at windmills with Coq: formal verification of a compilation algorithm for parallel moves".
final pending = BitVector(moves.length);
final processed = BitVector(moves.length);
for (var i = 0; i < moves.length; ++i) {
if (!processed[i]) {
_generateOneMove(i, moves, pending, processed);
}
}
for (final move in instr.moves) {
if (move is LoadConstant) {
generateLoadConstant(move.value, move.to.physicalLocation);
}
}
}
void _generateOneMove(
int i,
List<Move> moves,
BitVector pending,
BitVector processed,
) {
pending[i] = true;
final dst = moves[i].to;
for (var j = 0; j < moves.length; ++j) {
if (processed[j]) {
continue;
}
if (dst == moves[j].from) {
if (pending[j]) {
final temp = getMoveTempRegister(
(moves[j].from is FPRegister || moves[j].to is FPRegister)
? RegisterClass.fpu
: RegisterClass.cpu,
);
generateMove(moves[j].from, temp);
moves[j].from = temp;
} else {
_generateOneMove(j, moves, pending, processed);
}
}
}
generateMove(moves[i].from, moves[i].to);
processed[i] = true;
}
Location getMoveTempRegister(RegisterClass registerClass);
void generateMove(Location from, Location to);
void generateLoadConstant(ConstantValue value, Location to);
@override
void visitTypeParameters(TypeParameters instr) =>
throw 'Unexpected TypeParameters (should be lowered)';
@override
void visitAllocateListLiteral(AllocateListLiteral instr) =>
throw 'Unexpected AllocateListLiteral (should be lowered)';
@override
void visitAllocateMapLiteral(AllocateMapLiteral instr) =>
throw 'Unexpected AllocateMapLiteral (should be lowered)';
@override
void visitAllocateRecordLiteral(AllocateRecordLiteral instr) =>
throw 'Unexpected AllocateRecordLiteral (should be lowered)';
@override
void visitStringInterpolation(StringInterpolation instr) =>
throw 'Unexpected StringInterpolation (should be lowered)';
@override
void visitInstantiateClosure(InstantiateClosure instr) =>
throw 'Unexpected InstantiateClosure (should be lowered)';
}
class SlowPath(
final Instruction instruction,
final Label entry,
final void Function() generator,
);