| // 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/asm_intrinsics.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/back_end/stub_code_generator.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; | |
| final AsmIntrinsics asmIntrinsics; | |
| 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, this.asmIntrinsics) : super('CodeGen'); | |
| VMOffsets get vmOffsets => backEndState.vmOffsets; | |
| ObjectLayout get objectLayout => backEndState.objectLayout; | |
| StubFactory get stubFactory => backEndState.stubFactory; | |
| 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(); | |
| if (!asmIntrinsics.generate(graph.function, _asm)) { | |
| 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; | |
| _compressedStackMaps.add( | |
| _asm.currentPcOffset, | |
| _getCurrentSafepoint(.exceptionHandler), | |
| stackFrame.frameSizeInSlots, | |
| ); | |
| } | |
| @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, | |
| ); |