mirror of
https://github.com/dolphin-emu/dolphin.git
synced 2025-01-24 15:01:16 +01:00
Parser and Assembler implementations
This commit is contained in:
parent
88cd618b4d
commit
38c15df464
26
Source/Core/Common/Assembler/AssemblerShared.cpp
Normal file
26
Source/Core/Common/Assembler/AssemblerShared.cpp
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
// Copyright 2023 Dolphin Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include "Common/Assembler/AssemblerShared.h"
|
||||||
|
|
||||||
|
#include <fmt/format.h>
|
||||||
|
|
||||||
|
namespace Common::GekkoAssembler
|
||||||
|
{
|
||||||
|
std::string AssemblerError::FormatError() const
|
||||||
|
{
|
||||||
|
const char* space_char = col == 0 ? "" : " ";
|
||||||
|
|
||||||
|
std::string_view line_str = error_line;
|
||||||
|
if (line_str.back() == '\n')
|
||||||
|
{
|
||||||
|
line_str = line_str.substr(0, line_str.length() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt::format("Error on line {0} col {1}:\n"
|
||||||
|
" {2}\n"
|
||||||
|
" {3:{4}}{5:^^{6}}\n"
|
||||||
|
"{7}",
|
||||||
|
line + 1, col + 1, line_str, space_char, col, '^', len, message);
|
||||||
|
}
|
||||||
|
} // namespace Common::GekkoAssembler
|
545
Source/Core/Common/Assembler/AssemblerShared.h
Normal file
545
Source/Core/Common/Assembler/AssemblerShared.h
Normal file
@ -0,0 +1,545 @@
|
|||||||
|
// Copyright 2023 Dolphin Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <variant>
|
||||||
|
|
||||||
|
namespace Common::GekkoAssembler
|
||||||
|
{
|
||||||
|
struct Interval
|
||||||
|
{
|
||||||
|
size_t begin;
|
||||||
|
size_t len;
|
||||||
|
constexpr size_t End() const { return begin + len; }
|
||||||
|
};
|
||||||
|
|
||||||
|
struct AssemblerError
|
||||||
|
{
|
||||||
|
std::string message;
|
||||||
|
std::string_view error_line;
|
||||||
|
size_t line;
|
||||||
|
size_t col;
|
||||||
|
size_t len;
|
||||||
|
|
||||||
|
std::string FormatError() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename Tag, typename T>
|
||||||
|
using Tagged = std::pair<Tag, T>;
|
||||||
|
template <typename Tag, typename T>
|
||||||
|
constexpr const Tag& TagOf(const Tagged<Tag, T>& val)
|
||||||
|
{
|
||||||
|
return std::get<0>(val);
|
||||||
|
}
|
||||||
|
template <typename Tag, typename T>
|
||||||
|
constexpr Tag& TagOf(Tagged<Tag, T>& val)
|
||||||
|
{
|
||||||
|
return std::get<0>(val);
|
||||||
|
}
|
||||||
|
template <typename Tag, typename T>
|
||||||
|
constexpr const T& ValueOf(const Tagged<Tag, T>& val)
|
||||||
|
{
|
||||||
|
return std::get<1>(val);
|
||||||
|
}
|
||||||
|
template <typename Tag, typename T>
|
||||||
|
constexpr T& ValueOf(Tagged<Tag, T>& val)
|
||||||
|
{
|
||||||
|
return std::get<1>(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
using FailureOr = std::variant<AssemblerError, T>;
|
||||||
|
template <typename T>
|
||||||
|
constexpr bool IsFailure(const FailureOr<T>& var)
|
||||||
|
{
|
||||||
|
return std::holds_alternative<AssemblerError>(var);
|
||||||
|
}
|
||||||
|
template <typename T>
|
||||||
|
constexpr AssemblerError& GetFailure(FailureOr<T>& var)
|
||||||
|
{
|
||||||
|
return std::get<AssemblerError>(var);
|
||||||
|
}
|
||||||
|
template <typename T>
|
||||||
|
constexpr const AssemblerError& GetFailure(const FailureOr<T>& var)
|
||||||
|
{
|
||||||
|
return std::get<AssemblerError>(var);
|
||||||
|
}
|
||||||
|
template <typename T>
|
||||||
|
constexpr const T& GetT(const FailureOr<T>& var)
|
||||||
|
{
|
||||||
|
return std::get<T>(var);
|
||||||
|
}
|
||||||
|
template <typename T>
|
||||||
|
constexpr T& GetT(FailureOr<T>& var)
|
||||||
|
{
|
||||||
|
return std::get<T>(var);
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class GekkoDirective
|
||||||
|
{
|
||||||
|
Byte,
|
||||||
|
_2byte,
|
||||||
|
_4byte,
|
||||||
|
_8byte,
|
||||||
|
Float,
|
||||||
|
Double,
|
||||||
|
Locate,
|
||||||
|
PadAlign,
|
||||||
|
Align,
|
||||||
|
Zeros,
|
||||||
|
Skip,
|
||||||
|
DefVar,
|
||||||
|
Ascii,
|
||||||
|
Asciz
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class GekkoMnemonic : size_t
|
||||||
|
{
|
||||||
|
Add,
|
||||||
|
Addc,
|
||||||
|
Adde,
|
||||||
|
Addi,
|
||||||
|
Addic,
|
||||||
|
AddicDot,
|
||||||
|
Addis,
|
||||||
|
Addme,
|
||||||
|
Addze,
|
||||||
|
Divw,
|
||||||
|
Divwu,
|
||||||
|
Mulhw,
|
||||||
|
Mulhwu,
|
||||||
|
Mulli,
|
||||||
|
Mullw,
|
||||||
|
Neg,
|
||||||
|
Subf,
|
||||||
|
Subfc,
|
||||||
|
Subfe,
|
||||||
|
Subfic,
|
||||||
|
Subfme,
|
||||||
|
Subfze,
|
||||||
|
Cmp,
|
||||||
|
Cmpi,
|
||||||
|
Cmpl,
|
||||||
|
Cmpli,
|
||||||
|
And,
|
||||||
|
Andc,
|
||||||
|
AndiDot,
|
||||||
|
AndisDot,
|
||||||
|
Cntlzw,
|
||||||
|
Eqv,
|
||||||
|
Extsb,
|
||||||
|
Extsh,
|
||||||
|
Nand,
|
||||||
|
Nor,
|
||||||
|
Or,
|
||||||
|
Orc,
|
||||||
|
Ori,
|
||||||
|
Oris,
|
||||||
|
Xor,
|
||||||
|
Xori,
|
||||||
|
Xoris,
|
||||||
|
Rlwimi,
|
||||||
|
Rlwinm,
|
||||||
|
Rlwnm,
|
||||||
|
Slw,
|
||||||
|
Sraw,
|
||||||
|
Srawi,
|
||||||
|
Srw,
|
||||||
|
Fadd,
|
||||||
|
Fadds,
|
||||||
|
Fdiv,
|
||||||
|
Fdivs,
|
||||||
|
Fmul,
|
||||||
|
Fmuls,
|
||||||
|
Fres,
|
||||||
|
Frsqrte,
|
||||||
|
Fsub,
|
||||||
|
Fsubs,
|
||||||
|
Fsel,
|
||||||
|
Fmadd,
|
||||||
|
Fmadds,
|
||||||
|
Fmsub,
|
||||||
|
Fmsubs,
|
||||||
|
Fnmadd,
|
||||||
|
Fnmadds,
|
||||||
|
Fnmsub,
|
||||||
|
Fnmsubs,
|
||||||
|
Fctiw,
|
||||||
|
Fctiwz,
|
||||||
|
Frsp,
|
||||||
|
Fcmpo,
|
||||||
|
Fcmpu,
|
||||||
|
Mcrfs,
|
||||||
|
Mffs,
|
||||||
|
Mtfsb0,
|
||||||
|
Mtfsb1,
|
||||||
|
Mtfsf,
|
||||||
|
Mtfsfi,
|
||||||
|
Lbz,
|
||||||
|
Lbzu,
|
||||||
|
Lbzux,
|
||||||
|
Lbzx,
|
||||||
|
Lha,
|
||||||
|
Lhau,
|
||||||
|
Lhaux,
|
||||||
|
Lhax,
|
||||||
|
Lhz,
|
||||||
|
Lhzu,
|
||||||
|
Lhzux,
|
||||||
|
Lhzx,
|
||||||
|
Lwz,
|
||||||
|
Lwzu,
|
||||||
|
Lwzux,
|
||||||
|
Lwzx,
|
||||||
|
Stb,
|
||||||
|
Stbu,
|
||||||
|
Stbux,
|
||||||
|
Stbx,
|
||||||
|
Sth,
|
||||||
|
Sthu,
|
||||||
|
Sthux,
|
||||||
|
Sthx,
|
||||||
|
Stw,
|
||||||
|
Stwu,
|
||||||
|
Stwux,
|
||||||
|
Stwx,
|
||||||
|
Lhbrx,
|
||||||
|
Lwbrx,
|
||||||
|
Sthbrx,
|
||||||
|
Stwbrx,
|
||||||
|
Lmw,
|
||||||
|
Stmw,
|
||||||
|
Lswi,
|
||||||
|
Lswx,
|
||||||
|
Stswi,
|
||||||
|
Stswx,
|
||||||
|
Eieio,
|
||||||
|
Isync,
|
||||||
|
Lwarx,
|
||||||
|
StwcxDot,
|
||||||
|
Sync,
|
||||||
|
Lfd,
|
||||||
|
Lfdu,
|
||||||
|
Lfdux,
|
||||||
|
Lfdx,
|
||||||
|
Lfs,
|
||||||
|
Lfsu,
|
||||||
|
Lfsux,
|
||||||
|
Lfsx,
|
||||||
|
Stfd,
|
||||||
|
Stfdu,
|
||||||
|
Stfdux,
|
||||||
|
Stfdx,
|
||||||
|
Stfiwx,
|
||||||
|
Stfs,
|
||||||
|
Stfsu,
|
||||||
|
Stfsux,
|
||||||
|
Stfsx,
|
||||||
|
Fabs,
|
||||||
|
Fmr,
|
||||||
|
Fnabs,
|
||||||
|
Fneg,
|
||||||
|
B,
|
||||||
|
Bc,
|
||||||
|
Bcctr,
|
||||||
|
Bclr,
|
||||||
|
Crand,
|
||||||
|
Crandc,
|
||||||
|
Creqv,
|
||||||
|
Crnand,
|
||||||
|
Crnor,
|
||||||
|
Cror,
|
||||||
|
Crorc,
|
||||||
|
Crxor,
|
||||||
|
Mcrf,
|
||||||
|
Rfi,
|
||||||
|
Sc,
|
||||||
|
Tw,
|
||||||
|
Twi,
|
||||||
|
Mcrxr,
|
||||||
|
Mfcr,
|
||||||
|
Mfmsr,
|
||||||
|
Mfspr_nobitswap,
|
||||||
|
Mftb_nobitswap,
|
||||||
|
Mtcrf,
|
||||||
|
Mtmsr,
|
||||||
|
Mtspr_nobitswap,
|
||||||
|
Dcbf,
|
||||||
|
Dcbi,
|
||||||
|
Dcbst,
|
||||||
|
Dcbt,
|
||||||
|
Dcbtst,
|
||||||
|
Dcbz,
|
||||||
|
Icbi,
|
||||||
|
Mfsr,
|
||||||
|
Mfsrin,
|
||||||
|
Mtsr,
|
||||||
|
Mtsrin,
|
||||||
|
Tlbie,
|
||||||
|
Tlbsync,
|
||||||
|
Eciwx,
|
||||||
|
Ecowx,
|
||||||
|
Psq_lx,
|
||||||
|
Psq_stx,
|
||||||
|
Psq_lux,
|
||||||
|
Psq_stux,
|
||||||
|
Psq_l,
|
||||||
|
Psq_lu,
|
||||||
|
Psq_st,
|
||||||
|
Psq_stu,
|
||||||
|
Ps_div,
|
||||||
|
Ps_sub,
|
||||||
|
Ps_add,
|
||||||
|
Ps_sel,
|
||||||
|
Ps_res,
|
||||||
|
Ps_mul,
|
||||||
|
Ps_rsqrte,
|
||||||
|
Ps_msub,
|
||||||
|
Ps_madd,
|
||||||
|
Ps_nmsub,
|
||||||
|
Ps_nmadd,
|
||||||
|
Ps_neg,
|
||||||
|
Ps_mr,
|
||||||
|
Ps_nabs,
|
||||||
|
Ps_abs,
|
||||||
|
Ps_sum0,
|
||||||
|
Ps_sum1,
|
||||||
|
Ps_muls0,
|
||||||
|
Ps_muls1,
|
||||||
|
Ps_madds0,
|
||||||
|
Ps_madds1,
|
||||||
|
Ps_cmpu0,
|
||||||
|
Ps_cmpo0,
|
||||||
|
Ps_cmpu1,
|
||||||
|
Ps_cmpo1,
|
||||||
|
Ps_merge00,
|
||||||
|
Ps_merge01,
|
||||||
|
Ps_merge10,
|
||||||
|
Ps_merge11,
|
||||||
|
Dcbz_l,
|
||||||
|
LastMnemonic = Dcbz_l,
|
||||||
|
InvalidMnemonic,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class ExtendedGekkoMnemonic : size_t
|
||||||
|
{
|
||||||
|
Subi,
|
||||||
|
Subis,
|
||||||
|
Subic,
|
||||||
|
SubicDot,
|
||||||
|
Sub,
|
||||||
|
Subc,
|
||||||
|
Cmpwi,
|
||||||
|
Cmpw,
|
||||||
|
Cmplwi,
|
||||||
|
Cmplw,
|
||||||
|
Extlwi,
|
||||||
|
Extrwi,
|
||||||
|
Inslwi,
|
||||||
|
Insrwi,
|
||||||
|
Rotlwi,
|
||||||
|
Rotrwi,
|
||||||
|
Rotlw,
|
||||||
|
Slwi,
|
||||||
|
Srwi,
|
||||||
|
Clrlwi,
|
||||||
|
Clrrwi,
|
||||||
|
Clrlslwi,
|
||||||
|
Bt,
|
||||||
|
Bf,
|
||||||
|
Bdnz,
|
||||||
|
Bdnzt,
|
||||||
|
Bdnzf,
|
||||||
|
Bdz,
|
||||||
|
Bdzt,
|
||||||
|
Bdzf,
|
||||||
|
BtPredict,
|
||||||
|
BfPredict,
|
||||||
|
BdnzPredict,
|
||||||
|
BdnztPredict,
|
||||||
|
BdnzfPredict,
|
||||||
|
BdzPredict,
|
||||||
|
BdztPredict,
|
||||||
|
BdzfPredict,
|
||||||
|
Blr,
|
||||||
|
Btlr,
|
||||||
|
Bflr,
|
||||||
|
Bdnzlr,
|
||||||
|
Bdnztlr,
|
||||||
|
Bdnzflr,
|
||||||
|
Bdzlr,
|
||||||
|
Bdztlr,
|
||||||
|
Bdzflr,
|
||||||
|
BtlrPredict,
|
||||||
|
BflrPredict,
|
||||||
|
BdnzlrPredict,
|
||||||
|
BdnztlrPredict,
|
||||||
|
BdnzflrPredict,
|
||||||
|
BdzlrPredict,
|
||||||
|
BdztlrPredict,
|
||||||
|
BdzflrPredict,
|
||||||
|
Bctr,
|
||||||
|
Btctr,
|
||||||
|
Bfctr,
|
||||||
|
BtctrPredict,
|
||||||
|
BfctrPredict,
|
||||||
|
Blt,
|
||||||
|
Ble,
|
||||||
|
Beq,
|
||||||
|
Bge,
|
||||||
|
Bgt,
|
||||||
|
Bnl,
|
||||||
|
Bne,
|
||||||
|
Bng,
|
||||||
|
Bso,
|
||||||
|
Bns,
|
||||||
|
Bun,
|
||||||
|
Bnu,
|
||||||
|
BltPredict,
|
||||||
|
BlePredict,
|
||||||
|
BeqPredict,
|
||||||
|
BgePredict,
|
||||||
|
BgtPredict,
|
||||||
|
BnlPredict,
|
||||||
|
BnePredict,
|
||||||
|
BngPredict,
|
||||||
|
BsoPredict,
|
||||||
|
BnsPredict,
|
||||||
|
BunPredict,
|
||||||
|
BnuPredict,
|
||||||
|
Bltlr,
|
||||||
|
Blelr,
|
||||||
|
Beqlr,
|
||||||
|
Bgelr,
|
||||||
|
Bgtlr,
|
||||||
|
Bnllr,
|
||||||
|
Bnelr,
|
||||||
|
Bnglr,
|
||||||
|
Bsolr,
|
||||||
|
Bnslr,
|
||||||
|
Bunlr,
|
||||||
|
Bnulr,
|
||||||
|
BltlrPredict,
|
||||||
|
BlelrPredict,
|
||||||
|
BeqlrPredict,
|
||||||
|
BgelrPredict,
|
||||||
|
BgtlrPredict,
|
||||||
|
BnllrPredict,
|
||||||
|
BnelrPredict,
|
||||||
|
BnglrPredict,
|
||||||
|
BsolrPredict,
|
||||||
|
BnslrPredict,
|
||||||
|
BunlrPredict,
|
||||||
|
BnulrPredict,
|
||||||
|
Bltctr,
|
||||||
|
Blectr,
|
||||||
|
Beqctr,
|
||||||
|
Bgectr,
|
||||||
|
Bgtctr,
|
||||||
|
Bnlctr,
|
||||||
|
Bnectr,
|
||||||
|
Bngctr,
|
||||||
|
Bsoctr,
|
||||||
|
Bnsctr,
|
||||||
|
Bunctr,
|
||||||
|
Bnuctr,
|
||||||
|
BltctrPredict,
|
||||||
|
BlectrPredict,
|
||||||
|
BeqctrPredict,
|
||||||
|
BgectrPredict,
|
||||||
|
BgtctrPredict,
|
||||||
|
BnlctrPredict,
|
||||||
|
BnectrPredict,
|
||||||
|
BngctrPredict,
|
||||||
|
BsoctrPredict,
|
||||||
|
BnsctrPredict,
|
||||||
|
BunctrPredict,
|
||||||
|
BnuctrPredict,
|
||||||
|
Crset,
|
||||||
|
Crclr,
|
||||||
|
Crmove,
|
||||||
|
Crnot,
|
||||||
|
Twlt,
|
||||||
|
Twlti,
|
||||||
|
Twle,
|
||||||
|
Twlei,
|
||||||
|
Tweq,
|
||||||
|
Tweqi,
|
||||||
|
Twge,
|
||||||
|
Twgei,
|
||||||
|
Twgt,
|
||||||
|
Twgti,
|
||||||
|
Twnl,
|
||||||
|
Twnli,
|
||||||
|
Twne,
|
||||||
|
Twnei,
|
||||||
|
Twng,
|
||||||
|
Twngi,
|
||||||
|
Twllt,
|
||||||
|
Twllti,
|
||||||
|
Twlle,
|
||||||
|
Twllei,
|
||||||
|
Twlge,
|
||||||
|
Twlgei,
|
||||||
|
Twlgt,
|
||||||
|
Twlgti,
|
||||||
|
Twlnl,
|
||||||
|
Twlnli,
|
||||||
|
Twlng,
|
||||||
|
Twlngi,
|
||||||
|
Trap,
|
||||||
|
Mtxer,
|
||||||
|
Mfxer,
|
||||||
|
Mtlr,
|
||||||
|
Mflr,
|
||||||
|
Mtctr,
|
||||||
|
Mfctr,
|
||||||
|
Mtdsisr,
|
||||||
|
Mfdsisr,
|
||||||
|
Mtdar,
|
||||||
|
Mfdar,
|
||||||
|
Mtdec,
|
||||||
|
Mfdec,
|
||||||
|
Mtsdr1,
|
||||||
|
Mfsdr1,
|
||||||
|
Mtsrr0,
|
||||||
|
Mfsrr0,
|
||||||
|
Mtsrr1,
|
||||||
|
Mfsrr1,
|
||||||
|
Mtasr,
|
||||||
|
Mfasr,
|
||||||
|
Mtear,
|
||||||
|
Mfear,
|
||||||
|
Mttbl,
|
||||||
|
Mftbl,
|
||||||
|
Mttbu,
|
||||||
|
Mftbu,
|
||||||
|
Mtsprg,
|
||||||
|
Mfsprg,
|
||||||
|
Mtibatu,
|
||||||
|
Mfibatu,
|
||||||
|
Mtibatl,
|
||||||
|
Mfibatl,
|
||||||
|
Mtdbatu,
|
||||||
|
Mfdbatu,
|
||||||
|
Mtdbatl,
|
||||||
|
Mfdbatl,
|
||||||
|
Nop,
|
||||||
|
Li,
|
||||||
|
Lis,
|
||||||
|
La,
|
||||||
|
Mr,
|
||||||
|
Not,
|
||||||
|
Mtcr,
|
||||||
|
Mfspr,
|
||||||
|
Mftb,
|
||||||
|
Mtspr,
|
||||||
|
LastMnemonic = Mtspr,
|
||||||
|
InvalidMnemonic
|
||||||
|
};
|
||||||
|
} // namespace Common::GekkoAssembler
|
1482
Source/Core/Common/Assembler/AssemblerTables.cpp
Normal file
1482
Source/Core/Common/Assembler/AssemblerTables.cpp
Normal file
File diff suppressed because it is too large
Load Diff
152
Source/Core/Common/Assembler/AssemblerTables.h
Normal file
152
Source/Core/Common/Assembler/AssemblerTables.h
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
// Copyright 2023 Dolphin Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <optional>
|
||||||
|
#include <string_view>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "Common/Assembler/AssemblerShared.h"
|
||||||
|
#include "Common/Assembler/CaseInsensitiveDict.h"
|
||||||
|
#include "Common/CommonTypes.h"
|
||||||
|
|
||||||
|
namespace Common::GekkoAssembler::detail
|
||||||
|
{
|
||||||
|
///////////////////
|
||||||
|
// PARSER TABLES //
|
||||||
|
///////////////////
|
||||||
|
enum class ParseAlg
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
Op1,
|
||||||
|
NoneOrOp1,
|
||||||
|
Op1Off1,
|
||||||
|
Op2,
|
||||||
|
Op1Or2,
|
||||||
|
Op3,
|
||||||
|
Op2Or3,
|
||||||
|
Op4,
|
||||||
|
Op5,
|
||||||
|
Op1Off1Op2,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ParseInfo
|
||||||
|
{
|
||||||
|
size_t mnemonic_index;
|
||||||
|
ParseAlg parse_algorithm;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mapping of SPRG names to values
|
||||||
|
extern const CaseInsensitiveDict<u32, '_'> sprg_map;
|
||||||
|
// Mapping of directive names to an enumeration
|
||||||
|
extern const CaseInsensitiveDict<GekkoDirective> directives_map;
|
||||||
|
// Mapping of normal Gekko mnemonics to their index and argument form
|
||||||
|
extern const CaseInsensitiveDict<ParseInfo, '.', '_'> mnemonic_tokens;
|
||||||
|
// Mapping of extended Gekko mnemonics to their index and argument form
|
||||||
|
extern const CaseInsensitiveDict<ParseInfo, '.', '_', '+', '-'> extended_mnemonic_tokens;
|
||||||
|
|
||||||
|
//////////////////////
|
||||||
|
// ASSEMBLER TABLES //
|
||||||
|
//////////////////////
|
||||||
|
constexpr size_t MAX_OPERANDS = 5;
|
||||||
|
|
||||||
|
struct OperandList
|
||||||
|
{
|
||||||
|
std::array<Tagged<Interval, u32>, MAX_OPERANDS> list;
|
||||||
|
u32 count;
|
||||||
|
bool overfill;
|
||||||
|
|
||||||
|
constexpr u32 operator[](size_t index) const { return ValueOf(list[index]); }
|
||||||
|
constexpr u32& operator[](size_t index) { return ValueOf(list[index]); }
|
||||||
|
|
||||||
|
void Insert(size_t before, u32 val);
|
||||||
|
|
||||||
|
template <typename It>
|
||||||
|
void Copy(It begin, It end)
|
||||||
|
{
|
||||||
|
count = 0;
|
||||||
|
for (auto& i : list)
|
||||||
|
{
|
||||||
|
if (begin == end)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
i = *begin;
|
||||||
|
begin++;
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
overfill = begin != end;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct OperandDesc
|
||||||
|
{
|
||||||
|
u32 mask;
|
||||||
|
struct
|
||||||
|
{
|
||||||
|
u32 shift : 31;
|
||||||
|
bool is_signed : 1;
|
||||||
|
};
|
||||||
|
u32 MaxVal() const;
|
||||||
|
u32 MinVal() const;
|
||||||
|
u32 TruncBits() const;
|
||||||
|
|
||||||
|
bool Fits(u32 val) const;
|
||||||
|
u32 Fit(u32 val) const;
|
||||||
|
};
|
||||||
|
|
||||||
|
// MnemonicDesc holds the machine-code template for mnemonics
|
||||||
|
struct MnemonicDesc
|
||||||
|
{
|
||||||
|
// Initial value for a given mnemonic (opcode, func code, LK, AA, OE)
|
||||||
|
const u32 initial_value;
|
||||||
|
const u32 operand_count;
|
||||||
|
// Masks for operands
|
||||||
|
std::array<OperandDesc, MAX_OPERANDS> operand_masks;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ExtendedMnemonicDesc holds the name of the mnemonic it transforms to as well as a
|
||||||
|
// transformer callback to translate the operands into the correct form for the base mnemonic
|
||||||
|
struct ExtendedMnemonicDesc
|
||||||
|
{
|
||||||
|
size_t mnemonic_index;
|
||||||
|
void (*transform_operands)(OperandList&);
|
||||||
|
};
|
||||||
|
|
||||||
|
static constexpr size_t NUM_MNEMONICS = static_cast<size_t>(GekkoMnemonic::LastMnemonic) + 1;
|
||||||
|
static constexpr size_t NUM_EXT_MNEMONICS =
|
||||||
|
static_cast<size_t>(ExtendedGekkoMnemonic::LastMnemonic) + 1;
|
||||||
|
static constexpr size_t VARIANT_PERMUTATIONS = 4;
|
||||||
|
|
||||||
|
// Table for mapping mnemonic+variants to their descriptors
|
||||||
|
extern const std::array<MnemonicDesc, NUM_MNEMONICS * VARIANT_PERMUTATIONS> mnemonics;
|
||||||
|
// Table for mapping extended mnemonic+variants to their descriptors
|
||||||
|
extern const std::array<ExtendedMnemonicDesc, NUM_EXT_MNEMONICS * VARIANT_PERMUTATIONS>
|
||||||
|
extended_mnemonics;
|
||||||
|
|
||||||
|
//////////////////
|
||||||
|
// LEXER TABLES //
|
||||||
|
//////////////////
|
||||||
|
|
||||||
|
// In place of the reliace on std::regex, DFAs will be defined for matching sufficiently complex
|
||||||
|
// tokens This gives an extra benefit of providing reasons for match failures
|
||||||
|
using TransitionF = bool (*)(char c);
|
||||||
|
using DfaEdge = std::pair<TransitionF, size_t>;
|
||||||
|
struct DfaNode
|
||||||
|
{
|
||||||
|
std::vector<DfaEdge> edges;
|
||||||
|
// If nullopt: this is a final node
|
||||||
|
// If string: invalid reason
|
||||||
|
std::optional<std::string_view> match_failure_reason;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Floating point strings that will be accepted by std::stof/std::stod
|
||||||
|
// regex: [\+-]?(\d+(\.\d+)?|\.\d+)(e[\+-]?\d+)?
|
||||||
|
extern const std::vector<DfaNode> float_dfa;
|
||||||
|
// C-style strings
|
||||||
|
// regex: "([^\\\n]|\\([0-7]{1,3}|x[0-9a-fA-F]+|[^x0-7\n]))*"
|
||||||
|
extern const std::vector<DfaNode> string_dfa;
|
||||||
|
} // namespace Common::GekkoAssembler::detail
|
126
Source/Core/Common/Assembler/CaseInsensitiveDict.h
Normal file
126
Source/Core/Common/Assembler/CaseInsensitiveDict.h
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
// Copyright 2023 Dolphin Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <type_traits>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace Common::GekkoAssembler::detail
|
||||||
|
{
|
||||||
|
// Hacky implementation of a case insensitive alphanumeric trie supporting extended entries
|
||||||
|
// Standing in for std::map to support case-insensitive lookups while allowing string_views in
|
||||||
|
// lookups
|
||||||
|
template <typename V, char... ExtraMatches>
|
||||||
|
class CaseInsensitiveDict
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
CaseInsensitiveDict(const std::initializer_list<std::pair<std::string_view, V>>& il)
|
||||||
|
{
|
||||||
|
for (auto&& [k, v] : il)
|
||||||
|
{
|
||||||
|
Add(k, v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
V const* Find(const T& key) const
|
||||||
|
{
|
||||||
|
auto&& [last_e, it] = TryFind(key);
|
||||||
|
if (it == key.cend() && last_e->_val)
|
||||||
|
{
|
||||||
|
return &*last_e->_val;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
static constexpr size_t NUM_CONNS = 36 + sizeof...(ExtraMatches);
|
||||||
|
static constexpr uint32_t INVALID_CONN = static_cast<uint32_t>(-1);
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct TrieEntry
|
||||||
|
{
|
||||||
|
std::array<uint32_t, 36 + sizeof...(ExtraMatches)> _conns;
|
||||||
|
std::optional<V> _val;
|
||||||
|
|
||||||
|
TrieEntry() { std::fill(_conns.begin(), _conns.end(), INVALID_CONN); }
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr size_t IndexOf(char c) const
|
||||||
|
{
|
||||||
|
size_t idx;
|
||||||
|
if (std::isalpha(c))
|
||||||
|
{
|
||||||
|
idx = std::tolower(c) - 'a';
|
||||||
|
}
|
||||||
|
else if (std::isdigit(c))
|
||||||
|
{
|
||||||
|
idx = c - '0' + 26;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
idx = 36;
|
||||||
|
// Expands to an equivalent for loop over ExtraMatches
|
||||||
|
if constexpr (sizeof...(ExtraMatches) > 0)
|
||||||
|
{
|
||||||
|
(void)((c != ExtraMatches ? ++idx, true : false) && ...);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return idx;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
auto TryFind(const T& key) const -> std::pair<TrieEntry const*, decltype(key.cbegin())>
|
||||||
|
{
|
||||||
|
std::pair<TrieEntry const*, decltype(key.cbegin())> ret(&m_root_entry, key.cbegin());
|
||||||
|
const auto k_end = key.cend();
|
||||||
|
|
||||||
|
for (; ret.second != k_end; ret.second++)
|
||||||
|
{
|
||||||
|
const size_t idx = IndexOf(*ret.second);
|
||||||
|
if (idx >= NUM_CONNS || ret.first->_conns[idx] == INVALID_CONN)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
ret.first = &m_entry_pool[ret.first->_conns[idx]];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
auto TryFind(const T& key) -> std::pair<TrieEntry*, decltype(key.cbegin())>
|
||||||
|
{
|
||||||
|
auto&& [e_const, it] =
|
||||||
|
const_cast<CaseInsensitiveDict<V, ExtraMatches...> const*>(this)->TryFind(key);
|
||||||
|
return {const_cast<TrieEntry*>(e_const), it};
|
||||||
|
}
|
||||||
|
|
||||||
|
void Add(std::string_view key, const V& val)
|
||||||
|
{
|
||||||
|
auto&& [last_e, it] = TryFind(key);
|
||||||
|
if (it != key.cend())
|
||||||
|
{
|
||||||
|
for (; it != key.cend(); it++)
|
||||||
|
{
|
||||||
|
const size_t idx = IndexOf(*it);
|
||||||
|
if (idx >= NUM_CONNS)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
last_e->_conns[idx] = static_cast<uint32_t>(m_entry_pool.size());
|
||||||
|
last_e = &m_entry_pool.emplace_back();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
last_e->_val = val;
|
||||||
|
}
|
||||||
|
|
||||||
|
TrieEntry m_root_entry;
|
||||||
|
std::vector<TrieEntry> m_entry_pool;
|
||||||
|
};
|
||||||
|
} // namespace Common::GekkoAssembler::detail
|
189
Source/Core/Common/Assembler/GekkoAssembler.cpp
Normal file
189
Source/Core/Common/Assembler/GekkoAssembler.cpp
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
// Copyright 2023 Dolphin Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include "Common/Assembler/GekkoAssembler.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <fmt/format.h>
|
||||||
|
|
||||||
|
#include "Common/Assembler/AssemblerShared.h"
|
||||||
|
#include "Common/Assembler/AssemblerTables.h"
|
||||||
|
#include "Common/Assembler/GekkoIRGen.h"
|
||||||
|
#include "Common/Assert.h"
|
||||||
|
#include "Common/CommonTypes.h"
|
||||||
|
|
||||||
|
namespace Common::GekkoAssembler
|
||||||
|
{
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
using namespace Common::GekkoAssembler::detail;
|
||||||
|
|
||||||
|
FailureOr<u32> FillInstruction(const MnemonicDesc& desc, const OperandList& operands,
|
||||||
|
std::string_view inst_line)
|
||||||
|
{
|
||||||
|
// Parser shouldn't allow this to pass
|
||||||
|
ASSERT_MSG(COMMON, desc.operand_count == operands.count && !operands.overfill,
|
||||||
|
"Unexpected operand count mismatch for instruction {}. Expected {} but found {}",
|
||||||
|
inst_line, desc.operand_count, operands.overfill ? 6 : operands.count);
|
||||||
|
|
||||||
|
u32 instruction = desc.initial_value;
|
||||||
|
for (u32 i = 0; i < operands.count; i++)
|
||||||
|
{
|
||||||
|
if (!desc.operand_masks[i].Fits(operands[i]))
|
||||||
|
{
|
||||||
|
std::string message;
|
||||||
|
const u32 trunc_bits = desc.operand_masks[i].TruncBits();
|
||||||
|
if (trunc_bits == 0)
|
||||||
|
{
|
||||||
|
if (desc.operand_masks[i].is_signed)
|
||||||
|
{
|
||||||
|
message = fmt::format("{:#x} not between {:#x} and {:#x}", static_cast<s32>(operands[i]),
|
||||||
|
static_cast<s32>(desc.operand_masks[i].MinVal()),
|
||||||
|
static_cast<s32>(desc.operand_masks[i].MaxVal()));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
message = fmt::format("{:#x} not between {:#x} and {:#x}", operands[i],
|
||||||
|
desc.operand_masks[i].MinVal(), desc.operand_masks[i].MaxVal());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (desc.operand_masks[i].is_signed)
|
||||||
|
{
|
||||||
|
message = fmt::format("{:#x} not between {:#x} and {:#x} or not aligned to {}",
|
||||||
|
static_cast<s32>(operands[i]),
|
||||||
|
static_cast<s32>(desc.operand_masks[i].MinVal()),
|
||||||
|
static_cast<s32>(desc.operand_masks[i].MaxVal()), trunc_bits + 1);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
message = fmt::format("{:#x} not between {:#x} and {:#x} or not aligned to {}",
|
||||||
|
operands[i], desc.operand_masks[i].MinVal(),
|
||||||
|
desc.operand_masks[i].MaxVal(), trunc_bits + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return AssemblerError{std::move(message), "", 0, TagOf(operands.list[i]).begin,
|
||||||
|
TagOf(operands.list[i]).len};
|
||||||
|
}
|
||||||
|
instruction |= desc.operand_masks[i].Fit(operands[i]);
|
||||||
|
}
|
||||||
|
return instruction;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AdjustOperandsForGas(GekkoMnemonic mnemonic, OperandList& ops_list)
|
||||||
|
{
|
||||||
|
switch (mnemonic)
|
||||||
|
{
|
||||||
|
case GekkoMnemonic::Cmp:
|
||||||
|
case GekkoMnemonic::Cmpl:
|
||||||
|
case GekkoMnemonic::Cmpi:
|
||||||
|
case GekkoMnemonic::Cmpli:
|
||||||
|
if (ops_list.count < 4)
|
||||||
|
{
|
||||||
|
ops_list.Insert(0, 0);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case GekkoMnemonic::Addis:
|
||||||
|
// Because GAS wants to allow for addis and lis to work nice with absolute addresses, the
|
||||||
|
// immediate operand should also "fit" into the _UIMM field, so just turn a valid UIMM into a
|
||||||
|
// SIMM
|
||||||
|
if (ops_list[2] >= 0x8000 && ops_list[2] <= 0xffff)
|
||||||
|
{
|
||||||
|
ops_list[2] = ops_list[2] - 0x10000;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void CodeBlock::PushBigEndian(u32 val)
|
||||||
|
{
|
||||||
|
instructions.push_back((val >> 24) & 0xff);
|
||||||
|
instructions.push_back((val >> 16) & 0xff);
|
||||||
|
instructions.push_back((val >> 8) & 0xff);
|
||||||
|
instructions.push_back(val & 0xff);
|
||||||
|
}
|
||||||
|
|
||||||
|
FailureOr<std::vector<CodeBlock>> Assemble(std::string_view instruction,
|
||||||
|
u32 current_instruction_address)
|
||||||
|
{
|
||||||
|
FailureOr<detail::GekkoIR> parse_result =
|
||||||
|
detail::ParseToIR(instruction, current_instruction_address);
|
||||||
|
if (IsFailure(parse_result))
|
||||||
|
{
|
||||||
|
return GetFailure(parse_result);
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto& parsed_blocks = GetT(parse_result).blocks;
|
||||||
|
const auto& operands = GetT(parse_result).operand_pool;
|
||||||
|
std::vector<CodeBlock> out_blocks;
|
||||||
|
|
||||||
|
for (const detail::IRBlock& parsed_block : parsed_blocks)
|
||||||
|
{
|
||||||
|
CodeBlock new_block(parsed_block.block_address);
|
||||||
|
for (const detail::ChunkVariant& chunk : parsed_block.chunks)
|
||||||
|
{
|
||||||
|
if (std::holds_alternative<detail::InstChunk>(chunk))
|
||||||
|
{
|
||||||
|
for (const detail::GekkoInstruction& parsed_inst : std::get<detail::InstChunk>(chunk))
|
||||||
|
{
|
||||||
|
OperandList adjusted_ops;
|
||||||
|
ASSERT(parsed_inst.op_interval.len <= MAX_OPERANDS);
|
||||||
|
adjusted_ops.Copy(operands.begin() + parsed_inst.op_interval.begin,
|
||||||
|
operands.begin() + parsed_inst.op_interval.End());
|
||||||
|
|
||||||
|
size_t idx = parsed_inst.mnemonic_index;
|
||||||
|
if (parsed_inst.is_extended)
|
||||||
|
{
|
||||||
|
extended_mnemonics[idx].transform_operands(adjusted_ops);
|
||||||
|
idx = extended_mnemonics[idx].mnemonic_index;
|
||||||
|
}
|
||||||
|
|
||||||
|
AdjustOperandsForGas(static_cast<GekkoMnemonic>(idx >> 2), adjusted_ops);
|
||||||
|
|
||||||
|
FailureOr<u32> inst = FillInstruction(mnemonics[idx], adjusted_ops, parsed_inst.raw_text);
|
||||||
|
if (IsFailure(inst))
|
||||||
|
{
|
||||||
|
GetFailure(inst).error_line = parsed_inst.raw_text;
|
||||||
|
GetFailure(inst).line = parsed_inst.line_number;
|
||||||
|
return GetFailure(inst);
|
||||||
|
}
|
||||||
|
|
||||||
|
new_block.PushBigEndian(GetT(inst));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (std::holds_alternative<detail::ByteChunk>(chunk))
|
||||||
|
{
|
||||||
|
detail::ByteChunk byte_arr = std::get<detail::ByteChunk>(chunk);
|
||||||
|
new_block.instructions.insert(new_block.instructions.end(), byte_arr.begin(),
|
||||||
|
byte_arr.end());
|
||||||
|
}
|
||||||
|
else if (std::holds_alternative<detail::PadChunk>(chunk))
|
||||||
|
{
|
||||||
|
detail::PadChunk pad_len = std::get<detail::PadChunk>(chunk);
|
||||||
|
new_block.instructions.insert(new_block.instructions.end(), pad_len, 0);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ASSERT(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!new_block.instructions.empty())
|
||||||
|
{
|
||||||
|
out_blocks.emplace_back(std::move(new_block));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out_blocks;
|
||||||
|
}
|
||||||
|
} // namespace Common::GekkoAssembler
|
29
Source/Core/Common/Assembler/GekkoAssembler.h
Normal file
29
Source/Core/Common/Assembler/GekkoAssembler.h
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
// Copyright 2023 Dolphin Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string_view>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "Common/Assembler/AssemblerShared.h"
|
||||||
|
#include "Common/CommonTypes.h"
|
||||||
|
|
||||||
|
namespace Common::GekkoAssembler
|
||||||
|
{
|
||||||
|
struct CodeBlock
|
||||||
|
{
|
||||||
|
CodeBlock(u32 address) : block_address(address) {}
|
||||||
|
|
||||||
|
void PushBigEndian(u32 val);
|
||||||
|
|
||||||
|
u32 block_address;
|
||||||
|
std::vector<u8> instructions;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Common::GekkoAssember::Assemble - Core routine for assembling Gekko/Broadway instructions
|
||||||
|
// Supports the full Gekko ISA, as well as the extended mnemonics defined by the book "PowerPC
|
||||||
|
// Microprocessor Family: The Programming Environments" The input assembly is fully parsed and
|
||||||
|
// assembled with a base address specified by the base_virtual_address
|
||||||
|
FailureOr<std::vector<CodeBlock>> Assemble(std::string_view assembly, u32 base_virtual_address);
|
||||||
|
} // namespace Common::GekkoAssembler
|
832
Source/Core/Common/Assembler/GekkoIRGen.cpp
Normal file
832
Source/Core/Common/Assembler/GekkoIRGen.cpp
Normal file
@ -0,0 +1,832 @@
|
|||||||
|
// Copyright 2023 Dolphin Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include "Common/Assembler/GekkoIRGen.h"
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
#include <map>
|
||||||
|
#include <numeric>
|
||||||
|
#include <set>
|
||||||
|
#include <stack>
|
||||||
|
#include <variant>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <fmt/format.h>
|
||||||
|
|
||||||
|
#include "Common/Assembler/AssemblerShared.h"
|
||||||
|
#include "Common/Assembler/GekkoParser.h"
|
||||||
|
#include "Common/Assert.h"
|
||||||
|
#include "Common/BitUtils.h"
|
||||||
|
|
||||||
|
namespace Common::GekkoAssembler::detail
|
||||||
|
{
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
class GekkoIRPlugin : public ParsePlugin
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
GekkoIRPlugin(GekkoIR& result, u32 base_addr)
|
||||||
|
: m_output_result(result), m_active_var(nullptr), m_operand_scan_begin(0)
|
||||||
|
{
|
||||||
|
m_active_block = &m_output_result.blocks.emplace_back(base_addr);
|
||||||
|
}
|
||||||
|
virtual ~GekkoIRPlugin() = default;
|
||||||
|
|
||||||
|
void OnDirectivePre(GekkoDirective directive) override;
|
||||||
|
void OnDirectivePost(GekkoDirective directive) override;
|
||||||
|
void OnInstructionPre(const ParseInfo& mnemonic_info, bool extended) override;
|
||||||
|
void OnInstructionPost(const ParseInfo& mnemonic_info, bool extended) override;
|
||||||
|
void OnOperandPre() override;
|
||||||
|
void OnOperandPost() override;
|
||||||
|
void OnResolvedExprPost() override;
|
||||||
|
void OnOperator(AsmOp operation) override;
|
||||||
|
void OnTerminal(Terminal type, const AssemblerToken& val) override;
|
||||||
|
void OnHiaddr(std::string_view id) override;
|
||||||
|
void OnLoaddr(std::string_view id) override;
|
||||||
|
void OnCloseParen(ParenType type) override;
|
||||||
|
void OnLabelDecl(std::string_view name) override;
|
||||||
|
void OnVarDecl(std::string_view name) override;
|
||||||
|
void PostParseAction() override;
|
||||||
|
|
||||||
|
u32 CurrentAddress() const;
|
||||||
|
std::optional<u64> LookupVar(std::string_view lab);
|
||||||
|
std::optional<u32> LookupLabel(std::string_view lab);
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
T& GetChunk();
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
void AddBytes(T val);
|
||||||
|
|
||||||
|
void AddStringBytes(std::string_view str, bool null_term);
|
||||||
|
|
||||||
|
void PadAlign(u32 bits);
|
||||||
|
void PadSpace(size_t space);
|
||||||
|
|
||||||
|
void StartBlock(u32 address);
|
||||||
|
void StartBlockAlign(u32 bits);
|
||||||
|
void StartInstruction(size_t mnemonic_index, bool extended);
|
||||||
|
void FinishInstruction();
|
||||||
|
void SaveOperandFixup(size_t str_left, size_t str_right);
|
||||||
|
|
||||||
|
void AddBinaryEvaluator(u32 (*evaluator)(u32, u32));
|
||||||
|
void AddUnaryEvaluator(u32 (*evaluator)(u32));
|
||||||
|
void AddAbsoluteAddressConv();
|
||||||
|
void AddLiteral(u32 lit);
|
||||||
|
void AddSymbolResolve(std::string_view sym, bool absolute);
|
||||||
|
|
||||||
|
void RunFixups();
|
||||||
|
|
||||||
|
void EvalOperatorRel(AsmOp operation);
|
||||||
|
void EvalOperatorAbs(AsmOp operation);
|
||||||
|
void EvalTerminalRel(Terminal type, const AssemblerToken& tok);
|
||||||
|
void EvalTerminalAbs(Terminal type, const AssemblerToken& tok);
|
||||||
|
|
||||||
|
private:
|
||||||
|
enum class EvalMode
|
||||||
|
{
|
||||||
|
RelAddrDoublePass,
|
||||||
|
AbsAddrSinglePass,
|
||||||
|
};
|
||||||
|
|
||||||
|
GekkoIR& m_output_result;
|
||||||
|
|
||||||
|
IRBlock* m_active_block;
|
||||||
|
GekkoInstruction m_build_inst;
|
||||||
|
u64* m_active_var;
|
||||||
|
size_t m_operand_scan_begin;
|
||||||
|
|
||||||
|
std::map<std::string, u32, std::less<>> m_labels;
|
||||||
|
std::map<std::string, u64, std::less<>> m_constants;
|
||||||
|
std::set<std::string> m_symset;
|
||||||
|
|
||||||
|
EvalMode m_evaluation_mode;
|
||||||
|
|
||||||
|
// For operand parsing
|
||||||
|
std::stack<std::function<u32()>> m_fixup_stack;
|
||||||
|
std::vector<std::function<u32()>> m_operand_fixups;
|
||||||
|
size_t m_operand_str_start;
|
||||||
|
|
||||||
|
// For directive parsing
|
||||||
|
std::vector<u64> m_eval_stack;
|
||||||
|
std::variant<std::vector<float>, std::vector<double>> m_floats_list;
|
||||||
|
std::string_view m_string_lit;
|
||||||
|
GekkoDirective m_active_directive;
|
||||||
|
};
|
||||||
|
|
||||||
|
///////////////
|
||||||
|
// OVERRIDES //
|
||||||
|
///////////////
|
||||||
|
|
||||||
|
void GekkoIRPlugin::OnDirectivePre(GekkoDirective directive)
|
||||||
|
{
|
||||||
|
m_evaluation_mode = EvalMode::AbsAddrSinglePass;
|
||||||
|
m_active_directive = directive;
|
||||||
|
m_eval_stack = std::vector<u64>{};
|
||||||
|
|
||||||
|
switch (directive)
|
||||||
|
{
|
||||||
|
case GekkoDirective::Float:
|
||||||
|
m_floats_list = std::vector<float>{};
|
||||||
|
break;
|
||||||
|
case GekkoDirective::Double:
|
||||||
|
m_floats_list = std::vector<double>{};
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GekkoIRPlugin::OnDirectivePost(GekkoDirective directive)
|
||||||
|
{
|
||||||
|
switch (directive)
|
||||||
|
{
|
||||||
|
// .nbyte directives are handled by OnResolvedExprPost
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
|
||||||
|
case GekkoDirective::Float:
|
||||||
|
case GekkoDirective::Double:
|
||||||
|
std::visit(
|
||||||
|
[this](auto&& vec) {
|
||||||
|
for (auto&& val : vec)
|
||||||
|
{
|
||||||
|
AddBytes(val);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
m_floats_list);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case GekkoDirective::DefVar:
|
||||||
|
ASSERT(m_active_var != nullptr);
|
||||||
|
*m_active_var = m_eval_stack.back();
|
||||||
|
m_active_var = nullptr;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case GekkoDirective::Locate:
|
||||||
|
StartBlock(static_cast<u32>(m_eval_stack.back()));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case GekkoDirective::Zeros:
|
||||||
|
PadSpace(static_cast<u32>(m_eval_stack.back()));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case GekkoDirective::Skip:
|
||||||
|
{
|
||||||
|
const u32 skip_len = static_cast<u32>(m_eval_stack.back());
|
||||||
|
if (skip_len > 0)
|
||||||
|
{
|
||||||
|
StartBlock(CurrentAddress() + skip_len);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case GekkoDirective::PadAlign:
|
||||||
|
PadAlign(static_cast<u32>(m_eval_stack.back()));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case GekkoDirective::Align:
|
||||||
|
StartBlockAlign(static_cast<u32>(m_eval_stack.back()));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case GekkoDirective::Ascii:
|
||||||
|
AddStringBytes(m_string_lit, false);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case GekkoDirective::Asciz:
|
||||||
|
AddStringBytes(m_string_lit, true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
m_eval_stack = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
void GekkoIRPlugin::OnInstructionPre(const ParseInfo& mnemonic_info, bool extended)
|
||||||
|
{
|
||||||
|
m_evaluation_mode = EvalMode::RelAddrDoublePass;
|
||||||
|
StartInstruction(mnemonic_info.mnemonic_index, extended);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GekkoIRPlugin::OnInstructionPost(const ParseInfo&, bool)
|
||||||
|
{
|
||||||
|
FinishInstruction();
|
||||||
|
}
|
||||||
|
|
||||||
|
void GekkoIRPlugin::OnOperandPre()
|
||||||
|
{
|
||||||
|
m_operand_str_start = m_owner->lexer.ColNumber();
|
||||||
|
}
|
||||||
|
|
||||||
|
void GekkoIRPlugin::OnOperandPost()
|
||||||
|
{
|
||||||
|
SaveOperandFixup(m_operand_str_start, m_owner->lexer.ColNumber());
|
||||||
|
}
|
||||||
|
|
||||||
|
void GekkoIRPlugin::OnResolvedExprPost()
|
||||||
|
{
|
||||||
|
switch (m_active_directive)
|
||||||
|
{
|
||||||
|
case GekkoDirective::Byte:
|
||||||
|
AddBytes<u8>(static_cast<u8>(m_eval_stack.back()));
|
||||||
|
break;
|
||||||
|
case GekkoDirective::_2byte:
|
||||||
|
AddBytes<u16>(static_cast<u16>(m_eval_stack.back()));
|
||||||
|
break;
|
||||||
|
case GekkoDirective::_4byte:
|
||||||
|
AddBytes<u32>(static_cast<u32>(m_eval_stack.back()));
|
||||||
|
break;
|
||||||
|
case GekkoDirective::_8byte:
|
||||||
|
AddBytes<u64>(static_cast<u64>(m_eval_stack.back()));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_eval_stack.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void GekkoIRPlugin::OnOperator(AsmOp operation)
|
||||||
|
{
|
||||||
|
if (m_evaluation_mode == EvalMode::RelAddrDoublePass)
|
||||||
|
{
|
||||||
|
EvalOperatorRel(operation);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
EvalOperatorAbs(operation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GekkoIRPlugin::OnTerminal(Terminal type, const AssemblerToken& val)
|
||||||
|
{
|
||||||
|
if (type == Terminal::Str)
|
||||||
|
{
|
||||||
|
m_string_lit = val.token_val;
|
||||||
|
}
|
||||||
|
else if (m_evaluation_mode == EvalMode::RelAddrDoublePass)
|
||||||
|
{
|
||||||
|
EvalTerminalRel(type, val);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
EvalTerminalAbs(type, val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GekkoIRPlugin::OnHiaddr(std::string_view id)
|
||||||
|
{
|
||||||
|
if (m_evaluation_mode == EvalMode::RelAddrDoublePass)
|
||||||
|
{
|
||||||
|
AddSymbolResolve(id, true);
|
||||||
|
AddLiteral(16);
|
||||||
|
AddBinaryEvaluator([](u32 lhs, u32 rhs) { return lhs >> rhs; });
|
||||||
|
AddLiteral(0xffff);
|
||||||
|
AddBinaryEvaluator([](u32 lhs, u32 rhs) { return lhs & rhs; });
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
u32 base;
|
||||||
|
if (auto lbl = LookupLabel(id); lbl)
|
||||||
|
{
|
||||||
|
base = *lbl;
|
||||||
|
}
|
||||||
|
else if (auto var = LookupVar(id); var)
|
||||||
|
{
|
||||||
|
base = *var;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
m_owner->EmitErrorHere(fmt::format("Undefined reference to Label/Constant '{}'", id));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_eval_stack.push_back((base >> 16) & 0xffff);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GekkoIRPlugin::OnLoaddr(std::string_view id)
|
||||||
|
{
|
||||||
|
if (m_evaluation_mode == EvalMode::RelAddrDoublePass)
|
||||||
|
{
|
||||||
|
AddSymbolResolve(id, true);
|
||||||
|
AddLiteral(0xffff);
|
||||||
|
AddBinaryEvaluator([](u32 lhs, u32 rhs) { return lhs & rhs; });
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
u32 base;
|
||||||
|
if (auto lbl = LookupLabel(id); lbl)
|
||||||
|
{
|
||||||
|
base = *lbl;
|
||||||
|
}
|
||||||
|
else if (auto var = LookupVar(id); var)
|
||||||
|
{
|
||||||
|
base = *var;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
m_owner->EmitErrorHere(fmt::format("Undefined reference to Label/Constant '{}'", id));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_eval_stack.push_back(base & 0xffff);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GekkoIRPlugin::OnCloseParen(ParenType type)
|
||||||
|
{
|
||||||
|
if (type != ParenType::RelConv)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_evaluation_mode == EvalMode::RelAddrDoublePass)
|
||||||
|
{
|
||||||
|
AddAbsoluteAddressConv();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
m_eval_stack.push_back(CurrentAddress());
|
||||||
|
EvalOperatorAbs(AsmOp::Sub);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GekkoIRPlugin::OnLabelDecl(std::string_view name)
|
||||||
|
{
|
||||||
|
const std::string name_str(name);
|
||||||
|
if (m_symset.contains(name_str))
|
||||||
|
{
|
||||||
|
m_owner->EmitErrorHere(fmt::format("Label/Constant {} is already defined", name));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_labels[name_str] = m_active_block->BlockEndAddress();
|
||||||
|
m_symset.insert(name_str);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GekkoIRPlugin::OnVarDecl(std::string_view name)
|
||||||
|
{
|
||||||
|
const std::string name_str(name);
|
||||||
|
if (m_symset.contains(name_str))
|
||||||
|
{
|
||||||
|
m_owner->EmitErrorHere(fmt::format("Label/Constant {} is already defined", name));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_active_var = &m_constants[name_str];
|
||||||
|
m_symset.insert(name_str);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GekkoIRPlugin::PostParseAction()
|
||||||
|
{
|
||||||
|
RunFixups();
|
||||||
|
}
|
||||||
|
|
||||||
|
//////////////////////
|
||||||
|
// HELPER FUNCTIONS //
|
||||||
|
//////////////////////
|
||||||
|
|
||||||
|
u32 GekkoIRPlugin::CurrentAddress() const
|
||||||
|
{
|
||||||
|
return m_active_block->BlockEndAddress();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<u64> GekkoIRPlugin::LookupVar(std::string_view var)
|
||||||
|
{
|
||||||
|
auto var_it = m_constants.find(var);
|
||||||
|
return var_it == m_constants.end() ? std::nullopt : std::optional(var_it->second);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<u32> GekkoIRPlugin::LookupLabel(std::string_view lab)
|
||||||
|
{
|
||||||
|
auto label_it = m_labels.find(lab);
|
||||||
|
return label_it == m_labels.end() ? std::nullopt : std::optional(label_it->second);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GekkoIRPlugin::AddStringBytes(std::string_view str, bool null_term)
|
||||||
|
{
|
||||||
|
ByteChunk& bytes = GetChunk<ByteChunk>();
|
||||||
|
ConvertStringLiteral(str, &bytes);
|
||||||
|
if (null_term)
|
||||||
|
{
|
||||||
|
bytes.push_back('\0');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
T& GekkoIRPlugin::GetChunk()
|
||||||
|
{
|
||||||
|
if (!m_active_block->chunks.empty() && std::holds_alternative<T>(m_active_block->chunks.back()))
|
||||||
|
{
|
||||||
|
return std::get<T>(m_active_block->chunks.back());
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::get<T>(m_active_block->chunks.emplace_back(T{}));
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
void GekkoIRPlugin::AddBytes(T val)
|
||||||
|
{
|
||||||
|
if constexpr (std::is_integral_v<T>)
|
||||||
|
{
|
||||||
|
ByteChunk& bytes = GetChunk<ByteChunk>();
|
||||||
|
for (size_t i = sizeof(T) - 1; i > 0; i--)
|
||||||
|
{
|
||||||
|
bytes.push_back((val >> (8 * i)) & 0xff);
|
||||||
|
}
|
||||||
|
bytes.push_back(val & 0xff);
|
||||||
|
}
|
||||||
|
else if constexpr (std::is_same_v<T, float>)
|
||||||
|
{
|
||||||
|
static_assert(sizeof(double) == sizeof(u64));
|
||||||
|
AddBytes(BitCast<u32>(val));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// std::is_same_v<T, double>
|
||||||
|
static_assert(sizeof(double) == sizeof(u64));
|
||||||
|
AddBytes(BitCast<u64>(val));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GekkoIRPlugin::PadAlign(u32 bits)
|
||||||
|
{
|
||||||
|
const u32 align_mask = (1 << bits) - 1;
|
||||||
|
const u32 current_addr = m_active_block->BlockEndAddress();
|
||||||
|
if (current_addr & align_mask)
|
||||||
|
{
|
||||||
|
PadChunk& current_pad = GetChunk<PadChunk>();
|
||||||
|
current_pad += (1 << bits) - (current_addr & align_mask);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GekkoIRPlugin::PadSpace(size_t space)
|
||||||
|
{
|
||||||
|
GetChunk<PadChunk>() += space;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GekkoIRPlugin::StartBlock(u32 address)
|
||||||
|
{
|
||||||
|
m_active_block = &m_output_result.blocks.emplace_back(address);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GekkoIRPlugin::StartBlockAlign(u32 bits)
|
||||||
|
{
|
||||||
|
const u32 align_mask = (1 << bits) - 1;
|
||||||
|
const u32 current_addr = m_active_block->BlockEndAddress();
|
||||||
|
if (current_addr & align_mask)
|
||||||
|
{
|
||||||
|
StartBlock((1 << bits) + (current_addr & ~align_mask));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GekkoIRPlugin::StartInstruction(size_t mnemonic_index, bool extended)
|
||||||
|
{
|
||||||
|
m_build_inst = GekkoInstruction{
|
||||||
|
.mnemonic_index = mnemonic_index,
|
||||||
|
.raw_text = m_owner->lexer.CurrentLine(),
|
||||||
|
.line_number = m_owner->lexer.LineNumber(),
|
||||||
|
.is_extended = extended,
|
||||||
|
};
|
||||||
|
m_operand_scan_begin = m_output_result.operand_pool.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
void GekkoIRPlugin::AddBinaryEvaluator(u32 (*evaluator)(u32, u32))
|
||||||
|
{
|
||||||
|
std::function<u32()> rhs = std::move(m_fixup_stack.top());
|
||||||
|
m_fixup_stack.pop();
|
||||||
|
std::function<u32()> lhs = std::move(m_fixup_stack.top());
|
||||||
|
m_fixup_stack.pop();
|
||||||
|
m_fixup_stack.emplace([evaluator, lhs = std::move(lhs), rhs = std::move(rhs)]() {
|
||||||
|
return evaluator(lhs(), rhs());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void GekkoIRPlugin::AddUnaryEvaluator(u32 (*evaluator)(u32))
|
||||||
|
{
|
||||||
|
std::function<u32()> sub = std::move(m_fixup_stack.top());
|
||||||
|
m_fixup_stack.pop();
|
||||||
|
m_fixup_stack.emplace([evaluator, sub = std::move(sub)]() { return evaluator(sub()); });
|
||||||
|
}
|
||||||
|
|
||||||
|
void GekkoIRPlugin::AddAbsoluteAddressConv()
|
||||||
|
{
|
||||||
|
const u32 inst_address = m_active_block->BlockEndAddress();
|
||||||
|
std::function<u32()> sub = std::move(m_fixup_stack.top());
|
||||||
|
m_fixup_stack.pop();
|
||||||
|
m_fixup_stack.emplace([inst_address, sub = std::move(sub)] { return sub() - inst_address; });
|
||||||
|
}
|
||||||
|
|
||||||
|
void GekkoIRPlugin::AddLiteral(u32 lit)
|
||||||
|
{
|
||||||
|
m_fixup_stack.emplace([lit] { return lit; });
|
||||||
|
}
|
||||||
|
|
||||||
|
void GekkoIRPlugin::AddSymbolResolve(std::string_view sym, bool absolute)
|
||||||
|
{
|
||||||
|
const u32 source_address = m_active_block->BlockEndAddress();
|
||||||
|
AssemblerError err_on_fail = AssemblerError{
|
||||||
|
fmt::format("Unresolved symbol '{}'", sym),
|
||||||
|
m_owner->lexer.CurrentLine(),
|
||||||
|
m_owner->lexer.LineNumber(),
|
||||||
|
// Lexer should currently point to the label, as it hasn't been eaten yet
|
||||||
|
m_owner->lexer.ColNumber(),
|
||||||
|
sym.size(),
|
||||||
|
};
|
||||||
|
|
||||||
|
m_fixup_stack.emplace(
|
||||||
|
[this, sym, absolute, source_address, err_on_fail = std::move(err_on_fail)] {
|
||||||
|
auto label_it = m_labels.find(sym);
|
||||||
|
if (label_it != m_labels.end())
|
||||||
|
{
|
||||||
|
if (absolute)
|
||||||
|
{
|
||||||
|
return label_it->second;
|
||||||
|
}
|
||||||
|
return label_it->second - source_address;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto var_it = m_constants.find(sym);
|
||||||
|
if (var_it != m_constants.end())
|
||||||
|
{
|
||||||
|
return static_cast<u32>(var_it->second);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_owner->error = std::move(err_on_fail);
|
||||||
|
return u32{0};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void GekkoIRPlugin::SaveOperandFixup(size_t str_left, size_t str_right)
|
||||||
|
{
|
||||||
|
m_operand_fixups.emplace_back(std::move(m_fixup_stack.top()));
|
||||||
|
m_fixup_stack.pop();
|
||||||
|
m_output_result.operand_pool.emplace_back(Interval{str_left, str_right - str_left}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GekkoIRPlugin::RunFixups()
|
||||||
|
{
|
||||||
|
for (size_t i = 0; i < m_operand_fixups.size(); i++)
|
||||||
|
{
|
||||||
|
ValueOf(m_output_result.operand_pool[i]) = m_operand_fixups[i]();
|
||||||
|
if (m_owner->error)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GekkoIRPlugin::FinishInstruction()
|
||||||
|
{
|
||||||
|
m_build_inst.op_interval.begin = m_operand_scan_begin;
|
||||||
|
m_build_inst.op_interval.len = m_output_result.operand_pool.size() - m_operand_scan_begin;
|
||||||
|
GetChunk<InstChunk>().emplace_back(m_build_inst);
|
||||||
|
m_operand_scan_begin = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GekkoIRPlugin::EvalOperatorAbs(AsmOp operation)
|
||||||
|
{
|
||||||
|
#define EVAL_BINARY_OP(OPERATOR) \
|
||||||
|
{ \
|
||||||
|
u64 rhs = m_eval_stack.back(); \
|
||||||
|
m_eval_stack.pop_back(); \
|
||||||
|
m_eval_stack.back() = m_eval_stack.back() OPERATOR rhs; \
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (operation)
|
||||||
|
{
|
||||||
|
case AsmOp::Or:
|
||||||
|
EVAL_BINARY_OP(|);
|
||||||
|
break;
|
||||||
|
case AsmOp::Xor:
|
||||||
|
EVAL_BINARY_OP(^);
|
||||||
|
break;
|
||||||
|
case AsmOp::And:
|
||||||
|
EVAL_BINARY_OP(&);
|
||||||
|
break;
|
||||||
|
case AsmOp::Lsh:
|
||||||
|
EVAL_BINARY_OP(<<);
|
||||||
|
break;
|
||||||
|
case AsmOp::Rsh:
|
||||||
|
EVAL_BINARY_OP(>>);
|
||||||
|
break;
|
||||||
|
case AsmOp::Add:
|
||||||
|
EVAL_BINARY_OP(+);
|
||||||
|
break;
|
||||||
|
case AsmOp::Sub:
|
||||||
|
EVAL_BINARY_OP(-);
|
||||||
|
break;
|
||||||
|
case AsmOp::Mul:
|
||||||
|
EVAL_BINARY_OP(*);
|
||||||
|
break;
|
||||||
|
case AsmOp::Div:
|
||||||
|
EVAL_BINARY_OP(/);
|
||||||
|
break;
|
||||||
|
case AsmOp::Neg:
|
||||||
|
m_eval_stack.back() = static_cast<u32>(-static_cast<s32>(m_eval_stack.back()));
|
||||||
|
break;
|
||||||
|
case AsmOp::Not:
|
||||||
|
m_eval_stack.back() = ~m_eval_stack.back();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
#undef EVAL_BINARY_OP
|
||||||
|
#undef EVAL_UNARY_OP
|
||||||
|
}
|
||||||
|
|
||||||
|
void GekkoIRPlugin::EvalOperatorRel(AsmOp operation)
|
||||||
|
{
|
||||||
|
switch (operation)
|
||||||
|
{
|
||||||
|
case AsmOp::Or:
|
||||||
|
AddBinaryEvaluator([](u32 lhs, u32 rhs) { return lhs | rhs; });
|
||||||
|
break;
|
||||||
|
case AsmOp::Xor:
|
||||||
|
AddBinaryEvaluator([](u32 lhs, u32 rhs) { return lhs ^ rhs; });
|
||||||
|
break;
|
||||||
|
case AsmOp::And:
|
||||||
|
AddBinaryEvaluator([](u32 lhs, u32 rhs) { return lhs & rhs; });
|
||||||
|
break;
|
||||||
|
case AsmOp::Lsh:
|
||||||
|
AddBinaryEvaluator([](u32 lhs, u32 rhs) { return lhs << rhs; });
|
||||||
|
break;
|
||||||
|
case AsmOp::Rsh:
|
||||||
|
AddBinaryEvaluator([](u32 lhs, u32 rhs) { return lhs >> rhs; });
|
||||||
|
break;
|
||||||
|
case AsmOp::Add:
|
||||||
|
AddBinaryEvaluator([](u32 lhs, u32 rhs) { return lhs + rhs; });
|
||||||
|
break;
|
||||||
|
case AsmOp::Sub:
|
||||||
|
AddBinaryEvaluator([](u32 lhs, u32 rhs) { return lhs - rhs; });
|
||||||
|
break;
|
||||||
|
case AsmOp::Mul:
|
||||||
|
AddBinaryEvaluator([](u32 lhs, u32 rhs) { return lhs * rhs; });
|
||||||
|
break;
|
||||||
|
case AsmOp::Div:
|
||||||
|
AddBinaryEvaluator([](u32 lhs, u32 rhs) { return lhs / rhs; });
|
||||||
|
break;
|
||||||
|
case AsmOp::Neg:
|
||||||
|
AddUnaryEvaluator([](u32 val) { return static_cast<u32>(-static_cast<s32>(val)); });
|
||||||
|
break;
|
||||||
|
case AsmOp::Not:
|
||||||
|
AddUnaryEvaluator([](u32 val) { return ~val; });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GekkoIRPlugin::EvalTerminalRel(Terminal type, const AssemblerToken& tok)
|
||||||
|
{
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case Terminal::Hex:
|
||||||
|
case Terminal::Dec:
|
||||||
|
case Terminal::Oct:
|
||||||
|
case Terminal::Bin:
|
||||||
|
case Terminal::GPR:
|
||||||
|
case Terminal::FPR:
|
||||||
|
case Terminal::SPR:
|
||||||
|
case Terminal::CRField:
|
||||||
|
case Terminal::Lt:
|
||||||
|
case Terminal::Gt:
|
||||||
|
case Terminal::Eq:
|
||||||
|
case Terminal::So:
|
||||||
|
{
|
||||||
|
std::optional<u32> val = tok.EvalToken<u32>();
|
||||||
|
ASSERT(val.has_value());
|
||||||
|
AddLiteral(*val);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case Terminal::Dot:
|
||||||
|
AddLiteral(CurrentAddress());
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Terminal::Id:
|
||||||
|
{
|
||||||
|
if (auto label_it = m_labels.find(tok.token_val); label_it != m_labels.end())
|
||||||
|
{
|
||||||
|
AddLiteral(label_it->second - CurrentAddress());
|
||||||
|
}
|
||||||
|
else if (auto var_it = m_constants.find(tok.token_val); var_it != m_constants.end())
|
||||||
|
{
|
||||||
|
AddLiteral(var_it->second);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
AddSymbolResolve(tok.token_val, false);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parser should disallow this from happening
|
||||||
|
default:
|
||||||
|
ASSERT(false);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GekkoIRPlugin::EvalTerminalAbs(Terminal type, const AssemblerToken& tok)
|
||||||
|
{
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case Terminal::Hex:
|
||||||
|
case Terminal::Dec:
|
||||||
|
case Terminal::Oct:
|
||||||
|
case Terminal::Bin:
|
||||||
|
case Terminal::GPR:
|
||||||
|
case Terminal::FPR:
|
||||||
|
case Terminal::SPR:
|
||||||
|
case Terminal::CRField:
|
||||||
|
case Terminal::Lt:
|
||||||
|
case Terminal::Gt:
|
||||||
|
case Terminal::Eq:
|
||||||
|
case Terminal::So:
|
||||||
|
{
|
||||||
|
std::optional<u64> val = tok.EvalToken<u64>();
|
||||||
|
ASSERT(val.has_value());
|
||||||
|
m_eval_stack.push_back(*val);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case Terminal::Flt:
|
||||||
|
{
|
||||||
|
std::visit(
|
||||||
|
[&tok](auto&& vec) {
|
||||||
|
auto opt = tok.EvalToken<typename std::decay_t<decltype(vec)>::value_type>();
|
||||||
|
ASSERT(opt.has_value());
|
||||||
|
vec.push_back(*opt);
|
||||||
|
},
|
||||||
|
m_floats_list);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case Terminal::Dot:
|
||||||
|
m_eval_stack.push_back(static_cast<u64>(CurrentAddress()));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Terminal::Id:
|
||||||
|
{
|
||||||
|
if (auto label_it = m_labels.find(tok.token_val); label_it != m_labels.end())
|
||||||
|
{
|
||||||
|
m_eval_stack.push_back(label_it->second);
|
||||||
|
}
|
||||||
|
else if (auto var_it = m_constants.find(tok.token_val); var_it != m_constants.end())
|
||||||
|
{
|
||||||
|
m_eval_stack.push_back(var_it->second);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
m_owner->EmitErrorHere(
|
||||||
|
fmt::format("Undefined reference to Label/Constant '{}'", tok.ValStr()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parser should disallow this from happening
|
||||||
|
default:
|
||||||
|
ASSERT(false);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
u32 IRBlock::BlockEndAddress() const
|
||||||
|
{
|
||||||
|
return std::accumulate(chunks.begin(), chunks.end(), block_address,
|
||||||
|
[](u32 acc, const ChunkVariant& chunk) {
|
||||||
|
size_t size;
|
||||||
|
if (std::holds_alternative<InstChunk>(chunk))
|
||||||
|
{
|
||||||
|
size = std::get<InstChunk>(chunk).size() * 4;
|
||||||
|
}
|
||||||
|
else if (std::holds_alternative<ByteChunk>(chunk))
|
||||||
|
{
|
||||||
|
size = std::get<ByteChunk>(chunk).size();
|
||||||
|
}
|
||||||
|
else if (std::holds_alternative<PadChunk>(chunk))
|
||||||
|
{
|
||||||
|
size = std::get<PadChunk>(chunk);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ASSERT(false);
|
||||||
|
size = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc + static_cast<u32>(size);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
FailureOr<GekkoIR> ParseToIR(std::string_view assembly, u32 base_virtual_address)
|
||||||
|
{
|
||||||
|
GekkoIR ret;
|
||||||
|
GekkoIRPlugin plugin(ret, base_virtual_address);
|
||||||
|
|
||||||
|
ParseWithPlugin(&plugin, assembly);
|
||||||
|
|
||||||
|
if (plugin.Error())
|
||||||
|
{
|
||||||
|
return FailureOr<GekkoIR>(std::move(*plugin.Error()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::move(ret);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Common::GekkoAssembler::detail
|
50
Source/Core/Common/Assembler/GekkoIRGen.h
Normal file
50
Source/Core/Common/Assembler/GekkoIRGen.h
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
// Copyright 2023 Dolphin Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string_view>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "Common/Assembler/AssemblerShared.h"
|
||||||
|
#include "Common/Assembler/GekkoLexer.h"
|
||||||
|
#include "Common/CommonTypes.h"
|
||||||
|
|
||||||
|
namespace Common::GekkoAssembler::detail
|
||||||
|
{
|
||||||
|
struct GekkoInstruction
|
||||||
|
{
|
||||||
|
// Combination of a mnemonic index and variant:
|
||||||
|
// (<GekkoMnemonic> << 2) | (<variant bits>)
|
||||||
|
size_t mnemonic_index = 0;
|
||||||
|
// Below refers to GekkoParseResult::operand_pool
|
||||||
|
Interval op_interval = Interval{0, 0};
|
||||||
|
// Literal text of this instruction
|
||||||
|
std::string_view raw_text;
|
||||||
|
size_t line_number = 0;
|
||||||
|
bool is_extended = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
using InstChunk = std::vector<GekkoInstruction>;
|
||||||
|
using ByteChunk = std::vector<u8>;
|
||||||
|
using PadChunk = size_t;
|
||||||
|
using ChunkVariant = std::variant<InstChunk, ByteChunk, PadChunk>;
|
||||||
|
|
||||||
|
struct IRBlock
|
||||||
|
{
|
||||||
|
explicit IRBlock(u32 address) : block_address(address) {}
|
||||||
|
|
||||||
|
u32 BlockEndAddress() const;
|
||||||
|
|
||||||
|
std::vector<ChunkVariant> chunks;
|
||||||
|
u32 block_address;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct GekkoIR
|
||||||
|
{
|
||||||
|
std::vector<IRBlock> blocks;
|
||||||
|
std::vector<Tagged<Interval, u32>> operand_pool;
|
||||||
|
};
|
||||||
|
|
||||||
|
FailureOr<GekkoIR> ParseToIR(std::string_view assembly, u32 base_virtual_address);
|
||||||
|
} // namespace Common::GekkoAssembler::detail
|
794
Source/Core/Common/Assembler/GekkoLexer.cpp
Normal file
794
Source/Core/Common/Assembler/GekkoLexer.cpp
Normal file
@ -0,0 +1,794 @@
|
|||||||
|
// Copyright 2023 Dolphin Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include "Common/Assembler/GekkoLexer.h"
|
||||||
|
|
||||||
|
#include "Common/Assert.h"
|
||||||
|
|
||||||
|
#include <iterator>
|
||||||
|
#include <numeric>
|
||||||
|
|
||||||
|
namespace Common::GekkoAssembler::detail
|
||||||
|
{
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
constexpr bool IsOctal(char c)
|
||||||
|
{
|
||||||
|
return c >= '0' && c <= '7';
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr bool IsBinary(char c)
|
||||||
|
{
|
||||||
|
return c == '0' || c == '1';
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
constexpr T ConvertNib(char c)
|
||||||
|
{
|
||||||
|
if (c >= 'a' && c <= 'f')
|
||||||
|
{
|
||||||
|
return static_cast<T>(c - 'a' + 10);
|
||||||
|
}
|
||||||
|
if (c >= 'A' && c <= 'F')
|
||||||
|
{
|
||||||
|
return static_cast<T>(c - 'A' + 10);
|
||||||
|
}
|
||||||
|
return static_cast<T>(c - '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr TokenType SingleCharToken(char ch)
|
||||||
|
{
|
||||||
|
switch (ch)
|
||||||
|
{
|
||||||
|
case ',':
|
||||||
|
return TokenType::Comma;
|
||||||
|
case '(':
|
||||||
|
return TokenType::Lparen;
|
||||||
|
case ')':
|
||||||
|
return TokenType::Rparen;
|
||||||
|
case '|':
|
||||||
|
return TokenType::Pipe;
|
||||||
|
case '^':
|
||||||
|
return TokenType::Caret;
|
||||||
|
case '&':
|
||||||
|
return TokenType::Ampersand;
|
||||||
|
case '+':
|
||||||
|
return TokenType::Plus;
|
||||||
|
case '-':
|
||||||
|
return TokenType::Minus;
|
||||||
|
case '*':
|
||||||
|
return TokenType::Star;
|
||||||
|
case '/':
|
||||||
|
return TokenType::Slash;
|
||||||
|
case '~':
|
||||||
|
return TokenType::Tilde;
|
||||||
|
case '@':
|
||||||
|
return TokenType::At;
|
||||||
|
case ':':
|
||||||
|
return TokenType::Colon;
|
||||||
|
case '`':
|
||||||
|
return TokenType::Grave;
|
||||||
|
case '.':
|
||||||
|
return TokenType::Dot;
|
||||||
|
case '\0':
|
||||||
|
return TokenType::Eof;
|
||||||
|
case '\n':
|
||||||
|
return TokenType::Eol;
|
||||||
|
default:
|
||||||
|
return TokenType::Invalid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert a string literal into its raw-data form
|
||||||
|
template <typename Cont>
|
||||||
|
void ConvertStringLiteral(std::string_view literal, std::back_insert_iterator<Cont> out_it)
|
||||||
|
{
|
||||||
|
for (size_t i = 1; i < literal.size() - 1;)
|
||||||
|
{
|
||||||
|
if (literal[i] == '\\')
|
||||||
|
{
|
||||||
|
++i;
|
||||||
|
if (IsOctal(literal[i]))
|
||||||
|
{
|
||||||
|
// Octal escape
|
||||||
|
char octal_escape = 0;
|
||||||
|
for (char c = literal[i]; IsOctal(c); c = literal[++i])
|
||||||
|
{
|
||||||
|
octal_escape = (octal_escape << 3) + (c - '0');
|
||||||
|
}
|
||||||
|
out_it = static_cast<u8>(octal_escape);
|
||||||
|
}
|
||||||
|
else if (literal[i] == 'x')
|
||||||
|
{
|
||||||
|
// Hex escape
|
||||||
|
char hex_escape = 0;
|
||||||
|
for (char c = literal[++i]; std::isxdigit(c); c = literal[++i])
|
||||||
|
{
|
||||||
|
hex_escape = (hex_escape << 4) + ConvertNib<char>(c);
|
||||||
|
}
|
||||||
|
out_it = static_cast<u8>(hex_escape);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
char simple_escape;
|
||||||
|
switch (literal[i])
|
||||||
|
{
|
||||||
|
case '\'':
|
||||||
|
simple_escape = '\x27';
|
||||||
|
break;
|
||||||
|
case '"':
|
||||||
|
simple_escape = '\x22';
|
||||||
|
break;
|
||||||
|
case '?':
|
||||||
|
simple_escape = '\x3f';
|
||||||
|
break;
|
||||||
|
case '\\':
|
||||||
|
simple_escape = '\x5c';
|
||||||
|
break;
|
||||||
|
case 'a':
|
||||||
|
simple_escape = '\x07';
|
||||||
|
break;
|
||||||
|
case 'b':
|
||||||
|
simple_escape = '\x08';
|
||||||
|
break;
|
||||||
|
case 'f':
|
||||||
|
simple_escape = '\x0c';
|
||||||
|
break;
|
||||||
|
case 'n':
|
||||||
|
simple_escape = '\x0a';
|
||||||
|
break;
|
||||||
|
case 'r':
|
||||||
|
simple_escape = '\x0d';
|
||||||
|
break;
|
||||||
|
case 't':
|
||||||
|
simple_escape = '\x09';
|
||||||
|
break;
|
||||||
|
case 'v':
|
||||||
|
simple_escape = '\x0b';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
simple_escape = literal[i];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
out_it = static_cast<u8>(simple_escape);
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
out_it = static_cast<u8>(literal[i]);
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
std::optional<T> EvalIntegral(TokenType tp, std::string_view val)
|
||||||
|
{
|
||||||
|
constexpr auto hex_step = [](T acc, char c) { return acc << 4 | ConvertNib<T>(c); };
|
||||||
|
constexpr auto dec_step = [](T acc, char c) { return acc * 10 + (c - '0'); };
|
||||||
|
constexpr auto oct_step = [](T acc, char c) { return acc << 3 | (c - '0'); };
|
||||||
|
constexpr auto bin_step = [](T acc, char c) { return acc << 1 | (c - '0'); };
|
||||||
|
|
||||||
|
switch (tp)
|
||||||
|
{
|
||||||
|
case TokenType::HexadecimalLit:
|
||||||
|
return std::accumulate(val.begin() + 2, val.end(), T{0}, hex_step);
|
||||||
|
case TokenType::DecimalLit:
|
||||||
|
return std::accumulate(val.begin(), val.end(), T{0}, dec_step);
|
||||||
|
case TokenType::OctalLit:
|
||||||
|
return std::accumulate(val.begin() + 1, val.end(), T{0}, oct_step);
|
||||||
|
case TokenType::BinaryLit:
|
||||||
|
return std::accumulate(val.begin() + 2, val.end(), T{0}, bin_step);
|
||||||
|
case TokenType::GPR:
|
||||||
|
case TokenType::FPR:
|
||||||
|
return std::accumulate(val.begin() + 1, val.end(), T{0}, dec_step);
|
||||||
|
case TokenType::CRField:
|
||||||
|
return std::accumulate(val.begin() + 2, val.end(), T{0}, dec_step);
|
||||||
|
case TokenType::SPR:
|
||||||
|
return static_cast<T>(*sprg_map.Find(val));
|
||||||
|
case TokenType::Lt:
|
||||||
|
return T{0};
|
||||||
|
case TokenType::Gt:
|
||||||
|
return T{1};
|
||||||
|
case TokenType::Eq:
|
||||||
|
return T{2};
|
||||||
|
case TokenType::So:
|
||||||
|
return T{3};
|
||||||
|
default:
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void ConvertStringLiteral(std::string_view literal, std::vector<u8>* out_vec)
|
||||||
|
{
|
||||||
|
ConvertStringLiteral(literal, std::back_inserter(*out_vec));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string_view TokenTypeToStr(TokenType tp)
|
||||||
|
{
|
||||||
|
switch (tp)
|
||||||
|
{
|
||||||
|
case TokenType::GPR:
|
||||||
|
return "GPR";
|
||||||
|
case TokenType::FPR:
|
||||||
|
return "FPR";
|
||||||
|
case TokenType::SPR:
|
||||||
|
return "SPR";
|
||||||
|
case TokenType::CRField:
|
||||||
|
return "CR Field";
|
||||||
|
case TokenType::Lt:
|
||||||
|
case TokenType::Gt:
|
||||||
|
case TokenType::Eq:
|
||||||
|
case TokenType::So:
|
||||||
|
return "CR Bit";
|
||||||
|
case TokenType::Identifier:
|
||||||
|
return "Identifier";
|
||||||
|
case TokenType::StringLit:
|
||||||
|
return "String Literal";
|
||||||
|
case TokenType::DecimalLit:
|
||||||
|
return "Decimal Literal";
|
||||||
|
case TokenType::BinaryLit:
|
||||||
|
return "Binary Literal";
|
||||||
|
case TokenType::HexadecimalLit:
|
||||||
|
return "Hexadecimal Literal";
|
||||||
|
case TokenType::OctalLit:
|
||||||
|
return "Octal Literal";
|
||||||
|
case TokenType::FloatLit:
|
||||||
|
return "Float Literal";
|
||||||
|
case TokenType::Invalid:
|
||||||
|
return "Invalid";
|
||||||
|
case TokenType::Lsh:
|
||||||
|
return "<<";
|
||||||
|
case TokenType::Rsh:
|
||||||
|
return ">>";
|
||||||
|
case TokenType::Comma:
|
||||||
|
return ",";
|
||||||
|
case TokenType::Lparen:
|
||||||
|
return "(";
|
||||||
|
case TokenType::Rparen:
|
||||||
|
return ")";
|
||||||
|
case TokenType::Pipe:
|
||||||
|
return "|";
|
||||||
|
case TokenType::Caret:
|
||||||
|
return "^";
|
||||||
|
case TokenType::Ampersand:
|
||||||
|
return "&";
|
||||||
|
case TokenType::Plus:
|
||||||
|
return "+";
|
||||||
|
case TokenType::Minus:
|
||||||
|
return "-";
|
||||||
|
case TokenType::Star:
|
||||||
|
return "*";
|
||||||
|
case TokenType::Slash:
|
||||||
|
return "/";
|
||||||
|
case TokenType::Tilde:
|
||||||
|
return "~";
|
||||||
|
case TokenType::At:
|
||||||
|
return "@";
|
||||||
|
case TokenType::Colon:
|
||||||
|
return ":";
|
||||||
|
case TokenType::Grave:
|
||||||
|
return "`";
|
||||||
|
case TokenType::Dot:
|
||||||
|
return ".";
|
||||||
|
case TokenType::Eof:
|
||||||
|
return "End of File";
|
||||||
|
case TokenType::Eol:
|
||||||
|
return "End of Line";
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string_view AssemblerToken::TypeStr() const
|
||||||
|
{
|
||||||
|
return TokenTypeToStr(token_type);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string_view AssemblerToken::ValStr() const
|
||||||
|
{
|
||||||
|
switch (token_type)
|
||||||
|
{
|
||||||
|
case TokenType::Eol:
|
||||||
|
return "<EOL>";
|
||||||
|
case TokenType::Eof:
|
||||||
|
return "<EOF>";
|
||||||
|
default:
|
||||||
|
return token_val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template <>
|
||||||
|
std::optional<float> AssemblerToken::EvalToken() const
|
||||||
|
{
|
||||||
|
if (token_type == TokenType::FloatLit)
|
||||||
|
{
|
||||||
|
return std::stof(std::string(token_val));
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <>
|
||||||
|
std::optional<double> AssemblerToken::EvalToken() const
|
||||||
|
{
|
||||||
|
if (token_type == TokenType::FloatLit)
|
||||||
|
{
|
||||||
|
return std::stod(std::string(token_val));
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <>
|
||||||
|
std::optional<u8> AssemblerToken::EvalToken() const
|
||||||
|
{
|
||||||
|
return EvalIntegral<u8>(token_type, token_val);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <>
|
||||||
|
std::optional<u16> AssemblerToken::EvalToken() const
|
||||||
|
{
|
||||||
|
return EvalIntegral<u16>(token_type, token_val);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <>
|
||||||
|
std::optional<u32> AssemblerToken::EvalToken() const
|
||||||
|
{
|
||||||
|
return EvalIntegral<u32>(token_type, token_val);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <>
|
||||||
|
std::optional<u64> AssemblerToken::EvalToken() const
|
||||||
|
{
|
||||||
|
return EvalIntegral<u64>(token_type, token_val);
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t Lexer::LineNumber() const
|
||||||
|
{
|
||||||
|
return m_lexed_tokens.empty() ? m_pos.line : TagOf(m_lexed_tokens.front()).line;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t Lexer::ColNumber() const
|
||||||
|
{
|
||||||
|
return m_lexed_tokens.empty() ? m_pos.col : TagOf(m_lexed_tokens.front()).col;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string_view Lexer::CurrentLine() const
|
||||||
|
{
|
||||||
|
const size_t line_index =
|
||||||
|
m_lexed_tokens.empty() ? m_pos.index : TagOf(m_lexed_tokens.front()).index;
|
||||||
|
size_t begin_index = line_index == 0 ? 0 : line_index - 1;
|
||||||
|
for (; begin_index > 0; begin_index--)
|
||||||
|
{
|
||||||
|
if (m_lex_string[begin_index] == '\n')
|
||||||
|
{
|
||||||
|
begin_index++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
size_t end_index = begin_index;
|
||||||
|
for (; end_index < m_lex_string.size(); end_index++)
|
||||||
|
{
|
||||||
|
if (m_lex_string[end_index] == '\n')
|
||||||
|
{
|
||||||
|
end_index++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m_lex_string.substr(begin_index, end_index - begin_index);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Lexer::SetIdentifierMatchRule(IdentifierMatchRule set)
|
||||||
|
{
|
||||||
|
FeedbackTokens();
|
||||||
|
m_match_rule = set;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Tagged<CursorPosition, AssemblerToken>& Lexer::LookaheadTagRef(size_t num_fwd) const
|
||||||
|
{
|
||||||
|
while (m_lexed_tokens.size() < num_fwd)
|
||||||
|
{
|
||||||
|
LookaheadRef();
|
||||||
|
}
|
||||||
|
return m_lexed_tokens[num_fwd];
|
||||||
|
}
|
||||||
|
|
||||||
|
AssemblerToken Lexer::Lookahead() const
|
||||||
|
{
|
||||||
|
if (m_lexed_tokens.empty())
|
||||||
|
{
|
||||||
|
CursorPosition pos_pre = m_pos;
|
||||||
|
m_lexed_tokens.emplace_back(pos_pre, LexSingle());
|
||||||
|
}
|
||||||
|
return ValueOf(m_lexed_tokens.front());
|
||||||
|
}
|
||||||
|
|
||||||
|
const AssemblerToken& Lexer::LookaheadRef() const
|
||||||
|
{
|
||||||
|
if (m_lexed_tokens.empty())
|
||||||
|
{
|
||||||
|
CursorPosition pos_pre = m_pos;
|
||||||
|
m_lexed_tokens.emplace_back(pos_pre, LexSingle());
|
||||||
|
}
|
||||||
|
return ValueOf(m_lexed_tokens.front());
|
||||||
|
}
|
||||||
|
|
||||||
|
TokenType Lexer::LookaheadType() const
|
||||||
|
{
|
||||||
|
return LookaheadRef().token_type;
|
||||||
|
}
|
||||||
|
|
||||||
|
AssemblerToken Lexer::LookaheadFloat() const
|
||||||
|
{
|
||||||
|
FeedbackTokens();
|
||||||
|
SkipWs();
|
||||||
|
|
||||||
|
CursorPosition pos_pre = m_pos;
|
||||||
|
ScanStart();
|
||||||
|
|
||||||
|
std::optional<std::string_view> failure_reason = RunDfa(float_dfa);
|
||||||
|
|
||||||
|
// Special case: lex at least a single char for no matches for errors to make sense
|
||||||
|
if (m_scan_pos.index == pos_pre.index)
|
||||||
|
{
|
||||||
|
Step();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string_view tok_str = ScanFinishOut();
|
||||||
|
AssemblerToken tok;
|
||||||
|
if (!failure_reason)
|
||||||
|
{
|
||||||
|
tok = AssemblerToken{
|
||||||
|
TokenType::FloatLit,
|
||||||
|
tok_str,
|
||||||
|
"",
|
||||||
|
Interval{0, 0},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
tok = AssemblerToken{
|
||||||
|
TokenType::Invalid,
|
||||||
|
tok_str,
|
||||||
|
*failure_reason,
|
||||||
|
Interval{0, tok_str.length()},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
m_lexed_tokens.emplace_back(pos_pre, tok);
|
||||||
|
return tok;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Lexer::Eat()
|
||||||
|
{
|
||||||
|
if (m_lexed_tokens.empty())
|
||||||
|
{
|
||||||
|
LexSingle();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
m_lexed_tokens.pop_front();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Lexer::EatAndReset()
|
||||||
|
{
|
||||||
|
Eat();
|
||||||
|
SetIdentifierMatchRule(IdentifierMatchRule::Typical);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<std::string_view> Lexer::RunDfa(const std::vector<DfaNode>& dfa) const
|
||||||
|
{
|
||||||
|
size_t dfa_index = 0;
|
||||||
|
bool transition_found;
|
||||||
|
do
|
||||||
|
{
|
||||||
|
transition_found = false;
|
||||||
|
if (Peek() == '\0')
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DfaNode& n = dfa[dfa_index];
|
||||||
|
for (auto&& edge : n.edges)
|
||||||
|
{
|
||||||
|
if (edge.first(Peek()))
|
||||||
|
{
|
||||||
|
transition_found = true;
|
||||||
|
dfa_index = edge.second;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (transition_found)
|
||||||
|
{
|
||||||
|
Step();
|
||||||
|
}
|
||||||
|
} while (transition_found);
|
||||||
|
|
||||||
|
return dfa[dfa_index].match_failure_reason;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Lexer::SkipWs() const
|
||||||
|
{
|
||||||
|
ScanStart();
|
||||||
|
for (char c = Peek(); std::isspace(c) && c != '\n'; c = Step().Peek())
|
||||||
|
{
|
||||||
|
}
|
||||||
|
if (Peek() == '#')
|
||||||
|
{
|
||||||
|
while (Peek() != '\n' && Peek() != '\0')
|
||||||
|
{
|
||||||
|
Step();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ScanFinish();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Lexer::FeedbackTokens() const
|
||||||
|
{
|
||||||
|
if (m_lexed_tokens.empty())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_pos = m_scan_pos = TagOf(m_lexed_tokens.front());
|
||||||
|
m_lexed_tokens.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Lexer::IdentifierHeadExtra(char h) const
|
||||||
|
{
|
||||||
|
switch (m_match_rule)
|
||||||
|
{
|
||||||
|
case IdentifierMatchRule::Typical:
|
||||||
|
case IdentifierMatchRule::Mnemonic:
|
||||||
|
return false;
|
||||||
|
case IdentifierMatchRule::Directive:
|
||||||
|
return std::isdigit(h);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Lexer::IdentifierExtra(char c) const
|
||||||
|
{
|
||||||
|
switch (m_match_rule)
|
||||||
|
{
|
||||||
|
case IdentifierMatchRule::Typical:
|
||||||
|
case IdentifierMatchRule::Directive:
|
||||||
|
return false;
|
||||||
|
case IdentifierMatchRule::Mnemonic:
|
||||||
|
return c == '+' || c == '-' || c == '.';
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Lexer::ScanStart() const
|
||||||
|
{
|
||||||
|
m_scan_pos = m_pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Lexer::ScanFinish() const
|
||||||
|
{
|
||||||
|
m_pos = m_scan_pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string_view Lexer::ScanFinishOut() const
|
||||||
|
{
|
||||||
|
const size_t start = m_pos.index;
|
||||||
|
m_pos = m_scan_pos;
|
||||||
|
return m_lex_string.substr(start, m_scan_pos.index - start);
|
||||||
|
}
|
||||||
|
|
||||||
|
char Lexer::Peek() const
|
||||||
|
{
|
||||||
|
if (m_scan_pos.index >= m_lex_string.length())
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return m_lex_string[m_scan_pos.index];
|
||||||
|
}
|
||||||
|
|
||||||
|
const Lexer& Lexer::Step() const
|
||||||
|
{
|
||||||
|
if (m_scan_pos.index >= m_lex_string.length())
|
||||||
|
{
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Peek() == '\n')
|
||||||
|
{
|
||||||
|
m_scan_pos.line++;
|
||||||
|
m_scan_pos.col = 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
m_scan_pos.col++;
|
||||||
|
}
|
||||||
|
m_scan_pos.index++;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
TokenType Lexer::LexStringLit(std::string_view& invalid_reason, Interval& invalid_region) const
|
||||||
|
{
|
||||||
|
// The open quote has alread been matched
|
||||||
|
const size_t string_start = m_scan_pos.index - 1;
|
||||||
|
TokenType token_type = TokenType::StringLit;
|
||||||
|
|
||||||
|
std::optional<std::string_view> failure_reason = RunDfa(string_dfa);
|
||||||
|
|
||||||
|
if (failure_reason)
|
||||||
|
{
|
||||||
|
token_type = TokenType::Invalid;
|
||||||
|
invalid_reason = *failure_reason;
|
||||||
|
invalid_region = Interval{0, m_scan_pos.index - string_start};
|
||||||
|
}
|
||||||
|
|
||||||
|
return token_type;
|
||||||
|
}
|
||||||
|
|
||||||
|
TokenType Lexer::ClassifyAlnum() const
|
||||||
|
{
|
||||||
|
const std::string_view alnum = m_lex_string.substr(m_pos.index, m_scan_pos.index - m_pos.index);
|
||||||
|
constexpr auto valid_regnum = [](std::string_view rn) {
|
||||||
|
if (rn.length() == 1 && std::isdigit(rn[0]))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else if (rn.length() == 2 && std::isdigit(rn[0]) && std::isdigit(rn[1]))
|
||||||
|
{
|
||||||
|
if (rn[0] == '1' || rn[0] == '2')
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rn[0] == '3')
|
||||||
|
{
|
||||||
|
return rn[1] <= '2';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
constexpr auto eq_nocase = [](std::string_view str, std::string_view lwr) {
|
||||||
|
auto it_l = str.cbegin(), it_r = lwr.cbegin();
|
||||||
|
for (; it_l != str.cend() && it_r != lwr.cend(); it_l++, it_r++)
|
||||||
|
{
|
||||||
|
if (std::tolower(*it_l) != *it_r)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return it_l == str.end() && it_r == lwr.end();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (std::tolower(alnum[0]) == 'r' && valid_regnum(alnum.substr(1)))
|
||||||
|
{
|
||||||
|
return TokenType::GPR;
|
||||||
|
}
|
||||||
|
else if (std::tolower(alnum[0]) == 'f' && valid_regnum(alnum.substr(1)))
|
||||||
|
{
|
||||||
|
return TokenType::FPR;
|
||||||
|
}
|
||||||
|
else if (alnum.length() == 3 && eq_nocase(alnum.substr(0, 2), "cr") && alnum[2] >= '0' &&
|
||||||
|
alnum[2] <= '7')
|
||||||
|
{
|
||||||
|
return TokenType::CRField;
|
||||||
|
}
|
||||||
|
else if (eq_nocase(alnum, "lt"))
|
||||||
|
{
|
||||||
|
return TokenType::Lt;
|
||||||
|
}
|
||||||
|
else if (eq_nocase(alnum, "gt"))
|
||||||
|
{
|
||||||
|
return TokenType::Gt;
|
||||||
|
}
|
||||||
|
else if (eq_nocase(alnum, "eq"))
|
||||||
|
{
|
||||||
|
return TokenType::Eq;
|
||||||
|
}
|
||||||
|
else if (eq_nocase(alnum, "so"))
|
||||||
|
{
|
||||||
|
return TokenType::So;
|
||||||
|
}
|
||||||
|
else if (sprg_map.Find(alnum) != nullptr)
|
||||||
|
{
|
||||||
|
return TokenType::SPR;
|
||||||
|
}
|
||||||
|
return TokenType::Identifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
AssemblerToken Lexer::LexSingle() const
|
||||||
|
{
|
||||||
|
SkipWs();
|
||||||
|
|
||||||
|
ScanStart();
|
||||||
|
const char h = Peek();
|
||||||
|
|
||||||
|
TokenType token_type;
|
||||||
|
std::string_view invalid_reason = "";
|
||||||
|
Interval invalid_region = Interval{0, 0};
|
||||||
|
|
||||||
|
Step();
|
||||||
|
|
||||||
|
if (std::isalpha(h) || h == '_' || IdentifierHeadExtra(h))
|
||||||
|
{
|
||||||
|
for (char c = Peek(); std::isalnum(c) || c == '_' || IdentifierExtra(c); c = Step().Peek())
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
token_type = ClassifyAlnum();
|
||||||
|
}
|
||||||
|
else if (h == '"')
|
||||||
|
{
|
||||||
|
token_type = LexStringLit(invalid_reason, invalid_region);
|
||||||
|
}
|
||||||
|
else if (h == '0')
|
||||||
|
{
|
||||||
|
const char imm_type = Peek();
|
||||||
|
|
||||||
|
if (imm_type == 'x')
|
||||||
|
{
|
||||||
|
token_type = TokenType::HexadecimalLit;
|
||||||
|
Step();
|
||||||
|
for (char c = Peek(); std::isxdigit(c); c = Step().Peek())
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (imm_type == 'b')
|
||||||
|
{
|
||||||
|
token_type = TokenType::BinaryLit;
|
||||||
|
Step();
|
||||||
|
for (char c = Peek(); IsBinary(c); c = Step().Peek())
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (IsOctal(imm_type))
|
||||||
|
{
|
||||||
|
token_type = TokenType::OctalLit;
|
||||||
|
for (char c = Peek(); IsOctal(c); c = Step().Peek())
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
token_type = TokenType::DecimalLit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (std::isdigit(h))
|
||||||
|
{
|
||||||
|
for (char c = Peek(); std::isdigit(c); c = Step().Peek())
|
||||||
|
{
|
||||||
|
}
|
||||||
|
token_type = TokenType::DecimalLit;
|
||||||
|
}
|
||||||
|
else if (h == '<' || h == '>')
|
||||||
|
{
|
||||||
|
// Special case for two-character operators
|
||||||
|
const char second_ch = Peek();
|
||||||
|
if (second_ch == h)
|
||||||
|
{
|
||||||
|
Step();
|
||||||
|
token_type = second_ch == '<' ? TokenType::Lsh : TokenType::Rsh;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
token_type = TokenType::Invalid;
|
||||||
|
invalid_reason = "Unrecognized character";
|
||||||
|
invalid_region = Interval{0, 1};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
token_type = SingleCharToken(h);
|
||||||
|
if (token_type == TokenType::Invalid)
|
||||||
|
{
|
||||||
|
invalid_reason = "Unrecognized character";
|
||||||
|
invalid_region = Interval{0, 1};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AssemblerToken new_tok = {token_type, ScanFinishOut(), invalid_reason, invalid_region};
|
||||||
|
SkipWs();
|
||||||
|
return new_tok;
|
||||||
|
}
|
||||||
|
} // namespace Common::GekkoAssembler::detail
|
188
Source/Core/Common/Assembler/GekkoLexer.h
Normal file
188
Source/Core/Common/Assembler/GekkoLexer.h
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
// Copyright 2023 Dolphin Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <deque>
|
||||||
|
#include <optional>
|
||||||
|
#include <string_view>
|
||||||
|
#include <type_traits>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "Common/Assembler/AssemblerShared.h"
|
||||||
|
#include "Common/Assembler/AssemblerTables.h"
|
||||||
|
#include "Common/CommonTypes.h"
|
||||||
|
|
||||||
|
namespace Common::GekkoAssembler::detail
|
||||||
|
{
|
||||||
|
void ConvertStringLiteral(std::string_view literal, std::vector<u8>* out_vec);
|
||||||
|
|
||||||
|
enum class TokenType
|
||||||
|
{
|
||||||
|
Invalid,
|
||||||
|
Identifier,
|
||||||
|
StringLit,
|
||||||
|
HexadecimalLit,
|
||||||
|
DecimalLit,
|
||||||
|
OctalLit,
|
||||||
|
BinaryLit,
|
||||||
|
FloatLit,
|
||||||
|
GPR,
|
||||||
|
FPR,
|
||||||
|
CRField,
|
||||||
|
SPR,
|
||||||
|
Lt,
|
||||||
|
Gt,
|
||||||
|
Eq,
|
||||||
|
So,
|
||||||
|
// EOL signifies boundaries between instructions, a la ';'
|
||||||
|
Eol,
|
||||||
|
Eof,
|
||||||
|
|
||||||
|
Dot,
|
||||||
|
Colon,
|
||||||
|
Comma,
|
||||||
|
Lparen,
|
||||||
|
Rparen,
|
||||||
|
Pipe,
|
||||||
|
Caret,
|
||||||
|
Ampersand,
|
||||||
|
Lsh,
|
||||||
|
Rsh,
|
||||||
|
Plus,
|
||||||
|
Minus,
|
||||||
|
Star,
|
||||||
|
Slash,
|
||||||
|
Tilde,
|
||||||
|
Grave,
|
||||||
|
At,
|
||||||
|
|
||||||
|
OperatorBegin = Dot,
|
||||||
|
LastToken = At,
|
||||||
|
};
|
||||||
|
|
||||||
|
std::string_view TokenTypeToStr(TokenType);
|
||||||
|
|
||||||
|
struct AssemblerToken
|
||||||
|
{
|
||||||
|
TokenType token_type;
|
||||||
|
std::string_view token_val;
|
||||||
|
std::string_view invalid_reason;
|
||||||
|
// Within an invalid token, specifies the erroneous region
|
||||||
|
Interval invalid_region;
|
||||||
|
|
||||||
|
std::string_view TypeStr() const;
|
||||||
|
std::string_view ValStr() const;
|
||||||
|
|
||||||
|
// Supported Templates:
|
||||||
|
// u8, u16, u32, u64, float, double
|
||||||
|
template <typename T>
|
||||||
|
std::optional<T> EvalToken() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CursorPosition
|
||||||
|
{
|
||||||
|
size_t index = 0;
|
||||||
|
size_t line = 0;
|
||||||
|
size_t col = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class Lexer
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
enum class IdentifierMatchRule
|
||||||
|
{
|
||||||
|
Typical,
|
||||||
|
Mnemonic, // Mnemonics can contain +, -, or . to specify branch prediction rules and link bit
|
||||||
|
Directive, // Directives can start with a digit
|
||||||
|
};
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit Lexer(std::string_view str)
|
||||||
|
: m_lex_string(str), m_match_rule(IdentifierMatchRule::Typical)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t LineNumber() const;
|
||||||
|
size_t ColNumber() const;
|
||||||
|
std::string_view CurrentLine() const;
|
||||||
|
|
||||||
|
// Since there's only one place floats get lexed, it's 'okay' to have an explicit
|
||||||
|
// "lex a float token" function
|
||||||
|
void SetIdentifierMatchRule(IdentifierMatchRule set);
|
||||||
|
const Tagged<CursorPosition, AssemblerToken>& LookaheadTagRef(size_t num_fwd) const;
|
||||||
|
AssemblerToken Lookahead() const;
|
||||||
|
const AssemblerToken& LookaheadRef() const;
|
||||||
|
TokenType LookaheadType() const;
|
||||||
|
// Since there's only one place floats get lexed, it's 'okay' to have an explicit
|
||||||
|
// "lex a float token" function
|
||||||
|
AssemblerToken LookaheadFloat() const;
|
||||||
|
void Eat();
|
||||||
|
void EatAndReset();
|
||||||
|
|
||||||
|
template <size_t N>
|
||||||
|
void LookaheadTaggedN(std::array<Tagged<CursorPosition, AssemblerToken>, N>* tokens_out) const
|
||||||
|
{
|
||||||
|
const size_t filled_amt = std::min(m_lexed_tokens.size(), N);
|
||||||
|
|
||||||
|
std::copy_n(m_lexed_tokens.begin(), filled_amt, tokens_out->begin());
|
||||||
|
|
||||||
|
std::generate_n(tokens_out->begin() + filled_amt, N - filled_amt, [this] {
|
||||||
|
CursorPosition p = m_pos;
|
||||||
|
return m_lexed_tokens.emplace_back(p, LexSingle());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
template <size_t N>
|
||||||
|
void LookaheadN(std::array<AssemblerToken, N>* tokens_out) const
|
||||||
|
{
|
||||||
|
const size_t filled_amt = std::min(m_lexed_tokens.size(), N);
|
||||||
|
|
||||||
|
auto _it = m_lexed_tokens.begin();
|
||||||
|
std::generate_n(tokens_out->begin(), filled_amt, [&_it] { return ValueOf(*_it++); });
|
||||||
|
|
||||||
|
std::generate_n(tokens_out->begin() + filled_amt, N - filled_amt, [this] {
|
||||||
|
CursorPosition p = m_pos;
|
||||||
|
return ValueOf(m_lexed_tokens.emplace_back(p, LexSingle()));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
template <size_t N>
|
||||||
|
void EatN()
|
||||||
|
{
|
||||||
|
size_t consumed = 0;
|
||||||
|
while (m_lexed_tokens.size() > 0 && consumed < N)
|
||||||
|
{
|
||||||
|
m_lexed_tokens.pop_front();
|
||||||
|
consumed++;
|
||||||
|
}
|
||||||
|
for (size_t i = consumed; i < N; i++)
|
||||||
|
{
|
||||||
|
LexSingle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::optional<std::string_view> RunDfa(const std::vector<DfaNode>& dfa) const;
|
||||||
|
void SkipWs() const;
|
||||||
|
void FeedbackTokens() const;
|
||||||
|
bool IdentifierHeadExtra(char h) const;
|
||||||
|
bool IdentifierExtra(char c) const;
|
||||||
|
void ScanStart() const;
|
||||||
|
void ScanFinish() const;
|
||||||
|
std::string_view ScanFinishOut() const;
|
||||||
|
char Peek() const;
|
||||||
|
const Lexer& Step() const;
|
||||||
|
TokenType LexStringLit(std::string_view& invalid_reason, Interval& invalid_region) const;
|
||||||
|
TokenType ClassifyAlnum() const;
|
||||||
|
AssemblerToken LexSingle() const;
|
||||||
|
|
||||||
|
std::string_view m_lex_string;
|
||||||
|
mutable CursorPosition m_pos;
|
||||||
|
mutable CursorPosition m_scan_pos;
|
||||||
|
mutable std::deque<Tagged<CursorPosition, AssemblerToken>> m_lexed_tokens;
|
||||||
|
IdentifierMatchRule m_match_rule;
|
||||||
|
};
|
||||||
|
} // namespace Common::GekkoAssembler::detail
|
885
Source/Core/Common/Assembler/GekkoParser.cpp
Normal file
885
Source/Core/Common/Assembler/GekkoParser.cpp
Normal file
@ -0,0 +1,885 @@
|
|||||||
|
// Copyright 2023 Dolphin Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include "Common/Assembler/GekkoParser.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
|
#include <fmt/format.h>
|
||||||
|
|
||||||
|
#include "Common/Assembler/AssemblerShared.h"
|
||||||
|
#include "Common/Assembler/AssemblerTables.h"
|
||||||
|
#include "Common/Assembler/GekkoLexer.h"
|
||||||
|
#include "Common/Assert.h"
|
||||||
|
|
||||||
|
namespace Common::GekkoAssembler::detail
|
||||||
|
{
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
bool MatchOperandFirst(const AssemblerToken& tok)
|
||||||
|
{
|
||||||
|
switch (tok.token_type)
|
||||||
|
{
|
||||||
|
case TokenType::Minus:
|
||||||
|
case TokenType::Tilde:
|
||||||
|
case TokenType::Lparen:
|
||||||
|
case TokenType::Grave:
|
||||||
|
case TokenType::Identifier:
|
||||||
|
case TokenType::DecimalLit:
|
||||||
|
case TokenType::OctalLit:
|
||||||
|
case TokenType::HexadecimalLit:
|
||||||
|
case TokenType::BinaryLit:
|
||||||
|
case TokenType::Dot:
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ParseImm(ParseState* state)
|
||||||
|
{
|
||||||
|
AssemblerToken tok = state->lexer.Lookahead();
|
||||||
|
switch (tok.token_type)
|
||||||
|
{
|
||||||
|
case TokenType::HexadecimalLit:
|
||||||
|
state->plugin.OnTerminal(Terminal::Hex, tok);
|
||||||
|
break;
|
||||||
|
case TokenType::DecimalLit:
|
||||||
|
state->plugin.OnTerminal(Terminal::Dec, tok);
|
||||||
|
break;
|
||||||
|
case TokenType::OctalLit:
|
||||||
|
state->plugin.OnTerminal(Terminal::Oct, tok);
|
||||||
|
break;
|
||||||
|
case TokenType::BinaryLit:
|
||||||
|
state->plugin.OnTerminal(Terminal::Bin, tok);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
state->EmitErrorHere(fmt::format("Invalid {} with value '{}'", tok.TypeStr(), tok.ValStr()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (state->error)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state->lexer.Eat();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ParseId(ParseState* state)
|
||||||
|
{
|
||||||
|
AssemblerToken tok = state->lexer.Lookahead();
|
||||||
|
if (tok.token_type == TokenType::Identifier)
|
||||||
|
{
|
||||||
|
state->plugin.OnTerminal(Terminal::Id, tok);
|
||||||
|
if (state->error)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state->lexer.Eat();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
state->EmitErrorHere(fmt::format("Expected an identifier, but found '{}'", tok.ValStr()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ParseIdLocation(ParseState* state)
|
||||||
|
{
|
||||||
|
std::array<AssemblerToken, 3> toks;
|
||||||
|
state->lexer.LookaheadN(&toks);
|
||||||
|
|
||||||
|
if (toks[1].token_type == TokenType::At)
|
||||||
|
{
|
||||||
|
if (toks[2].token_val == "ha")
|
||||||
|
{
|
||||||
|
state->plugin.OnHiaddr(toks[0].token_val);
|
||||||
|
if (state->error)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state->lexer.EatN<3>();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else if (toks[2].token_val == "l")
|
||||||
|
{
|
||||||
|
state->plugin.OnLoaddr(toks[0].token_val);
|
||||||
|
if (state->error)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state->lexer.EatN<3>();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ParseId(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ParsePpcBuiltin(ParseState* state)
|
||||||
|
{
|
||||||
|
AssemblerToken tok = state->lexer.Lookahead();
|
||||||
|
switch (tok.token_type)
|
||||||
|
{
|
||||||
|
case TokenType::GPR:
|
||||||
|
state->plugin.OnTerminal(Terminal::GPR, tok);
|
||||||
|
break;
|
||||||
|
case TokenType::FPR:
|
||||||
|
state->plugin.OnTerminal(Terminal::FPR, tok);
|
||||||
|
break;
|
||||||
|
case TokenType::SPR:
|
||||||
|
state->plugin.OnTerminal(Terminal::SPR, tok);
|
||||||
|
break;
|
||||||
|
case TokenType::CRField:
|
||||||
|
state->plugin.OnTerminal(Terminal::CRField, tok);
|
||||||
|
break;
|
||||||
|
case TokenType::Lt:
|
||||||
|
state->plugin.OnTerminal(Terminal::Lt, tok);
|
||||||
|
break;
|
||||||
|
case TokenType::Gt:
|
||||||
|
state->plugin.OnTerminal(Terminal::Gt, tok);
|
||||||
|
break;
|
||||||
|
case TokenType::Eq:
|
||||||
|
state->plugin.OnTerminal(Terminal::Eq, tok);
|
||||||
|
break;
|
||||||
|
case TokenType::So:
|
||||||
|
state->plugin.OnTerminal(Terminal::So, tok);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
state->EmitErrorHere(
|
||||||
|
fmt::format("Unexpected token '{}' in ppc builtin", state->lexer.LookaheadRef().ValStr()));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (state->error)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state->lexer.Eat();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ParseBaseexpr(ParseState* state)
|
||||||
|
{
|
||||||
|
TokenType tok = state->lexer.LookaheadType();
|
||||||
|
switch (tok)
|
||||||
|
{
|
||||||
|
case TokenType::HexadecimalLit:
|
||||||
|
case TokenType::DecimalLit:
|
||||||
|
case TokenType::OctalLit:
|
||||||
|
case TokenType::BinaryLit:
|
||||||
|
ParseImm(state);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TokenType::Identifier:
|
||||||
|
ParseIdLocation(state);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TokenType::GPR:
|
||||||
|
case TokenType::FPR:
|
||||||
|
case TokenType::SPR:
|
||||||
|
case TokenType::CRField:
|
||||||
|
case TokenType::Lt:
|
||||||
|
case TokenType::Gt:
|
||||||
|
case TokenType::Eq:
|
||||||
|
case TokenType::So:
|
||||||
|
ParsePpcBuiltin(state);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TokenType::Dot:
|
||||||
|
state->plugin.OnTerminal(Terminal::Dot, state->lexer.Lookahead());
|
||||||
|
if (state->error)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state->lexer.Eat();
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
state->EmitErrorHere(
|
||||||
|
fmt::format("Unexpected token '{}' in expression", state->lexer.LookaheadRef().ValStr()));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ParseBitor(ParseState* state);
|
||||||
|
void ParseParen(ParseState* state)
|
||||||
|
{
|
||||||
|
if (state->HasToken(TokenType::Lparen))
|
||||||
|
{
|
||||||
|
state->plugin.OnOpenParen(ParenType::Normal);
|
||||||
|
if (state->error)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state->lexer.Eat();
|
||||||
|
ParseBitor(state);
|
||||||
|
if (state->error)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state->HasToken(TokenType::Rparen))
|
||||||
|
{
|
||||||
|
state->plugin.OnCloseParen(ParenType::Normal);
|
||||||
|
}
|
||||||
|
state->ParseToken(TokenType::Rparen);
|
||||||
|
}
|
||||||
|
else if (state->HasToken(TokenType::Grave))
|
||||||
|
{
|
||||||
|
state->plugin.OnOpenParen(ParenType::RelConv);
|
||||||
|
|
||||||
|
state->lexer.Eat();
|
||||||
|
ParseBitor(state);
|
||||||
|
if (state->error)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state->HasToken(TokenType::Grave))
|
||||||
|
{
|
||||||
|
state->plugin.OnCloseParen(ParenType::RelConv);
|
||||||
|
}
|
||||||
|
state->ParseToken(TokenType::Grave);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ParseBaseexpr(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ParseUnary(ParseState* state)
|
||||||
|
{
|
||||||
|
TokenType tok = state->lexer.LookaheadType();
|
||||||
|
if (tok == TokenType::Minus || tok == TokenType::Tilde)
|
||||||
|
{
|
||||||
|
state->lexer.Eat();
|
||||||
|
ParseUnary(state);
|
||||||
|
if (state->error)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tok == TokenType::Minus)
|
||||||
|
{
|
||||||
|
state->plugin.OnOperator(AsmOp::Neg);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
state->plugin.OnOperator(AsmOp::Not);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ParseParen(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ParseMultiplication(ParseState* state)
|
||||||
|
{
|
||||||
|
ParseUnary(state);
|
||||||
|
if (state->error)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TokenType tok = state->lexer.LookaheadType();
|
||||||
|
while (tok == TokenType::Star || tok == TokenType::Slash)
|
||||||
|
{
|
||||||
|
state->lexer.Eat();
|
||||||
|
ParseUnary(state);
|
||||||
|
if (state->error)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tok == TokenType::Star)
|
||||||
|
{
|
||||||
|
state->plugin.OnOperator(AsmOp::Mul);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
state->plugin.OnOperator(AsmOp::Div);
|
||||||
|
}
|
||||||
|
tok = state->lexer.LookaheadType();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ParseAddition(ParseState* state)
|
||||||
|
{
|
||||||
|
ParseMultiplication(state);
|
||||||
|
if (state->error)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TokenType tok = state->lexer.LookaheadType();
|
||||||
|
while (tok == TokenType::Plus || tok == TokenType::Minus)
|
||||||
|
{
|
||||||
|
state->lexer.Eat();
|
||||||
|
ParseMultiplication(state);
|
||||||
|
if (state->error)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tok == TokenType::Plus)
|
||||||
|
{
|
||||||
|
state->plugin.OnOperator(AsmOp::Add);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
state->plugin.OnOperator(AsmOp::Sub);
|
||||||
|
}
|
||||||
|
tok = state->lexer.LookaheadType();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ParseShift(ParseState* state)
|
||||||
|
{
|
||||||
|
ParseAddition(state);
|
||||||
|
if (state->error)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TokenType tok = state->lexer.LookaheadType();
|
||||||
|
while (tok == TokenType::Lsh || tok == TokenType::Rsh)
|
||||||
|
{
|
||||||
|
state->lexer.Eat();
|
||||||
|
ParseAddition(state);
|
||||||
|
if (state->error)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tok == TokenType::Lsh)
|
||||||
|
{
|
||||||
|
state->plugin.OnOperator(AsmOp::Lsh);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
state->plugin.OnOperator(AsmOp::Rsh);
|
||||||
|
}
|
||||||
|
tok = state->lexer.LookaheadType();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ParseBitand(ParseState* state)
|
||||||
|
{
|
||||||
|
ParseShift(state);
|
||||||
|
if (state->error)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (state->HasToken(TokenType::Ampersand))
|
||||||
|
{
|
||||||
|
state->lexer.Eat();
|
||||||
|
ParseShift(state);
|
||||||
|
if (state->error)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state->plugin.OnOperator(AsmOp::And);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ParseBitxor(ParseState* state)
|
||||||
|
{
|
||||||
|
ParseBitand(state);
|
||||||
|
if (state->error)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (state->HasToken(TokenType::Caret))
|
||||||
|
{
|
||||||
|
state->lexer.Eat();
|
||||||
|
ParseBitand(state);
|
||||||
|
if (state->error)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state->plugin.OnOperator(AsmOp::Xor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ParseBitor(ParseState* state)
|
||||||
|
{
|
||||||
|
ParseBitxor(state);
|
||||||
|
if (state->error)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (state->HasToken(TokenType::Pipe))
|
||||||
|
{
|
||||||
|
state->lexer.Eat();
|
||||||
|
ParseBitxor(state);
|
||||||
|
if (state->error)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state->plugin.OnOperator(AsmOp::Or);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ParseOperand(ParseState* state)
|
||||||
|
{
|
||||||
|
state->plugin.OnOperandPre();
|
||||||
|
ParseBitor(state);
|
||||||
|
if (state->error)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state->plugin.OnOperandPost();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ParseOperandList(ParseState* state, ParseAlg alg)
|
||||||
|
{
|
||||||
|
if (alg == ParseAlg::None)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (alg == ParseAlg::NoneOrOp1)
|
||||||
|
{
|
||||||
|
if (MatchOperandFirst(state->lexer.Lookahead()))
|
||||||
|
{
|
||||||
|
ParseOperand(state);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ParseStep
|
||||||
|
{
|
||||||
|
_Operand,
|
||||||
|
_Comma,
|
||||||
|
_Lparen,
|
||||||
|
_Rparen,
|
||||||
|
_OptComma
|
||||||
|
};
|
||||||
|
std::vector<ParseStep> steps;
|
||||||
|
|
||||||
|
switch (alg)
|
||||||
|
{
|
||||||
|
case ParseAlg::Op1:
|
||||||
|
steps = {_Operand};
|
||||||
|
break;
|
||||||
|
case ParseAlg::Op1Or2:
|
||||||
|
steps = {_Operand, _OptComma, _Operand};
|
||||||
|
break;
|
||||||
|
case ParseAlg::Op2Or3:
|
||||||
|
steps = {_Operand, _Comma, _Operand, _OptComma, _Operand};
|
||||||
|
break;
|
||||||
|
case ParseAlg::Op1Off1:
|
||||||
|
steps = {_Operand, _Comma, _Operand, _Lparen, _Operand, _Rparen};
|
||||||
|
break;
|
||||||
|
case ParseAlg::Op2:
|
||||||
|
steps = {_Operand, _Comma, _Operand};
|
||||||
|
break;
|
||||||
|
case ParseAlg::Op3:
|
||||||
|
steps = {_Operand, _Comma, _Operand, _Comma, _Operand};
|
||||||
|
break;
|
||||||
|
case ParseAlg::Op4:
|
||||||
|
steps = {_Operand, _Comma, _Operand, _Comma, _Operand, _Comma, _Operand};
|
||||||
|
break;
|
||||||
|
case ParseAlg::Op5:
|
||||||
|
steps = {_Operand, _Comma, _Operand, _Comma, _Operand, _Comma, _Operand, _Comma, _Operand};
|
||||||
|
break;
|
||||||
|
case ParseAlg::Op1Off1Op2:
|
||||||
|
steps = {_Operand, _Comma, _Operand, _Lparen, _Operand,
|
||||||
|
_Rparen, _Comma, _Operand, _Comma, _Operand};
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
ASSERT(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (ParseStep step : steps)
|
||||||
|
{
|
||||||
|
bool stop_parse = false;
|
||||||
|
switch (step)
|
||||||
|
{
|
||||||
|
case _Operand:
|
||||||
|
ParseOperand(state);
|
||||||
|
break;
|
||||||
|
case _Comma:
|
||||||
|
state->ParseToken(TokenType::Comma);
|
||||||
|
break;
|
||||||
|
case _Lparen:
|
||||||
|
state->ParseToken(TokenType::Lparen);
|
||||||
|
break;
|
||||||
|
case _Rparen:
|
||||||
|
state->ParseToken(TokenType::Rparen);
|
||||||
|
break;
|
||||||
|
case _OptComma:
|
||||||
|
if (state->HasToken(TokenType::Comma))
|
||||||
|
{
|
||||||
|
state->ParseToken(TokenType::Comma);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
stop_parse = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (state->error)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (stop_parse)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ParseInstruction(ParseState* state)
|
||||||
|
{
|
||||||
|
state->lexer.SetIdentifierMatchRule(Lexer::IdentifierMatchRule::Mnemonic);
|
||||||
|
|
||||||
|
AssemblerToken mnemonic_token = state->lexer.Lookahead();
|
||||||
|
if (mnemonic_token.token_type != TokenType::Identifier)
|
||||||
|
{
|
||||||
|
state->lexer.SetIdentifierMatchRule(Lexer::IdentifierMatchRule::Typical);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ParseInfo const* parse_info = mnemonic_tokens.Find(mnemonic_token.token_val);
|
||||||
|
bool is_extended = false;
|
||||||
|
if (parse_info == nullptr)
|
||||||
|
{
|
||||||
|
parse_info = extended_mnemonic_tokens.Find(mnemonic_token.token_val);
|
||||||
|
if (parse_info == nullptr)
|
||||||
|
{
|
||||||
|
state->EmitErrorHere(
|
||||||
|
fmt::format("Unknown or unsupported mnemonic '{}'", mnemonic_token.ValStr()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
is_extended = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
state->plugin.OnInstructionPre(*parse_info, is_extended);
|
||||||
|
|
||||||
|
state->lexer.EatAndReset();
|
||||||
|
|
||||||
|
ParseOperandList(state, parse_info->parse_algorithm);
|
||||||
|
if (state->error)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state->plugin.OnInstructionPost(*parse_info, is_extended);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ParseLabel(ParseState* state)
|
||||||
|
{
|
||||||
|
std::array<AssemblerToken, 2> tokens;
|
||||||
|
state->lexer.LookaheadN(&tokens);
|
||||||
|
|
||||||
|
if (tokens[0].token_type == TokenType::Identifier && tokens[1].token_type == TokenType::Colon)
|
||||||
|
{
|
||||||
|
state->plugin.OnLabelDecl(tokens[0].token_val);
|
||||||
|
if (state->error)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state->lexer.EatN<2>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ParseResolvedExpr(ParseState* state)
|
||||||
|
{
|
||||||
|
state->plugin.OnResolvedExprPre();
|
||||||
|
ParseBitor(state);
|
||||||
|
if (state->error)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state->plugin.OnResolvedExprPost();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ParseExpressionList(ParseState* state)
|
||||||
|
{
|
||||||
|
ParseResolvedExpr(state);
|
||||||
|
if (state->error)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (state->HasToken(TokenType::Comma))
|
||||||
|
{
|
||||||
|
state->lexer.Eat();
|
||||||
|
ParseResolvedExpr(state);
|
||||||
|
if (state->error)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ParseFloat(ParseState* state)
|
||||||
|
{
|
||||||
|
AssemblerToken flt_token = state->lexer.LookaheadFloat();
|
||||||
|
if (flt_token.token_type != TokenType::FloatLit)
|
||||||
|
{
|
||||||
|
state->EmitErrorHere("Invalid floating point literal");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state->plugin.OnTerminal(Terminal::Flt, flt_token);
|
||||||
|
state->lexer.Eat();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ParseFloatList(ParseState* state)
|
||||||
|
{
|
||||||
|
ParseFloat(state);
|
||||||
|
if (state->error)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (state->HasToken(TokenType::Comma))
|
||||||
|
{
|
||||||
|
state->lexer.Eat();
|
||||||
|
ParseFloat(state);
|
||||||
|
if (state->error)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ParseDefvar(ParseState* state)
|
||||||
|
{
|
||||||
|
AssemblerToken tok = state->lexer.Lookahead();
|
||||||
|
if (tok.token_type == TokenType::Identifier)
|
||||||
|
{
|
||||||
|
state->plugin.OnVarDecl(tok.token_val);
|
||||||
|
if (state->error)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state->lexer.Eat();
|
||||||
|
|
||||||
|
state->ParseToken(TokenType::Comma);
|
||||||
|
if (state->error)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ParseResolvedExpr(state);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
state->EmitErrorHere(fmt::format("Expected an identifier, but found '{}'", tok.ValStr()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ParseString(ParseState* state)
|
||||||
|
{
|
||||||
|
AssemblerToken tok = state->lexer.Lookahead();
|
||||||
|
if (tok.token_type == TokenType::StringLit)
|
||||||
|
{
|
||||||
|
state->plugin.OnTerminal(Terminal::Str, tok);
|
||||||
|
state->lexer.Eat();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
state->EmitErrorHere(fmt::format("Expected a string literal, but found '{}'", tok.ValStr()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ParseDirective(ParseState* state)
|
||||||
|
{
|
||||||
|
// TODO: test directives
|
||||||
|
state->lexer.SetIdentifierMatchRule(Lexer::IdentifierMatchRule::Directive);
|
||||||
|
AssemblerToken tok = state->lexer.Lookahead();
|
||||||
|
if (tok.token_type != TokenType::Identifier)
|
||||||
|
{
|
||||||
|
state->EmitErrorHere(fmt::format("Unexpected token '{}' in directive type", tok.ValStr()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
GekkoDirective const* directive_enum = directives_map.Find(tok.token_val);
|
||||||
|
if (directive_enum == nullptr)
|
||||||
|
{
|
||||||
|
state->EmitErrorHere(fmt::format("Unknown assembler directive '{}'", tok.ValStr()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state->plugin.OnDirectivePre(*directive_enum);
|
||||||
|
|
||||||
|
state->lexer.EatAndReset();
|
||||||
|
switch (*directive_enum)
|
||||||
|
{
|
||||||
|
case GekkoDirective::Byte:
|
||||||
|
case GekkoDirective::_2byte:
|
||||||
|
case GekkoDirective::_4byte:
|
||||||
|
case GekkoDirective::_8byte:
|
||||||
|
ParseExpressionList(state);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case GekkoDirective::Float:
|
||||||
|
case GekkoDirective::Double:
|
||||||
|
ParseFloatList(state);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case GekkoDirective::Locate:
|
||||||
|
case GekkoDirective::Zeros:
|
||||||
|
case GekkoDirective::Skip:
|
||||||
|
ParseResolvedExpr(state);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case GekkoDirective::PadAlign:
|
||||||
|
case GekkoDirective::Align:
|
||||||
|
ParseImm(state);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case GekkoDirective::DefVar:
|
||||||
|
ParseDefvar(state);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case GekkoDirective::Ascii:
|
||||||
|
case GekkoDirective::Asciz:
|
||||||
|
ParseString(state);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state->error)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state->plugin.OnDirectivePost(*directive_enum);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ParseLine(ParseState* state)
|
||||||
|
{
|
||||||
|
if (state->HasToken(TokenType::Dot))
|
||||||
|
{
|
||||||
|
state->ParseToken(TokenType::Dot);
|
||||||
|
ParseDirective(state);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ParseInstruction(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ParseProgram(ParseState* state)
|
||||||
|
{
|
||||||
|
AssemblerToken tok = state->lexer.Lookahead();
|
||||||
|
if (tok.token_type == TokenType::Eof)
|
||||||
|
{
|
||||||
|
state->eof = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ParseLabel(state);
|
||||||
|
if (state->error)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ParseLine(state);
|
||||||
|
if (state->error)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (!state->eof && !state->error)
|
||||||
|
{
|
||||||
|
tok = state->lexer.Lookahead();
|
||||||
|
if (tok.token_type == TokenType::Eof)
|
||||||
|
{
|
||||||
|
state->eof = true;
|
||||||
|
}
|
||||||
|
else if (tok.token_type == TokenType::Eol)
|
||||||
|
{
|
||||||
|
state->lexer.Eat();
|
||||||
|
ParseLabel(state);
|
||||||
|
if (state->error)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ParseLine(state);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
state->EmitErrorHere(
|
||||||
|
fmt::format("Unexpected token '{}' where line should have ended", tok.ValStr()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
ParseState::ParseState(std::string_view input_str, ParsePlugin& p)
|
||||||
|
: lexer(input_str), plugin(p), eof(false)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ParseState::HasToken(TokenType tp) const
|
||||||
|
{
|
||||||
|
return lexer.LookaheadType() == tp;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ParseState::ParseToken(TokenType tp)
|
||||||
|
{
|
||||||
|
AssemblerToken tok = lexer.LookaheadRef();
|
||||||
|
if (tok.token_type == tp)
|
||||||
|
{
|
||||||
|
lexer.Eat();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
EmitErrorHere(fmt::format("Expected '{}' but found '{}'", TokenTypeToStr(tp), tok.ValStr()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ParseState::EmitErrorHere(std::string&& message)
|
||||||
|
{
|
||||||
|
AssemblerToken cur_token = lexer.Lookahead();
|
||||||
|
if (cur_token.token_type == TokenType::Invalid)
|
||||||
|
{
|
||||||
|
error = AssemblerError{
|
||||||
|
std::string(cur_token.invalid_reason),
|
||||||
|
lexer.CurrentLine(),
|
||||||
|
lexer.LineNumber(),
|
||||||
|
lexer.ColNumber() + cur_token.invalid_region.begin,
|
||||||
|
cur_token.invalid_region.len,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
error = AssemblerError{
|
||||||
|
std::move(message), lexer.CurrentLine(), lexer.LineNumber(),
|
||||||
|
lexer.ColNumber(), cur_token.token_val.size(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ParseWithPlugin(ParsePlugin* plugin, std::string_view input)
|
||||||
|
{
|
||||||
|
ParseState parse_state = ParseState(input, *plugin);
|
||||||
|
plugin->SetOwner(&parse_state);
|
||||||
|
ParseProgram(&parse_state);
|
||||||
|
|
||||||
|
if (parse_state.error)
|
||||||
|
{
|
||||||
|
plugin->OnError();
|
||||||
|
plugin->ForwardError(std::move(*parse_state.error));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
plugin->PostParseAction();
|
||||||
|
if (parse_state.error)
|
||||||
|
{
|
||||||
|
plugin->OnError();
|
||||||
|
plugin->ForwardError(std::move(*parse_state.error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugin->SetOwner(nullptr);
|
||||||
|
}
|
||||||
|
} // namespace Common::GekkoAssembler::detail
|
124
Source/Core/Common/Assembler/GekkoParser.h
Normal file
124
Source/Core/Common/Assembler/GekkoParser.h
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
// Copyright 2023 Dolphin Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
|
#include "Common/Assembler/AssemblerShared.h"
|
||||||
|
#include "Common/Assembler/GekkoLexer.h"
|
||||||
|
#include "Common/CommonTypes.h"
|
||||||
|
|
||||||
|
namespace Common::GekkoAssembler::detail
|
||||||
|
{
|
||||||
|
class ParsePlugin;
|
||||||
|
|
||||||
|
struct ParseState
|
||||||
|
{
|
||||||
|
ParseState(std::string_view input_str, ParsePlugin& plugin);
|
||||||
|
|
||||||
|
bool HasToken(TokenType tp) const;
|
||||||
|
void ParseToken(TokenType tp);
|
||||||
|
void EmitErrorHere(std::string&& message);
|
||||||
|
|
||||||
|
Lexer lexer;
|
||||||
|
ParsePlugin& plugin;
|
||||||
|
|
||||||
|
std::optional<AssemblerError> error;
|
||||||
|
bool eof;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class AsmOp
|
||||||
|
{
|
||||||
|
Or,
|
||||||
|
Xor,
|
||||||
|
And,
|
||||||
|
Lsh,
|
||||||
|
Rsh,
|
||||||
|
Add,
|
||||||
|
Sub,
|
||||||
|
Mul,
|
||||||
|
Div,
|
||||||
|
Neg,
|
||||||
|
Not
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class Terminal
|
||||||
|
{
|
||||||
|
Hex,
|
||||||
|
Dec,
|
||||||
|
Oct,
|
||||||
|
Bin,
|
||||||
|
Flt,
|
||||||
|
Str,
|
||||||
|
Id,
|
||||||
|
GPR,
|
||||||
|
FPR,
|
||||||
|
SPR,
|
||||||
|
CRField,
|
||||||
|
Lt,
|
||||||
|
Gt,
|
||||||
|
Eq,
|
||||||
|
So,
|
||||||
|
Dot,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class ParenType
|
||||||
|
{
|
||||||
|
Normal,
|
||||||
|
RelConv,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Overridable plugin class supporting a series of skeleton functions which get called when
|
||||||
|
// the parser parses a given point of interest
|
||||||
|
class ParsePlugin
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ParsePlugin() : m_owner(nullptr) {}
|
||||||
|
virtual ~ParsePlugin() = default;
|
||||||
|
|
||||||
|
void SetOwner(ParseState* o) { m_owner = o; }
|
||||||
|
void ForwardError(AssemblerError&& err) { m_owner_error = std::move(err); }
|
||||||
|
std::optional<AssemblerError>& Error() { return m_owner_error; }
|
||||||
|
|
||||||
|
virtual void PostParseAction() {}
|
||||||
|
|
||||||
|
// Nonterminal callouts
|
||||||
|
// Pre occurs prior to the head nonterminal being parsed
|
||||||
|
// Post occurs after the nonterminal has been fully parsed
|
||||||
|
virtual void OnDirectivePre(GekkoDirective directive) {}
|
||||||
|
virtual void OnDirectivePost(GekkoDirective directive) {}
|
||||||
|
virtual void OnInstructionPre(const ParseInfo& mnemonic_info, bool extended) {}
|
||||||
|
virtual void OnInstructionPost(const ParseInfo& mnemonic_info, bool extended) {}
|
||||||
|
virtual void OnOperandPre() {}
|
||||||
|
virtual void OnOperandPost() {}
|
||||||
|
virtual void OnResolvedExprPre() {}
|
||||||
|
virtual void OnResolvedExprPost() {}
|
||||||
|
|
||||||
|
// Operator callouts
|
||||||
|
// All occur after the relevant operands have been parsed
|
||||||
|
virtual void OnOperator(AsmOp operation) {}
|
||||||
|
|
||||||
|
// Individual token callouts
|
||||||
|
// All occur prior to the token being parsed
|
||||||
|
// Due to ambiguity of some tokens, an explicit operation is provided
|
||||||
|
virtual void OnTerminal(Terminal type, const AssemblerToken& val) {}
|
||||||
|
virtual void OnHiaddr(std::string_view id) {}
|
||||||
|
virtual void OnLoaddr(std::string_view id) {}
|
||||||
|
virtual void OnOpenParen(ParenType type) {}
|
||||||
|
virtual void OnCloseParen(ParenType type) {}
|
||||||
|
virtual void OnError() {}
|
||||||
|
virtual void OnLabelDecl(std::string_view name) {}
|
||||||
|
virtual void OnVarDecl(std::string_view name) {}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
ParseState* m_owner;
|
||||||
|
std::optional<AssemblerError> m_owner_error;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parse the provided input with a plugin to handle what to do with certain points of interest
|
||||||
|
// e.g. Convert to an IR for generating final machine code, picking up syntactical information
|
||||||
|
void ParseWithPlugin(ParsePlugin* plugin, std::string_view input);
|
||||||
|
} // namespace Common::GekkoAssembler::detail
|
@ -1,6 +1,18 @@
|
|||||||
add_library(common
|
add_library(common
|
||||||
Analytics.cpp
|
Analytics.cpp
|
||||||
Analytics.h
|
Analytics.h
|
||||||
|
Assembler/AssemblerShared.cpp
|
||||||
|
Assembler/AssemblerShared.h
|
||||||
|
Assembler/AssemblerTables.cpp
|
||||||
|
Assembler/AssemblerTables.h
|
||||||
|
Assembler/GekkoAssembler.cpp
|
||||||
|
Assembler/GekkoAssembler.h
|
||||||
|
Assembler/GekkoIRGen.cpp
|
||||||
|
Assembler/GekkoIRGen.h
|
||||||
|
Assembler/GekkoLexer.cpp
|
||||||
|
Assembler/GekkoLexer.h
|
||||||
|
Assembler/GekkoParser.cpp
|
||||||
|
Assembler/GekkoParser.h
|
||||||
Assert.h
|
Assert.h
|
||||||
BitField.h
|
BitField.h
|
||||||
BitSet.h
|
BitSet.h
|
||||||
|
@ -94,6 +94,7 @@
|
|||||||
#define DYNAMICINPUT_DIR "DynamicInputTextures"
|
#define DYNAMICINPUT_DIR "DynamicInputTextures"
|
||||||
#define GRAPHICSMOD_DIR "GraphicMods"
|
#define GRAPHICSMOD_DIR "GraphicMods"
|
||||||
#define WIISDSYNC_DIR "WiiSDSync"
|
#define WIISDSYNC_DIR "WiiSDSync"
|
||||||
|
#define ASSEMBLY_DIR "SavedAssembly"
|
||||||
|
|
||||||
// This one is only used to remove it if it was present
|
// This one is only used to remove it if it was present
|
||||||
#define SHADERCACHE_LEGACY_DIR "ShaderCache"
|
#define SHADERCACHE_LEGACY_DIR "ShaderCache"
|
||||||
|
@ -897,6 +897,8 @@ static void RebuildUserDirectories(unsigned int dir_index)
|
|||||||
s_user_paths[D_GBASAVES_IDX] = s_user_paths[D_GBAUSER_IDX] + GBASAVES_DIR DIR_SEP;
|
s_user_paths[D_GBASAVES_IDX] = s_user_paths[D_GBAUSER_IDX] + GBASAVES_DIR DIR_SEP;
|
||||||
s_user_paths[F_GBABIOS_IDX] = s_user_paths[D_GBAUSER_IDX] + GBA_BIOS;
|
s_user_paths[F_GBABIOS_IDX] = s_user_paths[D_GBAUSER_IDX] + GBA_BIOS;
|
||||||
|
|
||||||
|
s_user_paths[D_ASM_ROOT_IDX] = s_user_paths[D_USER_IDX] + ASSEMBLY_DIR DIR_SEP;
|
||||||
|
|
||||||
// The shader cache has moved to the cache directory, so remove the old one.
|
// The shader cache has moved to the cache directory, so remove the old one.
|
||||||
// TODO: remove that someday.
|
// TODO: remove that someday.
|
||||||
File::DeleteDirRecursively(s_user_paths[D_USER_IDX] + SHADERCACHE_LEGACY_DIR DIR_SEP);
|
File::DeleteDirRecursively(s_user_paths[D_USER_IDX] + SHADERCACHE_LEGACY_DIR DIR_SEP);
|
||||||
|
@ -71,6 +71,7 @@ enum
|
|||||||
D_GPU_DRIVERS_TMP,
|
D_GPU_DRIVERS_TMP,
|
||||||
D_GPU_DRIVERS_HOOKS,
|
D_GPU_DRIVERS_HOOKS,
|
||||||
D_GPU_DRIVERS_FILE_REDIRECT,
|
D_GPU_DRIVERS_FILE_REDIRECT,
|
||||||
|
D_ASM_ROOT_IDX,
|
||||||
FIRST_FILE_USER_PATH_IDX,
|
FIRST_FILE_USER_PATH_IDX,
|
||||||
F_DOLPHINCONFIG_IDX = FIRST_FILE_USER_PATH_IDX,
|
F_DOLPHINCONFIG_IDX = FIRST_FILE_USER_PATH_IDX,
|
||||||
F_GCPADCONFIG_IDX,
|
F_GCPADCONFIG_IDX,
|
||||||
|
@ -198,6 +198,12 @@ add_executable(dolphin-emu
|
|||||||
Config/WiimoteControllersWidget.h
|
Config/WiimoteControllersWidget.h
|
||||||
ConvertDialog.cpp
|
ConvertDialog.cpp
|
||||||
ConvertDialog.h
|
ConvertDialog.h
|
||||||
|
Debugger/AssembleInstructionDialog.cpp
|
||||||
|
Debugger/AssembleInstructionDialog.h
|
||||||
|
Debugger/AssemblerWidget.cpp
|
||||||
|
Debugger/AssemblerWidget.h
|
||||||
|
Debugger/AssemblyEditor.cpp
|
||||||
|
Debugger/AssemblyEditor.h
|
||||||
Debugger/BreakpointDialog.cpp
|
Debugger/BreakpointDialog.cpp
|
||||||
Debugger/BreakpointDialog.h
|
Debugger/BreakpointDialog.h
|
||||||
Debugger/BreakpointWidget.cpp
|
Debugger/BreakpointWidget.cpp
|
||||||
@ -208,6 +214,8 @@ add_executable(dolphin-emu
|
|||||||
Debugger/CodeViewWidget.h
|
Debugger/CodeViewWidget.h
|
||||||
Debugger/CodeWidget.cpp
|
Debugger/CodeWidget.cpp
|
||||||
Debugger/CodeWidget.h
|
Debugger/CodeWidget.h
|
||||||
|
Debugger/GekkoSyntaxHighlight.cpp
|
||||||
|
Debugger/GekkoSyntaxHighlight.h
|
||||||
Debugger/JITWidget.cpp
|
Debugger/JITWidget.cpp
|
||||||
Debugger/JITWidget.h
|
Debugger/JITWidget.h
|
||||||
Debugger/MemoryViewWidget.cpp
|
Debugger/MemoryViewWidget.cpp
|
||||||
|
129
Source/Core/DolphinQt/Debugger/AssembleInstructionDialog.cpp
Normal file
129
Source/Core/DolphinQt/Debugger/AssembleInstructionDialog.cpp
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
// Copyright 2023 Dolphin Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include "DolphinQt/Debugger/AssembleInstructionDialog.h"
|
||||||
|
|
||||||
|
#include <QDialogButtonBox>
|
||||||
|
#include <QFontDatabase>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QLineEdit>
|
||||||
|
#include <QPushButton>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
|
||||||
|
#include "Common/Assembler/GekkoAssembler.h"
|
||||||
|
#include "Common/StringUtil.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
QString HtmlFormatErrorLoc(const Common::GekkoAssembler::AssemblerError& err)
|
||||||
|
{
|
||||||
|
return QObject::tr("<span style=\"color: red; font-weight: bold\">Error</span> on line %1 col %2")
|
||||||
|
.arg(err.line + 1)
|
||||||
|
.arg(err.col + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString HtmlFormatErrorLine(const Common::GekkoAssembler::AssemblerError& err)
|
||||||
|
{
|
||||||
|
const QString line_pre_error =
|
||||||
|
QString::fromStdString(std::string(err.error_line.substr(0, err.col))).toHtmlEscaped();
|
||||||
|
const QString line_error =
|
||||||
|
QString::fromStdString(std::string(err.error_line.substr(err.col, err.len))).toHtmlEscaped();
|
||||||
|
const QString line_post_error =
|
||||||
|
QString::fromStdString(std::string(err.error_line.substr(err.col + err.len))).toHtmlEscaped();
|
||||||
|
|
||||||
|
return QObject::tr("%1<u><span style=\"color:red; font-weight:bold\">%2</span></u>%3")
|
||||||
|
.arg(line_pre_error)
|
||||||
|
.arg(line_error)
|
||||||
|
.arg(line_post_error);
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
AssembleInstructionDialog::AssembleInstructionDialog(QWidget* parent, u32 address, u32 value)
|
||||||
|
: QDialog(parent), m_code(value), m_address(address)
|
||||||
|
{
|
||||||
|
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
|
||||||
|
setWindowModality(Qt::WindowModal);
|
||||||
|
setWindowTitle(tr("Instruction"));
|
||||||
|
|
||||||
|
CreateWidgets();
|
||||||
|
ConnectWidgets();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AssembleInstructionDialog::CreateWidgets()
|
||||||
|
{
|
||||||
|
auto* layout = new QVBoxLayout;
|
||||||
|
|
||||||
|
m_input_edit = new QLineEdit;
|
||||||
|
m_error_loc_label = new QLabel;
|
||||||
|
m_error_line_label = new QLabel;
|
||||||
|
m_msg_label = new QLabel(tr("No input"));
|
||||||
|
m_button_box = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
|
||||||
|
|
||||||
|
m_error_line_label->setFont(QFont(QFontDatabase::systemFont(QFontDatabase::FixedFont).family()));
|
||||||
|
m_input_edit->setFont(QFont(QFontDatabase::systemFont(QFontDatabase::FixedFont).family()));
|
||||||
|
layout->addWidget(new QLabel(tr("Inline Assembler")));
|
||||||
|
layout->addWidget(m_error_loc_label);
|
||||||
|
layout->addWidget(m_input_edit);
|
||||||
|
layout->addWidget(m_error_line_label);
|
||||||
|
layout->addWidget(m_msg_label);
|
||||||
|
layout->addWidget(m_button_box);
|
||||||
|
m_input_edit->setText(QStringLiteral(".4byte 0x%1").arg(m_code, 8, 16, QLatin1Char('0')));
|
||||||
|
|
||||||
|
setLayout(layout);
|
||||||
|
OnEditChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AssembleInstructionDialog::ConnectWidgets()
|
||||||
|
{
|
||||||
|
connect(m_button_box, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
||||||
|
connect(m_button_box, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||||
|
|
||||||
|
connect(m_input_edit, &QLineEdit::textChanged, this, &AssembleInstructionDialog::OnEditChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AssembleInstructionDialog::OnEditChanged()
|
||||||
|
{
|
||||||
|
using namespace Common::GekkoAssembler;
|
||||||
|
std::string line = m_input_edit->text().toStdString();
|
||||||
|
Common::ToLower(&line);
|
||||||
|
|
||||||
|
FailureOr<std::vector<CodeBlock>> asm_result = Assemble(line, m_address);
|
||||||
|
|
||||||
|
if (IsFailure(asm_result))
|
||||||
|
{
|
||||||
|
m_button_box->button(QDialogButtonBox::Ok)->setEnabled(false);
|
||||||
|
|
||||||
|
const AssemblerError& failure = GetFailure(asm_result);
|
||||||
|
m_error_loc_label->setText(HtmlFormatErrorLoc(failure));
|
||||||
|
m_error_line_label->setText(HtmlFormatErrorLine(failure));
|
||||||
|
m_msg_label->setText(QString::fromStdString(failure.message).toHtmlEscaped());
|
||||||
|
}
|
||||||
|
else if (GetT(asm_result).empty() || GetT(asm_result)[0].instructions.empty())
|
||||||
|
{
|
||||||
|
m_button_box->button(QDialogButtonBox::Ok)->setEnabled(false);
|
||||||
|
|
||||||
|
m_error_loc_label->setText(tr("<span style=\"color: red; font-weight: bold\">Error</span>"));
|
||||||
|
m_error_line_label->clear();
|
||||||
|
m_msg_label->setText(tr("No input"));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
m_button_box->button(QDialogButtonBox::Ok)->setEnabled(true);
|
||||||
|
m_code = 0;
|
||||||
|
|
||||||
|
const std::vector<u8>& block_bytes = GetT(asm_result)[0].instructions;
|
||||||
|
for (size_t i = 0; i < 4 && i < block_bytes.size(); i++)
|
||||||
|
{
|
||||||
|
m_code = (m_code << 8) | block_bytes[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
m_error_loc_label->setText(tr("<span style=\"color: green; font-weight: bold\">Ok</span>"));
|
||||||
|
m_error_line_label->clear();
|
||||||
|
m_msg_label->setText(tr("Instruction: %1").arg(m_code, 8, 16, QLatin1Char('0')));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 AssembleInstructionDialog::GetCode() const
|
||||||
|
{
|
||||||
|
return m_code;
|
||||||
|
}
|
36
Source/Core/DolphinQt/Debugger/AssembleInstructionDialog.h
Normal file
36
Source/Core/DolphinQt/Debugger/AssembleInstructionDialog.h
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
// Copyright 2023 Dolphin Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QDialog>
|
||||||
|
|
||||||
|
#include "Common/CommonTypes.h"
|
||||||
|
|
||||||
|
class QDialogButtonBox;
|
||||||
|
class QLabel;
|
||||||
|
class QLineEdit;
|
||||||
|
|
||||||
|
class AssembleInstructionDialog : public QDialog
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit AssembleInstructionDialog(QWidget* parent, u32 address, u32 value);
|
||||||
|
|
||||||
|
u32 GetCode() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void CreateWidgets();
|
||||||
|
void ConnectWidgets();
|
||||||
|
|
||||||
|
void OnEditChanged();
|
||||||
|
|
||||||
|
u32 m_code;
|
||||||
|
u32 m_address;
|
||||||
|
|
||||||
|
QLineEdit* m_input_edit;
|
||||||
|
QLabel* m_error_loc_label;
|
||||||
|
QLabel* m_error_line_label;
|
||||||
|
QLabel* m_msg_label;
|
||||||
|
QDialogButtonBox* m_button_box;
|
||||||
|
};
|
957
Source/Core/DolphinQt/Debugger/AssemblerWidget.cpp
Normal file
957
Source/Core/DolphinQt/Debugger/AssemblerWidget.cpp
Normal file
@ -0,0 +1,957 @@
|
|||||||
|
// Copyright 2023 Dolphin Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include "DolphinQt/Debugger/AssemblerWidget.h"
|
||||||
|
|
||||||
|
#include <QAction>
|
||||||
|
#include <QApplication>
|
||||||
|
#include <QClipboard>
|
||||||
|
#include <QComboBox>
|
||||||
|
#include <QFont>
|
||||||
|
#include <QFontDatabase>
|
||||||
|
#include <QGridLayout>
|
||||||
|
#include <QGroupBox>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QLineEdit>
|
||||||
|
#include <QMenu>
|
||||||
|
#include <QPlainTextEdit>
|
||||||
|
#include <QPushButton>
|
||||||
|
#include <QScrollBar>
|
||||||
|
#include <QShortcut>
|
||||||
|
#include <QStyle>
|
||||||
|
#include <QTabWidget>
|
||||||
|
#include <QTextBlock>
|
||||||
|
#include <QTextEdit>
|
||||||
|
#include <QToolBar>
|
||||||
|
#include <QToolButton>
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fmt/format.h>
|
||||||
|
|
||||||
|
#include "Common/Assert.h"
|
||||||
|
#include "Common/FileUtil.h"
|
||||||
|
|
||||||
|
#include "Core/Core.h"
|
||||||
|
#include "Core/PowerPC/MMU.h"
|
||||||
|
#include "Core/PowerPC/PowerPC.h"
|
||||||
|
#include "Core/System.h"
|
||||||
|
|
||||||
|
#include "DolphinQt/Debugger/AssemblyEditor.h"
|
||||||
|
#include "DolphinQt/QtUtils/DolphinFileDialog.h"
|
||||||
|
#include "DolphinQt/QtUtils/ModalMessageBox.h"
|
||||||
|
#include "DolphinQt/Resources.h"
|
||||||
|
#include "DolphinQt/Settings.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
using namespace Common::GekkoAssembler;
|
||||||
|
|
||||||
|
QString HtmlFormatErrorLoc(const AssemblerError& err)
|
||||||
|
{
|
||||||
|
return QObject::tr("<span style=\"color: red; font-weight: bold\">Error</span> on line %1 col %2")
|
||||||
|
.arg(err.line + 1)
|
||||||
|
.arg(err.col + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString HtmlFormatErrorLine(const AssemblerError& err)
|
||||||
|
{
|
||||||
|
const QString line_pre_error =
|
||||||
|
QString::fromStdString(std::string(err.error_line.substr(0, err.col))).toHtmlEscaped();
|
||||||
|
const QString line_error =
|
||||||
|
QString::fromStdString(std::string(err.error_line.substr(err.col, err.len))).toHtmlEscaped();
|
||||||
|
const QString line_post_error =
|
||||||
|
QString::fromStdString(std::string(err.error_line.substr(err.col + err.len))).toHtmlEscaped();
|
||||||
|
|
||||||
|
return QObject::tr("<span style=\"font-family:'monospace';font-size:16px\">"
|
||||||
|
"<pre>%1<u><span style=\"color:red;font-weight:bold\">%2</span></u>%3</pre>"
|
||||||
|
"</span>")
|
||||||
|
.arg(line_pre_error)
|
||||||
|
.arg(line_error)
|
||||||
|
.arg(line_post_error);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString HtmlFormatMessage(const AssemblerError& err)
|
||||||
|
{
|
||||||
|
return QObject::tr("<span>%1</span>").arg(QString::fromStdString(err.message).toHtmlEscaped());
|
||||||
|
}
|
||||||
|
|
||||||
|
void DeserializeBlock(const CodeBlock& blk, std::ostringstream& out_str, bool pad4)
|
||||||
|
{
|
||||||
|
size_t i = 0;
|
||||||
|
for (; i < blk.instructions.size(); i++)
|
||||||
|
{
|
||||||
|
out_str << fmt::format("{:02x}", blk.instructions[i]);
|
||||||
|
if (i % 8 == 7)
|
||||||
|
{
|
||||||
|
out_str << '\n';
|
||||||
|
}
|
||||||
|
else if (i % 4 == 3)
|
||||||
|
{
|
||||||
|
out_str << ' ';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (pad4)
|
||||||
|
{
|
||||||
|
bool did_pad = false;
|
||||||
|
for (; i % 4 != 0; i++)
|
||||||
|
{
|
||||||
|
out_str << "00";
|
||||||
|
did_pad = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (did_pad)
|
||||||
|
{
|
||||||
|
out_str << (i % 8 == 0 ? '\n' : ' ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (i % 8 != 7)
|
||||||
|
{
|
||||||
|
out_str << '\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DeserializeToRaw(const std::vector<CodeBlock>& blocks, std::ostringstream& out_str)
|
||||||
|
{
|
||||||
|
for (const auto& blk : blocks)
|
||||||
|
{
|
||||||
|
if (blk.instructions.empty())
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
out_str << fmt::format("# Block {:08x}\n", blk.block_address);
|
||||||
|
DeserializeBlock(blk, out_str, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DeserializeToAr(const std::vector<CodeBlock>& blocks, std::ostringstream& out_str)
|
||||||
|
{
|
||||||
|
for (const auto& blk : blocks)
|
||||||
|
{
|
||||||
|
if (blk.instructions.empty())
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t i = 0;
|
||||||
|
for (; i < blk.instructions.size() - 3; i += 4)
|
||||||
|
{
|
||||||
|
// type=NormalCode, subtype=SUB_RAM_WRITE, size=32bit
|
||||||
|
const u32 ar_addr = ((blk.block_address + i) & 0x1ffffff) | 0x04000000;
|
||||||
|
out_str << fmt::format("{:08x} {:02x}{:02x}{:02x}{:02x}\n", ar_addr, blk.instructions[i],
|
||||||
|
blk.instructions[i + 1], blk.instructions[i + 2],
|
||||||
|
blk.instructions[i + 3]);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (; i < blk.instructions.size(); i++)
|
||||||
|
{
|
||||||
|
// type=NormalCode, subtype=SUB_RAM_WRITE, size=8bit
|
||||||
|
const u32 ar_addr = ((blk.block_address + i) & 0x1ffffff);
|
||||||
|
out_str << fmt::format("{:08x} 000000{:02x}\n", ar_addr, blk.instructions[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DeserializeToGecko(const std::vector<CodeBlock>& blocks, std::ostringstream& out_str)
|
||||||
|
{
|
||||||
|
DeserializeToAr(blocks, out_str);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DeserializeToGeckoExec(const std::vector<CodeBlock>& blocks, std::ostringstream& out_str)
|
||||||
|
{
|
||||||
|
for (const auto& blk : blocks)
|
||||||
|
{
|
||||||
|
if (blk.instructions.empty())
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 nlines = 1 + static_cast<u32>((blk.instructions.size() - 1) / 8);
|
||||||
|
bool ret_on_newline = false;
|
||||||
|
if (blk.instructions.size() % 8 == 0 || blk.instructions.size() % 8 > 4)
|
||||||
|
{
|
||||||
|
// Append extra line for blr
|
||||||
|
nlines++;
|
||||||
|
ret_on_newline = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
out_str << fmt::format("c0000000 {:08x}\n", nlines);
|
||||||
|
DeserializeBlock(blk, out_str, true);
|
||||||
|
if (ret_on_newline)
|
||||||
|
{
|
||||||
|
out_str << "4e800020 00000000\n";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
out_str << "4e800020\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DeserializeToGeckoTramp(const std::vector<CodeBlock>& blocks, std::ostringstream& out_str)
|
||||||
|
{
|
||||||
|
for (const auto& blk : blocks)
|
||||||
|
{
|
||||||
|
if (blk.instructions.empty())
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const u32 inject_addr = (blk.block_address & 0x1ffffff) | 0x02000000;
|
||||||
|
u32 nlines = 1 + static_cast<u32>((blk.instructions.size() - 1) / 8);
|
||||||
|
bool padding_on_newline = false;
|
||||||
|
if (blk.instructions.size() % 8 == 0 || blk.instructions.size() % 8 > 4)
|
||||||
|
{
|
||||||
|
// Append extra line for nop+branchback
|
||||||
|
nlines++;
|
||||||
|
padding_on_newline = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
out_str << fmt::format("c{:07x} {:08x}\n", inject_addr, nlines);
|
||||||
|
DeserializeBlock(blk, out_str, true);
|
||||||
|
if (padding_on_newline)
|
||||||
|
{
|
||||||
|
out_str << "60000000 00000000\n";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
out_str << "00000000\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
AssemblerWidget::AssemblerWidget(QWidget* parent)
|
||||||
|
: QDockWidget(parent), m_system(Core::System::GetInstance()), m_unnamed_editor_count(0),
|
||||||
|
m_net_zoom_delta(0)
|
||||||
|
{
|
||||||
|
{
|
||||||
|
QPalette base_palette;
|
||||||
|
m_dark_scheme = base_palette.color(QPalette::WindowText).value() >
|
||||||
|
base_palette.color(QPalette::Window).value();
|
||||||
|
}
|
||||||
|
|
||||||
|
setWindowTitle(tr("Assembler"));
|
||||||
|
setObjectName(QStringLiteral("assemblerwidget"));
|
||||||
|
|
||||||
|
setHidden(!Settings::Instance().IsAssemblerVisible() ||
|
||||||
|
!Settings::Instance().IsDebugModeEnabled());
|
||||||
|
|
||||||
|
this->setVisible(true);
|
||||||
|
CreateWidgets();
|
||||||
|
|
||||||
|
restoreGeometry(
|
||||||
|
Settings::GetQSettings().value(QStringLiteral("assemblerwidget/geometry")).toByteArray());
|
||||||
|
setFloating(Settings::GetQSettings().value(QStringLiteral("assemblerwidget/floating")).toBool());
|
||||||
|
|
||||||
|
connect(&Settings::Instance(), &Settings::AssemblerVisibilityChanged, this,
|
||||||
|
[this](bool visible) { setHidden(!visible); });
|
||||||
|
|
||||||
|
connect(&Settings::Instance(), &Settings::DebugModeToggled, this, [this](bool enabled) {
|
||||||
|
setHidden(!enabled || !Settings::Instance().IsAssemblerVisible());
|
||||||
|
});
|
||||||
|
|
||||||
|
connect(&Settings::Instance(), &Settings::EmulationStateChanged, this,
|
||||||
|
&AssemblerWidget::OnEmulationStateChanged);
|
||||||
|
connect(&Settings::Instance(), &Settings::ThemeChanged, this, &AssemblerWidget::UpdateIcons);
|
||||||
|
connect(m_asm_tabs, &QTabWidget::tabCloseRequested, this, &AssemblerWidget::OnTabClose);
|
||||||
|
|
||||||
|
auto* save_shortcut = new QShortcut(QKeySequence::Save, this);
|
||||||
|
// Save should only activate if the active tab is in focus
|
||||||
|
save_shortcut->connect(save_shortcut, &QShortcut::activated, this, [this] {
|
||||||
|
if (m_asm_tabs->currentIndex() != -1 && m_asm_tabs->currentWidget()->hasFocus())
|
||||||
|
{
|
||||||
|
OnSave();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
auto* zoom_in_shortcut = new QShortcut(QKeySequence::ZoomIn, this);
|
||||||
|
zoom_in_shortcut->setContext(Qt::WidgetWithChildrenShortcut);
|
||||||
|
connect(zoom_in_shortcut, &QShortcut::activated, this, &AssemblerWidget::OnZoomIn);
|
||||||
|
auto* zoom_out_shortcut = new QShortcut(QKeySequence::ZoomOut, this);
|
||||||
|
zoom_out_shortcut->setContext(Qt::WidgetWithChildrenShortcut);
|
||||||
|
connect(zoom_out_shortcut, &QShortcut::activated, this, &AssemblerWidget::OnZoomOut);
|
||||||
|
|
||||||
|
auto* zoom_in_alternate = new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_Equal), this);
|
||||||
|
zoom_in_alternate->setContext(Qt::WidgetWithChildrenShortcut);
|
||||||
|
connect(zoom_in_alternate, &QShortcut::activated, this, &AssemblerWidget::OnZoomIn);
|
||||||
|
auto* zoom_out_alternate = new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_Underscore), this);
|
||||||
|
zoom_out_alternate->setContext(Qt::WidgetWithChildrenShortcut);
|
||||||
|
connect(zoom_out_alternate, &QShortcut::activated, this, &AssemblerWidget::OnZoomOut);
|
||||||
|
|
||||||
|
auto* zoom_reset = new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_0), this);
|
||||||
|
zoom_reset->setContext(Qt::WidgetWithChildrenShortcut);
|
||||||
|
connect(zoom_reset, &QShortcut::activated, this, &AssemblerWidget::OnZoomReset);
|
||||||
|
|
||||||
|
ConnectWidgets();
|
||||||
|
UpdateIcons();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AssemblerWidget::closeEvent(QCloseEvent*)
|
||||||
|
{
|
||||||
|
Settings::Instance().SetAssemblerVisible(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AssemblerWidget::ApplicationCloseRequest()
|
||||||
|
{
|
||||||
|
int num_unsaved = 0;
|
||||||
|
for (int i = 0; i < m_asm_tabs->count(); i++)
|
||||||
|
{
|
||||||
|
if (GetEditor(i)->IsDirty())
|
||||||
|
{
|
||||||
|
num_unsaved++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (num_unsaved > 0)
|
||||||
|
{
|
||||||
|
const int result = ModalMessageBox::question(
|
||||||
|
this, tr("Unsaved Changes"),
|
||||||
|
tr("You have %1 unsaved assembly tabs open\n\n"
|
||||||
|
"Do you want to save all and exit?")
|
||||||
|
.arg(num_unsaved),
|
||||||
|
QMessageBox::YesToAll | QMessageBox::NoToAll | QMessageBox::Cancel, QMessageBox::Cancel);
|
||||||
|
switch (result)
|
||||||
|
{
|
||||||
|
case QMessageBox::YesToAll:
|
||||||
|
for (int i = 0; i < m_asm_tabs->count(); i++)
|
||||||
|
{
|
||||||
|
AsmEditor* editor = GetEditor(i);
|
||||||
|
if (editor->IsDirty())
|
||||||
|
{
|
||||||
|
if (!SaveEditor(editor))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
case QMessageBox::NoToAll:
|
||||||
|
return true;
|
||||||
|
case QMessageBox::Cancel:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
AssemblerWidget::~AssemblerWidget()
|
||||||
|
{
|
||||||
|
auto& settings = Settings::GetQSettings();
|
||||||
|
|
||||||
|
settings.setValue(QStringLiteral("assemblerwidget/geometry"), saveGeometry());
|
||||||
|
settings.setValue(QStringLiteral("assemblerwidget/floating"), isFloating());
|
||||||
|
}
|
||||||
|
|
||||||
|
void AssemblerWidget::CreateWidgets()
|
||||||
|
{
|
||||||
|
m_asm_tabs = new QTabWidget;
|
||||||
|
m_toolbar = new QToolBar;
|
||||||
|
m_output_type = new QComboBox;
|
||||||
|
m_output_box = new QPlainTextEdit;
|
||||||
|
m_error_box = new QTextEdit;
|
||||||
|
m_address_line = new QLineEdit;
|
||||||
|
m_copy_output_button = new QPushButton;
|
||||||
|
|
||||||
|
m_asm_tabs->setTabsClosable(true);
|
||||||
|
|
||||||
|
// Initialize toolbar and actions
|
||||||
|
// m_toolbar->setIconSize(QSize(32, 32));
|
||||||
|
m_toolbar->setContentsMargins(0, 0, 0, 0);
|
||||||
|
m_toolbar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
|
||||||
|
|
||||||
|
m_open = m_toolbar->addAction(tr("Open"), this, &AssemblerWidget::OnOpen);
|
||||||
|
m_new = m_toolbar->addAction(tr("New"), this, &AssemblerWidget::OnNew);
|
||||||
|
m_assemble = m_toolbar->addAction(tr("Assemble"), this, [this] {
|
||||||
|
std::vector<CodeBlock> unused;
|
||||||
|
OnAssemble(&unused);
|
||||||
|
});
|
||||||
|
m_inject = m_toolbar->addAction(tr("Inject"), this, &AssemblerWidget::OnInject);
|
||||||
|
m_save = m_toolbar->addAction(tr("Save"), this, &AssemblerWidget::OnSave);
|
||||||
|
|
||||||
|
m_inject->setEnabled(false);
|
||||||
|
m_save->setEnabled(false);
|
||||||
|
m_assemble->setEnabled(false);
|
||||||
|
|
||||||
|
// Initialize input, output, error text areas
|
||||||
|
auto palette = m_output_box->palette();
|
||||||
|
if (m_dark_scheme)
|
||||||
|
{
|
||||||
|
palette.setColor(QPalette::Base, QColor::fromRgb(76, 76, 76));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
palette.setColor(QPalette::Base, QColor::fromRgb(180, 180, 180));
|
||||||
|
}
|
||||||
|
m_output_box->setPalette(palette);
|
||||||
|
m_error_box->setPalette(palette);
|
||||||
|
|
||||||
|
QFont mono_font(QFontDatabase::systemFont(QFontDatabase::FixedFont).family());
|
||||||
|
QFont error_font(QFontDatabase::systemFont(QFontDatabase::GeneralFont).family());
|
||||||
|
mono_font.setPointSize(12);
|
||||||
|
error_font.setPointSize(12);
|
||||||
|
QFontMetrics mono_metrics(mono_font);
|
||||||
|
QFontMetrics err_metrics(mono_font);
|
||||||
|
|
||||||
|
m_output_box->setFont(mono_font);
|
||||||
|
m_error_box->setFont(error_font);
|
||||||
|
m_output_box->setReadOnly(true);
|
||||||
|
m_error_box->setReadOnly(true);
|
||||||
|
|
||||||
|
const int output_area_width = mono_metrics.horizontalAdvance(QLatin1Char('0')) * OUTPUT_BOX_WIDTH;
|
||||||
|
m_error_box->setVerticalScrollBarPolicy(Qt::ScrollBarPolicy::ScrollBarAlwaysOff);
|
||||||
|
m_error_box->setFixedHeight(err_metrics.height() * 3 + mono_metrics.height());
|
||||||
|
m_output_box->setFixedWidth(output_area_width);
|
||||||
|
m_error_box->setVerticalScrollBarPolicy(Qt::ScrollBarPolicy::ScrollBarAlwaysOff);
|
||||||
|
|
||||||
|
// Initialize output format selection box
|
||||||
|
m_output_type->addItem(tr("Raw"));
|
||||||
|
m_output_type->addItem(tr("AR Code"));
|
||||||
|
m_output_type->addItem(tr("Gecko (04)"));
|
||||||
|
m_output_type->addItem(tr("Gecko (C0)"));
|
||||||
|
m_output_type->addItem(tr("Gecko (C2)"));
|
||||||
|
|
||||||
|
// Setup layouts
|
||||||
|
auto* addr_input_layout = new QHBoxLayout;
|
||||||
|
addr_input_layout->addWidget(new QLabel(tr("Base Address")));
|
||||||
|
addr_input_layout->addWidget(m_address_line);
|
||||||
|
|
||||||
|
auto* output_extra_layout = new QHBoxLayout;
|
||||||
|
output_extra_layout->addWidget(m_output_type);
|
||||||
|
output_extra_layout->addWidget(m_copy_output_button);
|
||||||
|
|
||||||
|
QWidget* address_input_box = new QWidget();
|
||||||
|
address_input_box->setLayout(addr_input_layout);
|
||||||
|
addr_input_layout->setContentsMargins(0, 0, 0, 0);
|
||||||
|
|
||||||
|
QWidget* output_extra_box = new QWidget();
|
||||||
|
output_extra_box->setFixedWidth(output_area_width);
|
||||||
|
output_extra_box->setLayout(output_extra_layout);
|
||||||
|
output_extra_layout->setContentsMargins(0, 0, 0, 0);
|
||||||
|
|
||||||
|
auto* assembler_layout = new QGridLayout;
|
||||||
|
assembler_layout->setSpacing(0);
|
||||||
|
assembler_layout->setContentsMargins(5, 0, 5, 5);
|
||||||
|
assembler_layout->addWidget(m_toolbar, 0, 0, 1, 2);
|
||||||
|
{
|
||||||
|
auto* input_group = new QGroupBox(tr("Input"));
|
||||||
|
auto* layout = new QVBoxLayout;
|
||||||
|
input_group->setLayout(layout);
|
||||||
|
layout->addWidget(m_asm_tabs);
|
||||||
|
layout->addWidget(address_input_box);
|
||||||
|
assembler_layout->addWidget(input_group, 1, 0, 1, 1);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
auto* output_group = new QGroupBox(tr("Output"));
|
||||||
|
auto* layout = new QGridLayout;
|
||||||
|
output_group->setLayout(layout);
|
||||||
|
layout->addWidget(m_output_box, 0, 0);
|
||||||
|
layout->addWidget(output_extra_box, 1, 0);
|
||||||
|
assembler_layout->addWidget(output_group, 1, 1, 1, 1);
|
||||||
|
output_group->setSizePolicy(
|
||||||
|
QSizePolicy(QSizePolicy::Policy::Fixed, QSizePolicy::Policy::Expanding));
|
||||||
|
}
|
||||||
|
{
|
||||||
|
auto* error_group = new QGroupBox(tr("Error Log"));
|
||||||
|
auto* layout = new QHBoxLayout;
|
||||||
|
error_group->setLayout(layout);
|
||||||
|
layout->addWidget(m_error_box);
|
||||||
|
assembler_layout->addWidget(error_group, 2, 0, 1, 2);
|
||||||
|
error_group->setSizePolicy(
|
||||||
|
QSizePolicy(QSizePolicy::Policy::Expanding, QSizePolicy::Policy::Fixed));
|
||||||
|
}
|
||||||
|
|
||||||
|
QWidget* widget = new QWidget;
|
||||||
|
widget->setLayout(assembler_layout);
|
||||||
|
setWidget(widget);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AssemblerWidget::ConnectWidgets()
|
||||||
|
{
|
||||||
|
m_output_box->connect(m_output_box, &QPlainTextEdit::updateRequest, this, [this] {
|
||||||
|
if (m_output_box->verticalScrollBar()->isVisible())
|
||||||
|
{
|
||||||
|
m_output_box->setFixedWidth(m_output_box->fontMetrics().horizontalAdvance(QLatin1Char('0')) *
|
||||||
|
OUTPUT_BOX_WIDTH +
|
||||||
|
m_output_box->style()->pixelMetric(QStyle::PM_ScrollBarExtent));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
m_output_box->setFixedWidth(m_output_box->fontMetrics().horizontalAdvance(QLatin1Char('0')) *
|
||||||
|
OUTPUT_BOX_WIDTH);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
m_copy_output_button->connect(m_copy_output_button, &QPushButton::released, this,
|
||||||
|
&AssemblerWidget::OnCopyOutput);
|
||||||
|
m_address_line->connect(m_address_line, &QLineEdit::textChanged, this,
|
||||||
|
&AssemblerWidget::OnBaseAddressChanged);
|
||||||
|
m_asm_tabs->connect(m_asm_tabs, &QTabWidget::currentChanged, this, &AssemblerWidget::OnTabChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AssemblerWidget::OnAssemble(std::vector<CodeBlock>* asm_out)
|
||||||
|
{
|
||||||
|
if (m_asm_tabs->currentIndex() == -1)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
AsmEditor* active_editor = GetEditor(m_asm_tabs->currentIndex());
|
||||||
|
|
||||||
|
AsmKind kind = AsmKind::Raw;
|
||||||
|
m_error_box->clear();
|
||||||
|
m_output_box->clear();
|
||||||
|
switch (m_output_type->currentIndex())
|
||||||
|
{
|
||||||
|
case 0:
|
||||||
|
kind = AsmKind::Raw;
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
kind = AsmKind::ActionReplay;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
kind = AsmKind::Gecko;
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
kind = AsmKind::GeckoExec;
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
kind = AsmKind::GeckoTrampoline;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool good;
|
||||||
|
u32 base_address = m_address_line->text().toUInt(&good, 16);
|
||||||
|
if (!good)
|
||||||
|
{
|
||||||
|
base_address = 0;
|
||||||
|
m_error_box->append(
|
||||||
|
tr("<span style=\"color:#ffcc00\">Warning</span> invalid base address, defaulting to 0"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string contents = active_editor->toPlainText().toStdString();
|
||||||
|
auto result = Assemble(contents, base_address);
|
||||||
|
if (IsFailure(result))
|
||||||
|
{
|
||||||
|
m_error_box->clear();
|
||||||
|
asm_out->clear();
|
||||||
|
|
||||||
|
const AssemblerError& error = GetFailure(result);
|
||||||
|
m_error_box->append(HtmlFormatErrorLoc(error));
|
||||||
|
m_error_box->append(HtmlFormatErrorLine(error));
|
||||||
|
m_error_box->append(HtmlFormatMessage(error));
|
||||||
|
asm_out->clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto& blocks = GetT(result);
|
||||||
|
std::ostringstream str_contents;
|
||||||
|
switch (kind)
|
||||||
|
{
|
||||||
|
case AsmKind::Raw:
|
||||||
|
DeserializeToRaw(blocks, str_contents);
|
||||||
|
break;
|
||||||
|
case AsmKind::ActionReplay:
|
||||||
|
DeserializeToAr(blocks, str_contents);
|
||||||
|
break;
|
||||||
|
case AsmKind::Gecko:
|
||||||
|
DeserializeToGecko(blocks, str_contents);
|
||||||
|
break;
|
||||||
|
case AsmKind::GeckoExec:
|
||||||
|
DeserializeToGeckoExec(blocks, str_contents);
|
||||||
|
break;
|
||||||
|
case AsmKind::GeckoTrampoline:
|
||||||
|
DeserializeToGeckoTramp(blocks, str_contents);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_output_box->appendPlainText(QString::fromStdString(str_contents.str()));
|
||||||
|
m_output_box->moveCursor(QTextCursor::MoveOperation::Start);
|
||||||
|
m_output_box->ensureCursorVisible();
|
||||||
|
|
||||||
|
*asm_out = std::move(GetT(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
void AssemblerWidget::OnCopyOutput()
|
||||||
|
{
|
||||||
|
QApplication::clipboard()->setText(m_output_box->toPlainText());
|
||||||
|
}
|
||||||
|
|
||||||
|
void AssemblerWidget::OnOpen()
|
||||||
|
{
|
||||||
|
const std::string default_dir = File::GetUserPath(D_ASM_ROOT_IDX);
|
||||||
|
const QStringList paths = DolphinFileDialog::getOpenFileNames(
|
||||||
|
this, tr("Select a File"), QString::fromStdString(default_dir),
|
||||||
|
QStringLiteral("%1 (*.s *.S *.asm);;%2 (*)")
|
||||||
|
.arg(tr("All Assembly files"))
|
||||||
|
.arg(tr("All Files")));
|
||||||
|
if (paths.isEmpty())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<int> show_index;
|
||||||
|
for (auto path : paths)
|
||||||
|
{
|
||||||
|
show_index = std::nullopt;
|
||||||
|
for (int i = 0; i < m_asm_tabs->count(); i++)
|
||||||
|
{
|
||||||
|
AsmEditor* editor = GetEditor(i);
|
||||||
|
if (editor->PathsMatch(path))
|
||||||
|
{
|
||||||
|
show_index = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!show_index)
|
||||||
|
{
|
||||||
|
NewEditor(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (show_index)
|
||||||
|
{
|
||||||
|
m_asm_tabs->setCurrentIndex(*show_index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AssemblerWidget::OnNew()
|
||||||
|
{
|
||||||
|
NewEditor();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AssemblerWidget::OnInject()
|
||||||
|
{
|
||||||
|
Core::CPUThreadGuard guard(m_system);
|
||||||
|
|
||||||
|
std::vector<CodeBlock> asm_result;
|
||||||
|
OnAssemble(&asm_result);
|
||||||
|
for (const auto& blk : asm_result)
|
||||||
|
{
|
||||||
|
if (!PowerPC::MMU::HostIsRAMAddress(guard, blk.block_address) || blk.instructions.empty())
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_system.GetPowerPC().GetDebugInterface().SetPatch(guard, blk.block_address, blk.instructions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AssemblerWidget::OnSave()
|
||||||
|
{
|
||||||
|
if (m_asm_tabs->currentIndex() == -1)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
AsmEditor* active_editor = GetEditor(m_asm_tabs->currentIndex());
|
||||||
|
|
||||||
|
SaveEditor(active_editor);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AssemblerWidget::OnZoomIn()
|
||||||
|
{
|
||||||
|
if (m_asm_tabs->currentIndex() != -1)
|
||||||
|
{
|
||||||
|
ZoomAllEditors(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AssemblerWidget::OnZoomOut()
|
||||||
|
{
|
||||||
|
if (m_asm_tabs->currentIndex() != -1)
|
||||||
|
{
|
||||||
|
ZoomAllEditors(-2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AssemblerWidget::OnZoomReset()
|
||||||
|
{
|
||||||
|
if (m_asm_tabs->currentIndex() != -1)
|
||||||
|
{
|
||||||
|
ZoomAllEditors(-m_net_zoom_delta);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AssemblerWidget::OnBaseAddressChanged()
|
||||||
|
{
|
||||||
|
if (m_asm_tabs->currentIndex() == -1)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
AsmEditor* active_editor = GetEditor(m_asm_tabs->currentIndex());
|
||||||
|
|
||||||
|
active_editor->SetBaseAddress(m_address_line->text());
|
||||||
|
}
|
||||||
|
|
||||||
|
void AssemblerWidget::OnTabChange(int index)
|
||||||
|
{
|
||||||
|
if (index == -1)
|
||||||
|
{
|
||||||
|
m_address_line->clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
AsmEditor* active_editor = GetEditor(index);
|
||||||
|
|
||||||
|
m_address_line->setText(active_editor->BaseAddress());
|
||||||
|
}
|
||||||
|
|
||||||
|
QString AssemblerWidget::TabTextForEditor(AsmEditor* editor, bool with_dirty)
|
||||||
|
{
|
||||||
|
ASSERT(editor != nullptr);
|
||||||
|
QString dirtyFlag = QStringLiteral();
|
||||||
|
if (editor->IsDirty() && with_dirty)
|
||||||
|
{
|
||||||
|
dirtyFlag = QStringLiteral(" *");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (editor->Path().isEmpty())
|
||||||
|
{
|
||||||
|
if (editor->EditorNum() == 0)
|
||||||
|
{
|
||||||
|
return tr("New File%1").arg(dirtyFlag);
|
||||||
|
}
|
||||||
|
return tr("New File (%1)%2").arg(editor->EditorNum() + 1).arg(dirtyFlag);
|
||||||
|
}
|
||||||
|
return tr("%1%2").arg(editor->EditorTitle()).arg(dirtyFlag);
|
||||||
|
}
|
||||||
|
|
||||||
|
AsmEditor* AssemblerWidget::GetEditor(int idx)
|
||||||
|
{
|
||||||
|
return qobject_cast<AsmEditor*>(m_asm_tabs->widget(idx));
|
||||||
|
}
|
||||||
|
|
||||||
|
void AssemblerWidget::NewEditor(const QString& path)
|
||||||
|
{
|
||||||
|
AsmEditor* new_editor =
|
||||||
|
new AsmEditor(path, path.isEmpty() ? AllocateTabNum() : INVALID_EDITOR_NUM, m_dark_scheme);
|
||||||
|
if (!path.isEmpty() && !new_editor->LoadFromPath())
|
||||||
|
{
|
||||||
|
ModalMessageBox::warning(this, tr("Failed to open file"),
|
||||||
|
tr("Failed to read the contents of file\n\n"
|
||||||
|
"\"%1\"")
|
||||||
|
.arg(path));
|
||||||
|
delete new_editor;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int tab_idx = m_asm_tabs->addTab(new_editor, QStringLiteral());
|
||||||
|
new_editor->connect(new_editor, &AsmEditor::PathChanged, this, [this] {
|
||||||
|
AsmEditor* updated_tab = qobject_cast<AsmEditor*>(sender());
|
||||||
|
DisambiguateTabTitles(updated_tab);
|
||||||
|
UpdateTabText(updated_tab);
|
||||||
|
});
|
||||||
|
new_editor->connect(new_editor, &AsmEditor::DirtyChanged, this,
|
||||||
|
[this] { UpdateTabText(qobject_cast<AsmEditor*>(sender())); });
|
||||||
|
new_editor->connect(new_editor, &AsmEditor::ZoomRequested, this,
|
||||||
|
&AssemblerWidget::ZoomAllEditors);
|
||||||
|
new_editor->Zoom(m_net_zoom_delta);
|
||||||
|
|
||||||
|
DisambiguateTabTitles(new_editor);
|
||||||
|
|
||||||
|
m_asm_tabs->setTabText(tab_idx, TabTextForEditor(new_editor, true));
|
||||||
|
|
||||||
|
if (m_save && m_assemble)
|
||||||
|
{
|
||||||
|
m_save->setEnabled(true);
|
||||||
|
m_assemble->setEnabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_asm_tabs->setCurrentIndex(tab_idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AssemblerWidget::SaveEditor(AsmEditor* editor)
|
||||||
|
{
|
||||||
|
QString save_path = editor->Path();
|
||||||
|
if (save_path.isEmpty())
|
||||||
|
{
|
||||||
|
const std::string default_dir = File::GetUserPath(D_ASM_ROOT_IDX);
|
||||||
|
const QString asm_filter = QStringLiteral("%1 (*.S)").arg(tr("Assembly File"));
|
||||||
|
const QString all_filter = QStringLiteral("%2 (*)").arg(tr("All Files"));
|
||||||
|
|
||||||
|
QString selected_filter;
|
||||||
|
save_path = DolphinFileDialog::getSaveFileName(
|
||||||
|
this, tr("Save File to"), QString::fromStdString(default_dir),
|
||||||
|
QStringLiteral("%1;;%2").arg(asm_filter).arg(all_filter), &selected_filter);
|
||||||
|
|
||||||
|
if (save_path.isEmpty())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selected_filter == asm_filter &&
|
||||||
|
std::filesystem::path(save_path.toStdString()).extension().empty())
|
||||||
|
{
|
||||||
|
save_path.append(QStringLiteral(".S"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
editor->SaveFile(save_path);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AssemblerWidget::OnEmulationStateChanged(Core::State state)
|
||||||
|
{
|
||||||
|
m_inject->setEnabled(state != Core::State::Uninitialized);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AssemblerWidget::OnTabClose(int index)
|
||||||
|
{
|
||||||
|
ASSERT(index < m_asm_tabs->count());
|
||||||
|
AsmEditor* editor = GetEditor(index);
|
||||||
|
|
||||||
|
if (editor->IsDirty())
|
||||||
|
{
|
||||||
|
const int result = ModalMessageBox::question(
|
||||||
|
this, tr("Unsaved Changes"),
|
||||||
|
tr("There are unsaved changes in \"%1\".\n\n"
|
||||||
|
"Do you want to save before closing?")
|
||||||
|
.arg(TabTextForEditor(editor, false)),
|
||||||
|
QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel, QMessageBox::Cancel);
|
||||||
|
switch (result)
|
||||||
|
{
|
||||||
|
case QMessageBox::Yes:
|
||||||
|
if (editor->IsDirty())
|
||||||
|
{
|
||||||
|
if (!SaveEditor(editor))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case QMessageBox::No:
|
||||||
|
break;
|
||||||
|
case QMessageBox::Cancel:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CloseTab(index, editor);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AssemblerWidget::CloseTab(int index, AsmEditor* editor)
|
||||||
|
{
|
||||||
|
FreeTabNum(editor->EditorNum());
|
||||||
|
|
||||||
|
m_asm_tabs->removeTab(index);
|
||||||
|
editor->deleteLater();
|
||||||
|
|
||||||
|
DisambiguateTabTitles(nullptr);
|
||||||
|
|
||||||
|
if (m_asm_tabs->count() == 0 && m_save && m_assemble)
|
||||||
|
{
|
||||||
|
m_save->setEnabled(false);
|
||||||
|
m_assemble->setEnabled(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int AssemblerWidget::AllocateTabNum()
|
||||||
|
{
|
||||||
|
auto min_it = std::min_element(m_free_editor_nums.begin(), m_free_editor_nums.end());
|
||||||
|
if (min_it == m_free_editor_nums.end())
|
||||||
|
{
|
||||||
|
return m_unnamed_editor_count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int min = *min_it;
|
||||||
|
m_free_editor_nums.erase(min_it);
|
||||||
|
return min;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AssemblerWidget::FreeTabNum(int num)
|
||||||
|
{
|
||||||
|
if (num != INVALID_EDITOR_NUM)
|
||||||
|
{
|
||||||
|
m_free_editor_nums.push_back(num);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AssemblerWidget::UpdateTabText(AsmEditor* editor)
|
||||||
|
{
|
||||||
|
int tab_idx = 0;
|
||||||
|
for (; tab_idx < m_asm_tabs->count(); tab_idx++)
|
||||||
|
{
|
||||||
|
if (m_asm_tabs->widget(tab_idx) == editor)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ASSERT(tab_idx < m_asm_tabs->count());
|
||||||
|
|
||||||
|
m_asm_tabs->setTabText(tab_idx, TabTextForEditor(editor, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
void AssemblerWidget::DisambiguateTabTitles(AsmEditor* new_tab)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < m_asm_tabs->count(); i++)
|
||||||
|
{
|
||||||
|
AsmEditor* check = GetEditor(i);
|
||||||
|
if (check->IsAmbiguous())
|
||||||
|
{
|
||||||
|
// Could group all editors with matching titles in a linked list
|
||||||
|
// but tracking that nicely without dangling pointers feels messy
|
||||||
|
bool still_ambiguous = false;
|
||||||
|
for (int j = 0; j < m_asm_tabs->count(); j++)
|
||||||
|
{
|
||||||
|
AsmEditor* against = GetEditor(j);
|
||||||
|
if (j != i && check->FileName() == against->FileName())
|
||||||
|
{
|
||||||
|
if (!against->IsAmbiguous())
|
||||||
|
{
|
||||||
|
against->SetAmbiguous(true);
|
||||||
|
UpdateTabText(against);
|
||||||
|
}
|
||||||
|
still_ambiguous = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!still_ambiguous)
|
||||||
|
{
|
||||||
|
check->SetAmbiguous(false);
|
||||||
|
UpdateTabText(check);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (new_tab != nullptr)
|
||||||
|
{
|
||||||
|
bool is_ambiguous = false;
|
||||||
|
for (int i = 0; i < m_asm_tabs->count(); i++)
|
||||||
|
{
|
||||||
|
AsmEditor* against = GetEditor(i);
|
||||||
|
if (new_tab != against && against->FileName() == new_tab->FileName())
|
||||||
|
{
|
||||||
|
against->SetAmbiguous(true);
|
||||||
|
UpdateTabText(against);
|
||||||
|
is_ambiguous = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_ambiguous)
|
||||||
|
{
|
||||||
|
new_tab->SetAmbiguous(true);
|
||||||
|
UpdateTabText(new_tab);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AssemblerWidget::UpdateIcons()
|
||||||
|
{
|
||||||
|
m_new->setIcon(Resources::GetThemeIcon("assembler_new"));
|
||||||
|
m_open->setIcon(Resources::GetThemeIcon("assembler_openasm"));
|
||||||
|
m_save->setIcon(Resources::GetThemeIcon("assembler_save"));
|
||||||
|
m_assemble->setIcon(Resources::GetThemeIcon("assembler_assemble"));
|
||||||
|
m_inject->setIcon(Resources::GetThemeIcon("assembler_inject"));
|
||||||
|
m_copy_output_button->setIcon(Resources::GetThemeIcon("assembler_clipboard"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void AssemblerWidget::ZoomAllEditors(int amount)
|
||||||
|
{
|
||||||
|
if (amount != 0)
|
||||||
|
{
|
||||||
|
m_net_zoom_delta += amount;
|
||||||
|
for (int i = 0; i < m_asm_tabs->count(); i++)
|
||||||
|
{
|
||||||
|
GetEditor(i)->Zoom(amount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
100
Source/Core/DolphinQt/Debugger/AssemblerWidget.h
Normal file
100
Source/Core/DolphinQt/Debugger/AssemblerWidget.h
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
// Copyright 2023 Dolphin Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QDockWidget>
|
||||||
|
|
||||||
|
#include "Common/Assembler/GekkoAssembler.h"
|
||||||
|
#include "Core/Core.h"
|
||||||
|
|
||||||
|
class QTabWidget;
|
||||||
|
class AsmEditor;
|
||||||
|
class QAction;
|
||||||
|
class QComboBox;
|
||||||
|
class QLineEdit;
|
||||||
|
class QPlainTextEdit;
|
||||||
|
class QPushButton;
|
||||||
|
class QTextEdit;
|
||||||
|
class QToolBar;
|
||||||
|
|
||||||
|
namespace Core
|
||||||
|
{
|
||||||
|
class System;
|
||||||
|
} // namespace Core
|
||||||
|
|
||||||
|
class AssemblerWidget : public QDockWidget
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit AssemblerWidget(QWidget* parent);
|
||||||
|
|
||||||
|
bool ApplicationCloseRequest();
|
||||||
|
|
||||||
|
~AssemblerWidget();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void closeEvent(QCloseEvent*);
|
||||||
|
|
||||||
|
private:
|
||||||
|
enum class AsmKind
|
||||||
|
{
|
||||||
|
Raw,
|
||||||
|
ActionReplay,
|
||||||
|
Gecko,
|
||||||
|
GeckoExec,
|
||||||
|
GeckoTrampoline
|
||||||
|
};
|
||||||
|
static constexpr int OUTPUT_BOX_WIDTH = 18;
|
||||||
|
void CreateWidgets();
|
||||||
|
void ConnectWidgets();
|
||||||
|
|
||||||
|
void OnEditChanged();
|
||||||
|
|
||||||
|
void OnAssemble(std::vector<Common::GekkoAssembler::CodeBlock>* asm_out);
|
||||||
|
void OnCopyOutput();
|
||||||
|
void OnOpen();
|
||||||
|
void OnNew();
|
||||||
|
void OnInject();
|
||||||
|
void OnSave();
|
||||||
|
void OnZoomIn();
|
||||||
|
void OnZoomOut();
|
||||||
|
void OnZoomReset();
|
||||||
|
void OnBaseAddressChanged();
|
||||||
|
void OnTabChange(int index);
|
||||||
|
QString TabTextForEditor(AsmEditor* editor, bool with_dirty);
|
||||||
|
AsmEditor* GetEditor(int idx);
|
||||||
|
void NewEditor(const QString& path = QStringLiteral());
|
||||||
|
bool SaveEditor(AsmEditor* editor);
|
||||||
|
void OnEmulationStateChanged(Core::State state);
|
||||||
|
void OnTabClose(int index);
|
||||||
|
void CloseTab(int index, AsmEditor* editor);
|
||||||
|
int AllocateTabNum();
|
||||||
|
void FreeTabNum(int num);
|
||||||
|
void UpdateTabText(AsmEditor* editor);
|
||||||
|
void DisambiguateTabTitles(AsmEditor* editor);
|
||||||
|
void UpdateIcons();
|
||||||
|
void ZoomAllEditors(int amount);
|
||||||
|
|
||||||
|
static constexpr int INVALID_EDITOR_NUM = -1;
|
||||||
|
|
||||||
|
Core::System& m_system;
|
||||||
|
|
||||||
|
QTabWidget* m_asm_tabs;
|
||||||
|
QPlainTextEdit* m_output_box;
|
||||||
|
QComboBox* m_output_type;
|
||||||
|
QPushButton* m_copy_output_button;
|
||||||
|
QTextEdit* m_error_box;
|
||||||
|
QLineEdit* m_address_line;
|
||||||
|
QToolBar* m_toolbar;
|
||||||
|
QAction* m_open;
|
||||||
|
QAction* m_new;
|
||||||
|
QAction* m_assemble;
|
||||||
|
QAction* m_inject;
|
||||||
|
QAction* m_save;
|
||||||
|
|
||||||
|
std::list<int> m_free_editor_nums;
|
||||||
|
int m_unnamed_editor_count;
|
||||||
|
int m_net_zoom_delta;
|
||||||
|
bool m_dark_scheme;
|
||||||
|
};
|
369
Source/Core/DolphinQt/Debugger/AssemblyEditor.cpp
Normal file
369
Source/Core/DolphinQt/Debugger/AssemblyEditor.cpp
Normal file
@ -0,0 +1,369 @@
|
|||||||
|
// Copyright 2023 Dolphin Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include "DolphinQt/Debugger/AssemblyEditor.h"
|
||||||
|
|
||||||
|
#include <QFile>
|
||||||
|
#include <QPainter>
|
||||||
|
#include <QTextBlock>
|
||||||
|
#include <QToolTip>
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
|
||||||
|
#include "Common/Assembler/GekkoParser.h"
|
||||||
|
#include "Common/StringUtil.h"
|
||||||
|
#include "DolphinQt/Debugger/GekkoSyntaxHighlight.h"
|
||||||
|
|
||||||
|
QSize AsmEditor::LineNumberArea::sizeHint() const
|
||||||
|
{
|
||||||
|
return QSize(asm_editor->LineNumberAreaWidth(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AsmEditor::LineNumberArea::paintEvent(QPaintEvent* event)
|
||||||
|
{
|
||||||
|
asm_editor->LineNumberAreaPaintEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
AsmEditor::AsmEditor(const QString& path, int editor_num, bool dark_scheme, QWidget* parent)
|
||||||
|
: QPlainTextEdit(parent), m_path(path), m_base_address(QStringLiteral("0")),
|
||||||
|
m_editor_num(editor_num), m_dirty(false), m_dark_scheme(dark_scheme)
|
||||||
|
{
|
||||||
|
if (!m_path.isEmpty())
|
||||||
|
{
|
||||||
|
m_filename =
|
||||||
|
QString::fromStdString(std::filesystem::path(m_path.toStdString()).filename().string());
|
||||||
|
}
|
||||||
|
|
||||||
|
m_line_number_area = new LineNumberArea(this);
|
||||||
|
m_highlighter = new GekkoSyntaxHighlight(document(), currentCharFormat(), dark_scheme);
|
||||||
|
m_last_block = textCursor().block();
|
||||||
|
|
||||||
|
QFont mono_font(QFontDatabase::systemFont(QFontDatabase::FixedFont).family());
|
||||||
|
mono_font.setPointSize(12);
|
||||||
|
setFont(mono_font);
|
||||||
|
m_line_number_area->setFont(mono_font);
|
||||||
|
|
||||||
|
UpdateLineNumberAreaWidth(0);
|
||||||
|
HighlightCurrentLine();
|
||||||
|
setMouseTracking(true);
|
||||||
|
|
||||||
|
connect(this, &AsmEditor::blockCountChanged, this, &AsmEditor::UpdateLineNumberAreaWidth);
|
||||||
|
connect(this, &AsmEditor::updateRequest, this, &AsmEditor::UpdateLineNumberArea);
|
||||||
|
connect(this, &AsmEditor::cursorPositionChanged, this, &AsmEditor::HighlightCurrentLine);
|
||||||
|
connect(this, &AsmEditor::textChanged, this, [this] {
|
||||||
|
m_dirty = true;
|
||||||
|
emit DirtyChanged();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
int AsmEditor::LineNumberAreaWidth()
|
||||||
|
{
|
||||||
|
int num_digits = 1;
|
||||||
|
for (int max = qMax(1, blockCount()); max >= 10; max /= 10, ++num_digits)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
return 3 + CharWidth() * qMax(2, num_digits);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AsmEditor::SetBaseAddress(const QString& ba)
|
||||||
|
{
|
||||||
|
if (ba != m_base_address)
|
||||||
|
{
|
||||||
|
m_base_address = ba;
|
||||||
|
m_dirty = true;
|
||||||
|
emit DirtyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AsmEditor::LoadFromPath()
|
||||||
|
{
|
||||||
|
QFile file(m_path);
|
||||||
|
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string base_addr_line = file.readLine().toStdString();
|
||||||
|
std::string base_address = "";
|
||||||
|
for (size_t i = 0; i < base_addr_line.length(); i++)
|
||||||
|
{
|
||||||
|
if (std::isspace(base_addr_line[i]))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
else if (base_addr_line[i] == '#')
|
||||||
|
{
|
||||||
|
base_address = base_addr_line.substr(i + 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (base_address.empty())
|
||||||
|
{
|
||||||
|
file.seek(0);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
StringPopBackIf(&base_address, '\n');
|
||||||
|
if (base_address.empty())
|
||||||
|
{
|
||||||
|
base_address = "0";
|
||||||
|
}
|
||||||
|
m_base_address = QString::fromStdString(base_address);
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool old_block = blockSignals(true);
|
||||||
|
setPlainText(QString::fromStdString(file.readAll().toStdString()));
|
||||||
|
blockSignals(old_block);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AsmEditor::PathsMatch(const QString& path) const
|
||||||
|
{
|
||||||
|
if (m_path.isEmpty() || path.isEmpty())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return std::filesystem::path(m_path.toStdString()) == std::filesystem::path(path.toStdString());
|
||||||
|
}
|
||||||
|
|
||||||
|
void AsmEditor::Zoom(int amount)
|
||||||
|
{
|
||||||
|
if (amount > 0)
|
||||||
|
{
|
||||||
|
zoomIn(amount);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
zoomOut(-amount);
|
||||||
|
}
|
||||||
|
m_line_number_area->setFont(font());
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AsmEditor::SaveFile(const QString& save_path)
|
||||||
|
{
|
||||||
|
QFile file(save_path);
|
||||||
|
if (!file.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_path != save_path)
|
||||||
|
{
|
||||||
|
m_path = save_path;
|
||||||
|
m_filename =
|
||||||
|
QString::fromStdString(std::filesystem::path(m_path.toStdString()).filename().string());
|
||||||
|
emit PathChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.write(QStringLiteral("#%1\n").arg(m_base_address).toUtf8()) == -1)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.write(toPlainText().toUtf8()) == -1)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_dirty = false;
|
||||||
|
emit DirtyChanged();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AsmEditor::UpdateLineNumberAreaWidth(int)
|
||||||
|
{
|
||||||
|
setViewportMargins(LineNumberAreaWidth(), 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AsmEditor::UpdateLineNumberArea(const QRect& rect, int dy)
|
||||||
|
{
|
||||||
|
if (dy != 0)
|
||||||
|
{
|
||||||
|
m_line_number_area->scroll(0, dy);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
m_line_number_area->update(0, rect.y(), m_line_number_area->width(), rect.height());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rect.contains(viewport()->rect()))
|
||||||
|
{
|
||||||
|
UpdateLineNumberAreaWidth(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int AsmEditor::CharWidth() const
|
||||||
|
{
|
||||||
|
return fontMetrics().horizontalAdvance(QLatin1Char(' '));
|
||||||
|
}
|
||||||
|
|
||||||
|
void AsmEditor::resizeEvent(QResizeEvent* e)
|
||||||
|
{
|
||||||
|
QPlainTextEdit::resizeEvent(e);
|
||||||
|
|
||||||
|
const QRect cr = contentsRect();
|
||||||
|
m_line_number_area->setGeometry(QRect(cr.left(), cr.top(), LineNumberAreaWidth(), cr.height()));
|
||||||
|
}
|
||||||
|
|
||||||
|
void AsmEditor::paintEvent(QPaintEvent* event)
|
||||||
|
{
|
||||||
|
QPlainTextEdit::paintEvent(event);
|
||||||
|
|
||||||
|
QPainter painter(viewport());
|
||||||
|
QTextCursor tc(document());
|
||||||
|
|
||||||
|
QPen p = QPen(Qt::red);
|
||||||
|
p.setStyle(Qt::PenStyle::SolidLine);
|
||||||
|
p.setWidth(1);
|
||||||
|
painter.setPen(p);
|
||||||
|
const int width = CharWidth();
|
||||||
|
|
||||||
|
for (QTextBlock blk = firstVisibleBlock(); blk.isVisible() && blk.isValid(); blk = blk.next())
|
||||||
|
{
|
||||||
|
if (blk.userData() == nullptr)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
BlockInfo* info = static_cast<BlockInfo*>(blk.userData());
|
||||||
|
if (info->error_at_eol)
|
||||||
|
{
|
||||||
|
tc.setPosition(blk.position() + blk.length() - 1);
|
||||||
|
tc.clearSelection();
|
||||||
|
const QRect qr = cursorRect(tc);
|
||||||
|
painter.drawLine(qr.x(), qr.y() + qr.height(), qr.x() + width, qr.y() + qr.height());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AsmEditor::event(QEvent* e)
|
||||||
|
{
|
||||||
|
if (e->type() == QEvent::ToolTip)
|
||||||
|
{
|
||||||
|
QHelpEvent* he = static_cast<QHelpEvent*>(e);
|
||||||
|
QTextCursor hover_cursor = cursorForPosition(he->pos());
|
||||||
|
QTextBlock hover_block = hover_cursor.block();
|
||||||
|
|
||||||
|
BlockInfo* info = static_cast<BlockInfo*>(hover_block.userData());
|
||||||
|
if (info == nullptr || !info->error)
|
||||||
|
{
|
||||||
|
QToolTip::hideText();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
QRect check_rect;
|
||||||
|
if (info->error_at_eol)
|
||||||
|
{
|
||||||
|
hover_cursor.setPosition(hover_block.position() +
|
||||||
|
static_cast<int>(info->error->col + info->error->len));
|
||||||
|
const QRect cursor_left = cursorRect(hover_cursor);
|
||||||
|
const int area_width = CharWidth();
|
||||||
|
check_rect = QRect(cursor_left.x() + LineNumberAreaWidth(), cursor_left.y(),
|
||||||
|
cursor_left.x() + area_width, cursor_left.height());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
hover_cursor.setPosition(hover_block.position() + static_cast<int>(info->error->col));
|
||||||
|
const QRect cursor_left = cursorRect(hover_cursor);
|
||||||
|
hover_cursor.setPosition(hover_block.position() +
|
||||||
|
static_cast<int>(info->error->col + info->error->len));
|
||||||
|
const QRect cursor_right = cursorRect(hover_cursor);
|
||||||
|
check_rect = QRect(cursor_left.x() + LineNumberAreaWidth(), cursor_left.y(),
|
||||||
|
cursor_right.x() - cursor_left.x(), cursor_left.height());
|
||||||
|
}
|
||||||
|
if (check_rect.contains(he->pos()))
|
||||||
|
{
|
||||||
|
QToolTip::showText(he->globalPos(), QString::fromStdString(info->error->message));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
QToolTip::hideText();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return QPlainTextEdit::event(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AsmEditor::keyPressEvent(QKeyEvent* event)
|
||||||
|
{
|
||||||
|
// HACK: Change shift+enter to enter to keep lines as blocks
|
||||||
|
if (event->modifiers() & Qt::ShiftModifier &&
|
||||||
|
(event->key() == Qt::Key_Enter || event->key() == Qt::Key_Return))
|
||||||
|
{
|
||||||
|
event->setModifiers(event->modifiers() & ~Qt::ShiftModifier);
|
||||||
|
}
|
||||||
|
QPlainTextEdit::keyPressEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AsmEditor::wheelEvent(QWheelEvent* event)
|
||||||
|
{
|
||||||
|
QPlainTextEdit::wheelEvent(event);
|
||||||
|
|
||||||
|
if (event->modifiers() & Qt::ControlModifier)
|
||||||
|
{
|
||||||
|
auto delta = static_cast<int>(std::round((event->angleDelta().y() / 120.0)));
|
||||||
|
if (delta != 0)
|
||||||
|
{
|
||||||
|
emit ZoomRequested(delta);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AsmEditor::HighlightCurrentLine()
|
||||||
|
{
|
||||||
|
const bool old_state = blockSignals(true);
|
||||||
|
|
||||||
|
if (m_last_block.blockNumber() != textCursor().blockNumber())
|
||||||
|
{
|
||||||
|
m_highlighter->SetMode(2);
|
||||||
|
m_highlighter->rehighlightBlock(m_last_block);
|
||||||
|
|
||||||
|
m_last_block = textCursor().block();
|
||||||
|
}
|
||||||
|
|
||||||
|
m_highlighter->SetCursorLoc(textCursor().positionInBlock());
|
||||||
|
m_highlighter->SetMode(1);
|
||||||
|
m_highlighter->rehighlightBlock(textCursor().block());
|
||||||
|
m_highlighter->SetMode(0);
|
||||||
|
|
||||||
|
blockSignals(old_state);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AsmEditor::LineNumberAreaPaintEvent(QPaintEvent* event)
|
||||||
|
{
|
||||||
|
QPainter painter(m_line_number_area);
|
||||||
|
if (m_dark_scheme)
|
||||||
|
{
|
||||||
|
painter.fillRect(event->rect(), QColor::fromRgb(76, 76, 76));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
painter.fillRect(event->rect(), QColor::fromRgb(180, 180, 180));
|
||||||
|
}
|
||||||
|
|
||||||
|
QTextBlock block = firstVisibleBlock();
|
||||||
|
int block_num = block.blockNumber();
|
||||||
|
int top = qRound(blockBoundingGeometry(block).translated(contentOffset()).top());
|
||||||
|
int bottom = top + qRound(blockBoundingRect(block).height());
|
||||||
|
|
||||||
|
while (block.isValid() && top <= event->rect().bottom())
|
||||||
|
{
|
||||||
|
if (block.isVisible() && bottom >= event->rect().top())
|
||||||
|
{
|
||||||
|
const QString num = QString::number(block_num + 1);
|
||||||
|
painter.drawText(0, top, m_line_number_area->width(), fontMetrics().height(), Qt::AlignRight,
|
||||||
|
num);
|
||||||
|
}
|
||||||
|
|
||||||
|
block = block.next();
|
||||||
|
top = bottom;
|
||||||
|
bottom = top + qRound(blockBoundingRect(block).height());
|
||||||
|
++block_num;
|
||||||
|
}
|
||||||
|
}
|
81
Source/Core/DolphinQt/Debugger/AssemblyEditor.h
Normal file
81
Source/Core/DolphinQt/Debugger/AssemblyEditor.h
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
// Copyright 2023 Dolphin Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QPlainTextEdit>
|
||||||
|
#include <QTextBlock>
|
||||||
|
|
||||||
|
class QWidget;
|
||||||
|
class QPaintEvent;
|
||||||
|
class QResizeEvent;
|
||||||
|
class QRect;
|
||||||
|
class QWheelEvent;
|
||||||
|
class GekkoSyntaxHighlight;
|
||||||
|
|
||||||
|
class AsmEditor : public QPlainTextEdit
|
||||||
|
{
|
||||||
|
Q_OBJECT;
|
||||||
|
|
||||||
|
public:
|
||||||
|
AsmEditor(const QString& file_path, int editor_num, bool dark_scheme, QWidget* parent = nullptr);
|
||||||
|
void LineNumberAreaPaintEvent(QPaintEvent* event);
|
||||||
|
int LineNumberAreaWidth();
|
||||||
|
const QString& Path() const { return m_path; }
|
||||||
|
const QString& FileName() const { return m_filename; }
|
||||||
|
const QString& EditorTitle() const { return m_title_ambiguous ? Path() : FileName(); }
|
||||||
|
const QString& BaseAddress() const { return m_base_address; }
|
||||||
|
void SetBaseAddress(const QString& ba);
|
||||||
|
void SetAmbiguous(bool b) { m_title_ambiguous = b; }
|
||||||
|
int EditorNum() const { return m_editor_num; }
|
||||||
|
bool LoadFromPath();
|
||||||
|
bool IsDirty() const { return m_dirty; }
|
||||||
|
bool IsAmbiguous() const { return m_title_ambiguous; }
|
||||||
|
bool PathsMatch(const QString& path) const;
|
||||||
|
void Zoom(int amount);
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
bool SaveFile(const QString& save_path);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void PathChanged();
|
||||||
|
void DirtyChanged();
|
||||||
|
void ZoomRequested(int amount);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void resizeEvent(QResizeEvent* event) override;
|
||||||
|
void paintEvent(QPaintEvent* event) override;
|
||||||
|
bool event(QEvent* e) override;
|
||||||
|
void keyPressEvent(QKeyEvent* event) override;
|
||||||
|
void wheelEvent(QWheelEvent* event) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void UpdateLineNumberAreaWidth(int new_block_count);
|
||||||
|
void HighlightCurrentLine();
|
||||||
|
void UpdateLineNumberArea(const QRect& rect, int dy);
|
||||||
|
int CharWidth() const;
|
||||||
|
|
||||||
|
class LineNumberArea : public QWidget
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
LineNumberArea(AsmEditor* editor) : QWidget(editor), asm_editor(editor) {}
|
||||||
|
QSize sizeHint() const override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void paintEvent(QPaintEvent* event) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
AsmEditor* asm_editor;
|
||||||
|
};
|
||||||
|
|
||||||
|
QWidget* m_line_number_area;
|
||||||
|
GekkoSyntaxHighlight* m_highlighter;
|
||||||
|
QString m_path;
|
||||||
|
QString m_filename;
|
||||||
|
QString m_base_address;
|
||||||
|
const int m_editor_num;
|
||||||
|
bool m_dirty;
|
||||||
|
QTextBlock m_last_block;
|
||||||
|
bool m_title_ambiguous;
|
||||||
|
bool m_dark_scheme;
|
||||||
|
};
|
@ -35,6 +35,7 @@
|
|||||||
#include "Core/PowerPC/PPCSymbolDB.h"
|
#include "Core/PowerPC/PPCSymbolDB.h"
|
||||||
#include "Core/PowerPC/PowerPC.h"
|
#include "Core/PowerPC/PowerPC.h"
|
||||||
#include "Core/System.h"
|
#include "Core/System.h"
|
||||||
|
#include "DolphinQt/Debugger/AssembleInstructionDialog.h"
|
||||||
#include "DolphinQt/Debugger/PatchInstructionDialog.h"
|
#include "DolphinQt/Debugger/PatchInstructionDialog.h"
|
||||||
#include "DolphinQt/Host.h"
|
#include "DolphinQt/Host.h"
|
||||||
#include "DolphinQt/QtUtils/SetWindowDecorations.h"
|
#include "DolphinQt/QtUtils/SetWindowDecorations.h"
|
||||||
@ -597,6 +598,8 @@ void CodeViewWidget::OnContextMenu()
|
|||||||
auto* insert_nop_action = menu->addAction(tr("Insert &nop"), this, &CodeViewWidget::OnInsertNOP);
|
auto* insert_nop_action = menu->addAction(tr("Insert &nop"), this, &CodeViewWidget::OnInsertNOP);
|
||||||
auto* replace_action =
|
auto* replace_action =
|
||||||
menu->addAction(tr("Re&place instruction"), this, &CodeViewWidget::OnReplaceInstruction);
|
menu->addAction(tr("Re&place instruction"), this, &CodeViewWidget::OnReplaceInstruction);
|
||||||
|
auto* assemble_action =
|
||||||
|
menu->addAction(tr("Assemble instruction"), this, &CodeViewWidget::OnAssembleInstruction);
|
||||||
auto* restore_action =
|
auto* restore_action =
|
||||||
menu->addAction(tr("Restore instruction"), this, &CodeViewWidget::OnRestoreInstruction);
|
menu->addAction(tr("Restore instruction"), this, &CodeViewWidget::OnRestoreInstruction);
|
||||||
|
|
||||||
@ -637,8 +640,9 @@ void CodeViewWidget::OnContextMenu()
|
|||||||
run_until_menu->setEnabled(!target.isEmpty());
|
run_until_menu->setEnabled(!target.isEmpty());
|
||||||
follow_branch_action->setEnabled(follow_branch_enabled);
|
follow_branch_action->setEnabled(follow_branch_enabled);
|
||||||
|
|
||||||
for (auto* action : {copy_address_action, copy_line_action, copy_hex_action, function_action,
|
for (auto* action :
|
||||||
ppc_action, insert_blr_action, insert_nop_action, replace_action})
|
{copy_address_action, copy_line_action, copy_hex_action, function_action, ppc_action,
|
||||||
|
insert_blr_action, insert_nop_action, replace_action, assemble_action})
|
||||||
{
|
{
|
||||||
action->setEnabled(running);
|
action->setEnabled(running);
|
||||||
}
|
}
|
||||||
@ -997,8 +1001,17 @@ void CodeViewWidget::OnSetSymbolEndAddress()
|
|||||||
|
|
||||||
void CodeViewWidget::OnReplaceInstruction()
|
void CodeViewWidget::OnReplaceInstruction()
|
||||||
{
|
{
|
||||||
Core::CPUThreadGuard guard(m_system);
|
DoPatchInstruction(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CodeViewWidget::OnAssembleInstruction()
|
||||||
|
{
|
||||||
|
DoPatchInstruction(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CodeViewWidget::DoPatchInstruction(bool assemble)
|
||||||
|
{
|
||||||
|
Core::CPUThreadGuard guard(m_system);
|
||||||
const u32 addr = GetContextAddress();
|
const u32 addr = GetContextAddress();
|
||||||
|
|
||||||
if (!PowerPC::MMU::HostIsInstructionRAMAddress(guard, addr))
|
if (!PowerPC::MMU::HostIsInstructionRAMAddress(guard, addr))
|
||||||
@ -1010,8 +1023,10 @@ void CodeViewWidget::OnReplaceInstruction()
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
auto& debug_interface = m_system.GetPowerPC().GetDebugInterface();
|
auto& debug_interface = m_system.GetPowerPC().GetDebugInterface();
|
||||||
PatchInstructionDialog dialog(this, addr, debug_interface.ReadInstruction(guard, addr));
|
|
||||||
|
|
||||||
|
if (assemble)
|
||||||
|
{
|
||||||
|
AssembleInstructionDialog dialog(this, addr, debug_interface.ReadInstruction(guard, addr));
|
||||||
SetQWidgetWindowDecorations(&dialog);
|
SetQWidgetWindowDecorations(&dialog);
|
||||||
if (dialog.exec() == QDialog::Accepted)
|
if (dialog.exec() == QDialog::Accepted)
|
||||||
{
|
{
|
||||||
@ -1019,6 +1034,17 @@ void CodeViewWidget::OnReplaceInstruction()
|
|||||||
Update(&guard);
|
Update(&guard);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
PatchInstructionDialog dialog(this, addr, debug_interface.ReadInstruction(guard, addr));
|
||||||
|
SetQWidgetWindowDecorations(&dialog);
|
||||||
|
if (dialog.exec() == QDialog::Accepted)
|
||||||
|
{
|
||||||
|
debug_interface.SetPatch(guard, addr, dialog.GetCode());
|
||||||
|
Update(&guard);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void CodeViewWidget::OnRestoreInstruction()
|
void CodeViewWidget::OnRestoreInstruction()
|
||||||
{
|
{
|
||||||
|
@ -95,6 +95,8 @@ private:
|
|||||||
void OnInsertBLR();
|
void OnInsertBLR();
|
||||||
void OnInsertNOP();
|
void OnInsertNOP();
|
||||||
void OnReplaceInstruction();
|
void OnReplaceInstruction();
|
||||||
|
void OnAssembleInstruction();
|
||||||
|
void DoPatchInstruction(bool assemble);
|
||||||
void OnRestoreInstruction();
|
void OnRestoreInstruction();
|
||||||
|
|
||||||
void CalculateBranchIndentation();
|
void CalculateBranchIndentation();
|
||||||
|
261
Source/Core/DolphinQt/Debugger/GekkoSyntaxHighlight.cpp
Normal file
261
Source/Core/DolphinQt/Debugger/GekkoSyntaxHighlight.cpp
Normal file
@ -0,0 +1,261 @@
|
|||||||
|
// Copyright 2023 Dolphin Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include "DolphinQt/Debugger/GekkoSyntaxHighlight.h"
|
||||||
|
|
||||||
|
#include "Common/Assembler/GekkoParser.h"
|
||||||
|
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QPalette>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
using namespace Common::GekkoAssembler;
|
||||||
|
using namespace Common::GekkoAssembler::detail;
|
||||||
|
|
||||||
|
class HighlightParsePlugin : public ParsePlugin
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual ~HighlightParsePlugin() = default;
|
||||||
|
|
||||||
|
std::vector<std::pair<int, int>>&& MoveParens() { return std::move(m_matched_parens); }
|
||||||
|
std::vector<std::tuple<int, int, HighlightFormat>>&& MoveFormatting()
|
||||||
|
{
|
||||||
|
return std::move(m_formatting);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnDirectivePre(GekkoDirective) override { HighlightCurToken(HighlightFormat::Directive); }
|
||||||
|
|
||||||
|
void OnInstructionPre(const ParseInfo&, bool) override
|
||||||
|
{
|
||||||
|
HighlightCurToken(HighlightFormat::Mnemonic);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnTerminal(Terminal type, const AssemblerToken& val) override
|
||||||
|
{
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case Terminal::Id:
|
||||||
|
HighlightCurToken(HighlightFormat::Symbol);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Terminal::Hex:
|
||||||
|
case Terminal::Dec:
|
||||||
|
case Terminal::Oct:
|
||||||
|
case Terminal::Bin:
|
||||||
|
case Terminal::Flt:
|
||||||
|
HighlightCurToken(HighlightFormat::Immediate);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Terminal::GPR:
|
||||||
|
HighlightCurToken(HighlightFormat::GPR);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Terminal::FPR:
|
||||||
|
HighlightCurToken(HighlightFormat::GPR);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Terminal::SPR:
|
||||||
|
HighlightCurToken(HighlightFormat::SPR);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Terminal::CRField:
|
||||||
|
HighlightCurToken(HighlightFormat::CRField);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Terminal::Lt:
|
||||||
|
case Terminal::Gt:
|
||||||
|
case Terminal::Eq:
|
||||||
|
case Terminal::So:
|
||||||
|
HighlightCurToken(HighlightFormat::CRFlag);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Terminal::Str:
|
||||||
|
HighlightCurToken(HighlightFormat::Str);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnHiaddr(std::string_view) override
|
||||||
|
{
|
||||||
|
HighlightCurToken(HighlightFormat::Symbol);
|
||||||
|
auto&& [ha_pos, ha_tok] = m_owner->lexer.LookaheadTagRef(2);
|
||||||
|
m_formatting.emplace_back(static_cast<int>(ha_pos.col),
|
||||||
|
static_cast<int>(ha_tok.token_val.length()), HighlightFormat::HaLa);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnLoaddr(std::string_view id) override { OnHiaddr(id); }
|
||||||
|
|
||||||
|
void OnOpenParen(ParenType type) override
|
||||||
|
{
|
||||||
|
m_paren_stack.push_back(static_cast<int>(m_owner->lexer.ColNumber()));
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnCloseParen(ParenType type) override
|
||||||
|
{
|
||||||
|
if (m_paren_stack.empty())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_matched_parens.emplace_back(m_paren_stack.back(),
|
||||||
|
static_cast<int>(m_owner->lexer.ColNumber()));
|
||||||
|
m_paren_stack.pop_back();
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnError() override
|
||||||
|
{
|
||||||
|
m_formatting.emplace_back(static_cast<int>(m_owner->error->col),
|
||||||
|
static_cast<int>(m_owner->error->len), HighlightFormat::Error);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnLabelDecl(std::string_view name) override
|
||||||
|
{
|
||||||
|
const int len = static_cast<int>(m_owner->lexer.LookaheadRef().token_val.length());
|
||||||
|
const int off = static_cast<int>(m_owner->lexer.ColNumber());
|
||||||
|
m_formatting.emplace_back(len, off, HighlightFormat::Symbol);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnVarDecl(std::string_view name) override { OnLabelDecl(name); }
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::vector<int> m_paren_stack;
|
||||||
|
std::vector<std::pair<int, int>> m_matched_parens;
|
||||||
|
std::vector<std::tuple<int, int, HighlightFormat>> m_formatting;
|
||||||
|
|
||||||
|
void HighlightCurToken(HighlightFormat format)
|
||||||
|
{
|
||||||
|
const int len = static_cast<int>(m_owner->lexer.LookaheadRef().token_val.length());
|
||||||
|
const int off = static_cast<int>(m_owner->lexer.ColNumber());
|
||||||
|
m_formatting.emplace_back(off, len, format);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void GekkoSyntaxHighlight::highlightBlock(const QString& text)
|
||||||
|
{
|
||||||
|
BlockInfo* info = static_cast<BlockInfo*>(currentBlockUserData());
|
||||||
|
if (info == nullptr)
|
||||||
|
{
|
||||||
|
info = new BlockInfo;
|
||||||
|
setCurrentBlockUserData(info);
|
||||||
|
}
|
||||||
|
|
||||||
|
qsizetype comment_idx = text.indexOf(QLatin1Char('#'));
|
||||||
|
if (comment_idx != -1)
|
||||||
|
{
|
||||||
|
HighlightSubstr(comment_idx, text.length() - comment_idx, HighlightFormat::Comment);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_mode == 0)
|
||||||
|
{
|
||||||
|
HighlightParsePlugin plugin;
|
||||||
|
ParseWithPlugin(&plugin, text.toStdString());
|
||||||
|
|
||||||
|
info->block_format = plugin.MoveFormatting();
|
||||||
|
info->parens = plugin.MoveParens();
|
||||||
|
info->error = std::move(plugin.Error());
|
||||||
|
info->error_at_eol = info->error && info->error->len == 0;
|
||||||
|
}
|
||||||
|
else if (m_mode == 1)
|
||||||
|
{
|
||||||
|
auto paren_it = std::find_if(info->parens.begin(), info->parens.end(),
|
||||||
|
[this](const std::pair<int, int>& p) {
|
||||||
|
return p.first == m_cursor_loc || p.second == m_cursor_loc;
|
||||||
|
});
|
||||||
|
if (paren_it != info->parens.end())
|
||||||
|
{
|
||||||
|
HighlightSubstr(paren_it->first, 1, HighlightFormat::Paren);
|
||||||
|
HighlightSubstr(paren_it->second, 1, HighlightFormat::Paren);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto&& [off, len, format] : info->block_format)
|
||||||
|
{
|
||||||
|
HighlightSubstr(off, len, format);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GekkoSyntaxHighlight::GekkoSyntaxHighlight(QTextDocument* document, QTextCharFormat base_format,
|
||||||
|
bool dark_scheme)
|
||||||
|
: QSyntaxHighlighter(document), m_base_format(base_format)
|
||||||
|
{
|
||||||
|
QPalette base_scheme;
|
||||||
|
m_theme_idx = dark_scheme ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GekkoSyntaxHighlight::HighlightSubstr(int start, int len, HighlightFormat format)
|
||||||
|
{
|
||||||
|
QTextCharFormat hl_format = m_base_format;
|
||||||
|
const QColor DIRECTIVE_COLOR[2] = {QColor(0x9d, 0x00, 0x06),
|
||||||
|
QColor(0xfb, 0x49, 0x34)}; // Gruvbox darkred
|
||||||
|
const QColor MNEMONIC_COLOR[2] = {QColor(0x79, 0x74, 0x0e),
|
||||||
|
QColor(0xb8, 0xbb, 0x26)}; // Gruvbox darkgreen
|
||||||
|
const QColor IMM_COLOR[2] = {QColor(0xb5, 0x76, 0x14),
|
||||||
|
QColor(0xfa, 0xbd, 0x2f)}; // Gruvbox darkyellow
|
||||||
|
const QColor BUILTIN_COLOR[2] = {QColor(0x07, 0x66, 0x78),
|
||||||
|
QColor(0x83, 0xa5, 0x98)}; // Gruvbox darkblue
|
||||||
|
const QColor HA_LA_COLOR[2] = {QColor(0xaf, 0x3a, 0x03),
|
||||||
|
QColor(0xfe, 0x80, 0x19)}; // Gruvbox darkorange
|
||||||
|
const QColor HOVER_BG_COLOR[2] = {QColor(0xd5, 0xc4, 0xa1),
|
||||||
|
QColor(0x50, 0x49, 0x45)}; // Gruvbox bg2
|
||||||
|
const QColor STRING_COLOR[2] = {QColor(0x98, 0x97, 0x1a),
|
||||||
|
QColor(0x98, 0x97, 0x1a)}; // Gruvbox green
|
||||||
|
const QColor COMMENT_COLOR[2] = {QColor(0x68, 0x9d, 0x6a),
|
||||||
|
QColor(0x68, 0x9d, 0x6a)}; // Gruvbox aqua
|
||||||
|
|
||||||
|
switch (format)
|
||||||
|
{
|
||||||
|
case HighlightFormat::Directive:
|
||||||
|
hl_format.setForeground(DIRECTIVE_COLOR[m_theme_idx]);
|
||||||
|
break;
|
||||||
|
case HighlightFormat::Mnemonic:
|
||||||
|
hl_format.setForeground(MNEMONIC_COLOR[m_theme_idx]);
|
||||||
|
break;
|
||||||
|
case HighlightFormat::Symbol:
|
||||||
|
break;
|
||||||
|
case HighlightFormat::Immediate:
|
||||||
|
hl_format.setForeground(IMM_COLOR[m_theme_idx]);
|
||||||
|
break;
|
||||||
|
case HighlightFormat::GPR:
|
||||||
|
hl_format.setForeground(BUILTIN_COLOR[m_theme_idx]);
|
||||||
|
break;
|
||||||
|
case HighlightFormat::FPR:
|
||||||
|
hl_format.setForeground(BUILTIN_COLOR[m_theme_idx]);
|
||||||
|
break;
|
||||||
|
case HighlightFormat::SPR:
|
||||||
|
hl_format.setForeground(BUILTIN_COLOR[m_theme_idx]);
|
||||||
|
break;
|
||||||
|
case HighlightFormat::CRField:
|
||||||
|
hl_format.setForeground(BUILTIN_COLOR[m_theme_idx]);
|
||||||
|
break;
|
||||||
|
case HighlightFormat::CRFlag:
|
||||||
|
hl_format.setForeground(BUILTIN_COLOR[m_theme_idx]);
|
||||||
|
break;
|
||||||
|
case HighlightFormat::Str:
|
||||||
|
hl_format.setForeground(STRING_COLOR[m_theme_idx]);
|
||||||
|
break;
|
||||||
|
case HighlightFormat::HaLa:
|
||||||
|
hl_format.setForeground(HA_LA_COLOR[m_theme_idx]);
|
||||||
|
break;
|
||||||
|
case HighlightFormat::Paren:
|
||||||
|
hl_format.setBackground(HOVER_BG_COLOR[m_theme_idx]);
|
||||||
|
break;
|
||||||
|
case HighlightFormat::Default:
|
||||||
|
hl_format.clearForeground();
|
||||||
|
hl_format.clearBackground();
|
||||||
|
break;
|
||||||
|
case HighlightFormat::Comment:
|
||||||
|
hl_format.setForeground(COMMENT_COLOR[m_theme_idx]);
|
||||||
|
break;
|
||||||
|
case HighlightFormat::Error:
|
||||||
|
hl_format.setUnderlineColor(Qt::red);
|
||||||
|
hl_format.setUnderlineStyle(QTextCharFormat::WaveUnderline);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
setFormat(start, len, hl_format);
|
||||||
|
}
|
60
Source/Core/DolphinQt/Debugger/GekkoSyntaxHighlight.h
Normal file
60
Source/Core/DolphinQt/Debugger/GekkoSyntaxHighlight.h
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
// Copyright 2023 Dolphin Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QSyntaxHighlighter>
|
||||||
|
#include <QTextCharFormat>
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
#include "Common/Assembler/AssemblerShared.h"
|
||||||
|
|
||||||
|
enum class HighlightFormat
|
||||||
|
{
|
||||||
|
Directive,
|
||||||
|
Mnemonic,
|
||||||
|
Symbol,
|
||||||
|
Immediate,
|
||||||
|
GPR,
|
||||||
|
FPR,
|
||||||
|
SPR,
|
||||||
|
CRField,
|
||||||
|
CRFlag,
|
||||||
|
Str,
|
||||||
|
HaLa,
|
||||||
|
Paren,
|
||||||
|
Default,
|
||||||
|
Comment,
|
||||||
|
Error,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct BlockInfo : public QTextBlockUserData
|
||||||
|
{
|
||||||
|
std::vector<std::tuple<int, int, HighlightFormat>> block_format;
|
||||||
|
std::vector<std::pair<int, int>> parens;
|
||||||
|
std::optional<Common::GekkoAssembler::AssemblerError> error;
|
||||||
|
bool error_at_eol = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
class GekkoSyntaxHighlight : public QSyntaxHighlighter
|
||||||
|
{
|
||||||
|
Q_OBJECT;
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit GekkoSyntaxHighlight(QTextDocument* document, QTextCharFormat base_format,
|
||||||
|
bool dark_scheme);
|
||||||
|
|
||||||
|
void HighlightSubstr(int start, int len, HighlightFormat format);
|
||||||
|
void SetMode(int mode) { m_mode = mode; }
|
||||||
|
void SetCursorLoc(int loc) { m_cursor_loc = loc; }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void highlightBlock(const QString& line) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
int m_mode = 0;
|
||||||
|
int m_cursor_loc = 0;
|
||||||
|
QTextCharFormat m_base_format;
|
||||||
|
int m_theme_idx = 0;
|
||||||
|
};
|
@ -123,8 +123,10 @@ GameList::GameList(QWidget* parent) : QStackedWidget(parent), m_model(this)
|
|||||||
m_prefer_list = Settings::Instance().GetPreferredView();
|
m_prefer_list = Settings::Instance().GetPreferredView();
|
||||||
ConsiderViewChange();
|
ConsiderViewChange();
|
||||||
|
|
||||||
const auto* zoom_in = new QShortcut(QKeySequence::ZoomIn, this);
|
auto* zoom_in = new QShortcut(QKeySequence::ZoomIn, this);
|
||||||
const auto* zoom_out = new QShortcut(QKeySequence::ZoomOut, this);
|
auto* zoom_out = new QShortcut(QKeySequence::ZoomOut, this);
|
||||||
|
zoom_in->setContext(Qt::WidgetWithChildrenShortcut);
|
||||||
|
zoom_out->setContext(Qt::WidgetWithChildrenShortcut);
|
||||||
|
|
||||||
connect(zoom_in, &QShortcut::activated, this, &GameList::ZoomIn);
|
connect(zoom_in, &QShortcut::activated, this, &GameList::ZoomIn);
|
||||||
connect(zoom_out, &QShortcut::activated, this, &GameList::ZoomOut);
|
connect(zoom_out, &QShortcut::activated, this, &GameList::ZoomOut);
|
||||||
|
@ -82,6 +82,7 @@
|
|||||||
#include "DolphinQt/Config/LogWidget.h"
|
#include "DolphinQt/Config/LogWidget.h"
|
||||||
#include "DolphinQt/Config/Mapping/MappingWindow.h"
|
#include "DolphinQt/Config/Mapping/MappingWindow.h"
|
||||||
#include "DolphinQt/Config/SettingsWindow.h"
|
#include "DolphinQt/Config/SettingsWindow.h"
|
||||||
|
#include "DolphinQt/Debugger/AssemblerWidget.h"
|
||||||
#include "DolphinQt/Debugger/BreakpointWidget.h"
|
#include "DolphinQt/Debugger/BreakpointWidget.h"
|
||||||
#include "DolphinQt/Debugger/CodeViewWidget.h"
|
#include "DolphinQt/Debugger/CodeViewWidget.h"
|
||||||
#include "DolphinQt/Debugger/CodeWidget.h"
|
#include "DolphinQt/Debugger/CodeWidget.h"
|
||||||
@ -449,6 +450,7 @@ void MainWindow::CreateComponents()
|
|||||||
m_breakpoint_widget = new BreakpointWidget(this);
|
m_breakpoint_widget = new BreakpointWidget(this);
|
||||||
m_code_widget = new CodeWidget(this);
|
m_code_widget = new CodeWidget(this);
|
||||||
m_cheats_manager = new CheatsManager(this);
|
m_cheats_manager = new CheatsManager(this);
|
||||||
|
m_assembler_widget = new AssemblerWidget(this);
|
||||||
|
|
||||||
const auto request_watch = [this](QString name, u32 addr) {
|
const auto request_watch = [this](QString name, u32 addr) {
|
||||||
m_watch_widget->AddWatch(name, addr);
|
m_watch_widget->AddWatch(name, addr);
|
||||||
@ -740,6 +742,7 @@ void MainWindow::ConnectStack()
|
|||||||
addDockWidget(Qt::LeftDockWidgetArea, m_memory_widget);
|
addDockWidget(Qt::LeftDockWidgetArea, m_memory_widget);
|
||||||
addDockWidget(Qt::LeftDockWidgetArea, m_network_widget);
|
addDockWidget(Qt::LeftDockWidgetArea, m_network_widget);
|
||||||
addDockWidget(Qt::LeftDockWidgetArea, m_jit_widget);
|
addDockWidget(Qt::LeftDockWidgetArea, m_jit_widget);
|
||||||
|
addDockWidget(Qt::LeftDockWidgetArea, m_assembler_widget);
|
||||||
|
|
||||||
tabifyDockWidget(m_log_widget, m_log_config_widget);
|
tabifyDockWidget(m_log_widget, m_log_config_widget);
|
||||||
tabifyDockWidget(m_log_widget, m_code_widget);
|
tabifyDockWidget(m_log_widget, m_code_widget);
|
||||||
@ -750,6 +753,7 @@ void MainWindow::ConnectStack()
|
|||||||
tabifyDockWidget(m_log_widget, m_memory_widget);
|
tabifyDockWidget(m_log_widget, m_memory_widget);
|
||||||
tabifyDockWidget(m_log_widget, m_network_widget);
|
tabifyDockWidget(m_log_widget, m_network_widget);
|
||||||
tabifyDockWidget(m_log_widget, m_jit_widget);
|
tabifyDockWidget(m_log_widget, m_jit_widget);
|
||||||
|
tabifyDockWidget(m_log_widget, m_assembler_widget);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::RefreshGameList()
|
void MainWindow::RefreshGameList()
|
||||||
@ -872,7 +876,16 @@ void MainWindow::OnStopComplete()
|
|||||||
SetFullScreenResolution(false);
|
SetFullScreenResolution(false);
|
||||||
|
|
||||||
if (m_exit_requested || Settings::Instance().IsBatchModeEnabled())
|
if (m_exit_requested || Settings::Instance().IsBatchModeEnabled())
|
||||||
|
{
|
||||||
|
if (m_assembler_widget->ApplicationCloseRequest())
|
||||||
|
{
|
||||||
QGuiApplication::exit(0);
|
QGuiApplication::exit(0);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
m_exit_requested = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If the current emulation prevented the booting of another, do that now
|
// If the current emulation prevented the booting of another, do that now
|
||||||
if (m_pending_boot != nullptr)
|
if (m_pending_boot != nullptr)
|
||||||
|
@ -18,6 +18,7 @@ class QStackedWidget;
|
|||||||
class QString;
|
class QString;
|
||||||
|
|
||||||
class AchievementsWindow;
|
class AchievementsWindow;
|
||||||
|
class AssemblerWidget;
|
||||||
class BreakpointWidget;
|
class BreakpointWidget;
|
||||||
struct BootParameters;
|
struct BootParameters;
|
||||||
class CheatsManager;
|
class CheatsManager;
|
||||||
@ -259,6 +260,7 @@ private:
|
|||||||
AchievementsWindow* m_achievements_window = nullptr;
|
AchievementsWindow* m_achievements_window = nullptr;
|
||||||
#endif // USE_RETRO_ACHIEVEMENTS
|
#endif // USE_RETRO_ACHIEVEMENTS
|
||||||
|
|
||||||
|
AssemblerWidget* m_assembler_widget;
|
||||||
BreakpointWidget* m_breakpoint_widget;
|
BreakpointWidget* m_breakpoint_widget;
|
||||||
CodeWidget* m_code_widget;
|
CodeWidget* m_code_widget;
|
||||||
JITWidget* m_jit_widget;
|
JITWidget* m_jit_widget;
|
||||||
|
@ -494,6 +494,14 @@ void MenuBar::AddViewMenu()
|
|||||||
connect(m_show_jit, &QAction::toggled, &Settings::Instance(), &Settings::SetJITVisible);
|
connect(m_show_jit, &QAction::toggled, &Settings::Instance(), &Settings::SetJITVisible);
|
||||||
connect(&Settings::Instance(), &Settings::JITVisibilityChanged, m_show_jit, &QAction::setChecked);
|
connect(&Settings::Instance(), &Settings::JITVisibilityChanged, m_show_jit, &QAction::setChecked);
|
||||||
|
|
||||||
|
m_show_assembler = view_menu->addAction(tr("&Assembler"));
|
||||||
|
m_show_assembler->setCheckable(true);
|
||||||
|
m_show_assembler->setChecked(Settings::Instance().IsAssemblerVisible());
|
||||||
|
connect(m_show_assembler, &QAction::toggled, &Settings::Instance(),
|
||||||
|
&Settings::SetAssemblerVisible);
|
||||||
|
connect(&Settings::Instance(), &Settings::AssemblerVisibilityChanged, m_show_assembler,
|
||||||
|
&QAction::setChecked);
|
||||||
|
|
||||||
view_menu->addSeparator();
|
view_menu->addSeparator();
|
||||||
|
|
||||||
AddGameListTypeSection(view_menu);
|
AddGameListTypeSection(view_menu);
|
||||||
|
@ -253,6 +253,7 @@ private:
|
|||||||
QAction* m_show_memory;
|
QAction* m_show_memory;
|
||||||
QAction* m_show_network;
|
QAction* m_show_network;
|
||||||
QAction* m_show_jit;
|
QAction* m_show_jit;
|
||||||
|
QAction* m_show_assembler;
|
||||||
QMenu* m_cols_menu;
|
QMenu* m_cols_menu;
|
||||||
|
|
||||||
// JIT
|
// JIT
|
||||||
|
@ -693,6 +693,20 @@ bool Settings::IsJITVisible() const
|
|||||||
return QSettings().value(QStringLiteral("debugger/showjit")).toBool();
|
return QSettings().value(QStringLiteral("debugger/showjit")).toBool();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Settings::SetAssemblerVisible(bool enabled)
|
||||||
|
{
|
||||||
|
if (IsAssemblerVisible() == enabled)
|
||||||
|
return;
|
||||||
|
QSettings().setValue(QStringLiteral("debugger/showassembler"), enabled);
|
||||||
|
|
||||||
|
emit AssemblerVisibilityChanged(enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Settings::IsAssemblerVisible() const
|
||||||
|
{
|
||||||
|
return QSettings().value(QStringLiteral("debugger/showassembler")).toBool();
|
||||||
|
}
|
||||||
|
|
||||||
void Settings::RefreshWidgetVisibility()
|
void Settings::RefreshWidgetVisibility()
|
||||||
{
|
{
|
||||||
emit DebugModeToggled(IsDebugModeEnabled());
|
emit DebugModeToggled(IsDebugModeEnabled());
|
||||||
|
@ -166,6 +166,8 @@ public:
|
|||||||
bool IsNetworkVisible() const;
|
bool IsNetworkVisible() const;
|
||||||
void SetJITVisible(bool enabled);
|
void SetJITVisible(bool enabled);
|
||||||
bool IsJITVisible() const;
|
bool IsJITVisible() const;
|
||||||
|
void SetAssemblerVisible(bool enabled);
|
||||||
|
bool IsAssemblerVisible() const;
|
||||||
QFont GetDebugFont() const;
|
QFont GetDebugFont() const;
|
||||||
void SetDebugFont(QFont font);
|
void SetDebugFont(QFont font);
|
||||||
|
|
||||||
@ -213,6 +215,7 @@ signals:
|
|||||||
void MemoryVisibilityChanged(bool visible);
|
void MemoryVisibilityChanged(bool visible);
|
||||||
void NetworkVisibilityChanged(bool visible);
|
void NetworkVisibilityChanged(bool visible);
|
||||||
void JITVisibilityChanged(bool visible);
|
void JITVisibilityChanged(bool visible);
|
||||||
|
void AssemblerVisibilityChanged(bool visible);
|
||||||
void DebugModeToggled(bool enabled);
|
void DebugModeToggled(bool enabled);
|
||||||
void DebugFontChanged(QFont font);
|
void DebugFontChanged(QFont font);
|
||||||
void AutoUpdateTrackChanged(const QString& mode);
|
void AutoUpdateTrackChanged(const QString& mode);
|
||||||
|
@ -266,6 +266,7 @@ void CreateDirectories()
|
|||||||
File::CreateFullPath(File::GetUserPath(D_SHADERS_IDX));
|
File::CreateFullPath(File::GetUserPath(D_SHADERS_IDX));
|
||||||
File::CreateFullPath(File::GetUserPath(D_SHADERS_IDX) + ANAGLYPH_DIR DIR_SEP);
|
File::CreateFullPath(File::GetUserPath(D_SHADERS_IDX) + ANAGLYPH_DIR DIR_SEP);
|
||||||
File::CreateFullPath(File::GetUserPath(D_STATESAVES_IDX));
|
File::CreateFullPath(File::GetUserPath(D_STATESAVES_IDX));
|
||||||
|
File::CreateFullPath(File::GetUserPath(D_ASM_ROOT_IDX));
|
||||||
#ifndef ANDROID
|
#ifndef ANDROID
|
||||||
File::CreateFullPath(File::GetUserPath(D_THEMES_IDX));
|
File::CreateFullPath(File::GetUserPath(D_THEMES_IDX));
|
||||||
File::CreateFullPath(File::GetUserPath(D_STYLES_IDX));
|
File::CreateFullPath(File::GetUserPath(D_STYLES_IDX));
|
||||||
|
Loading…
x
Reference in New Issue
Block a user