[Assembler] Initial commit for new assembler infrastructure
MODIFY
src/CMakeLists.txt
src/CMakeLists.txt
@@ -7,13 +7,15 @@ file(GLOB LIB_SOURCES
cachesim/*.cpp
editor/*.cpp
syscall/*.cpp
- version/version.cpp)
+ version/version.cpp
+ assembler/*.cpp)
file(GLOB LIB_HEADERS
*.h
cachesim/*.h
editor/*.h
syscall/*.h
- graphics/*.h)
+ graphics/*.h
+ assembler/*.h)
file(GLOB LIB_UIS
*.ui
cachesim/*.ui

ADD
src/assembler/assembler.cpp
+#include "assembler.h"
+
+#include <algorithm>
+
+#include <QRegExp>
+#include <QRegularExpression>
+#include <QString>
+
+namespace {
+/**
+ * A macro for running an assembler pass with error handling
+ */
+#define runPass(resName, resType, passFunction, ...) \
+ auto passFunction##_res = passFunction(__VA_ARGS__); \
+ try { \
+ auto& errors = std::get<Errors>(passFunction##_res); \
+ result.errors.insert(result.errors.end(), errors.begin(), errors.end()); \
+ return result; \
+ } catch (const std::bad_variant_access&) { \
+ } \
+ auto resName = std::get<resType>(passFunction##_res);
+
+/**
+ * A macro for running an ISA-specific assembler instruction with error handling
+ */
+#define runOperation(resName, resType, operationFunction, ...) \
+ auto operationFunction##_res = operationFunction(__VA_ARGS__); \
+ try { \
+ errors.push_back(std::get<Error>(operationFunction##_res)); \
+ continue; \
+ } catch (const std::bad_variant_access&) { \
+ } \
+ auto resName = std::get<resType>(operationFunction##_res);
+
+} // namespace
+
+namespace Ripes {
+
+Assembler::Result Assembler::assemble(const QString& program) const {
+ const auto programLines = program.split(QRegExp("[\r\n]"));
+ Result result;
+
+ runPass(tokenizedLines, Program, pass0, programLines);
+
+ // Pseudo SourceLine expansion
+ runPass(unused, Program, pass1, tokenizedLines);
+
+ // Symbol mapping
+ Symbols symbols;
+ runPass(unused2, NoPassResult, pass2, tokenizedLines, symbols);
+
+ // Machine code translation
+ // runPass(unused3, QByteArray, pass3, tokenizedLines, symbols);
+}
+
+std::variant<Assembler::Errors, Assembler::Program> Assembler::pass0(const QStringList& program) const {
+ Errors errors;
+ Program tokenizedLines;
+ tokenizedLines.reserve(program.size());
+ for (unsigned i = 0; i < program.size(); i++) {
+ const auto& line = program.at(i);
+ if (line.isEmpty())
+ continue;
+ runOperation(tokenizedLine, LineTokens, tokenize, program[i]);
+ tokenizedLines.push_back({tokenizedLine, i, {}});
+ }
+ if (errors.size() != 0) {
+ return {errors};
+ } else {
+ return {tokenizedLines};
+ }
+}
+
+std::variant<Assembler::Errors, Assembler::Program> Assembler::pass1(const Program& tokenizedLines) const {
+ Errors errors;
+ Program expandedLines;
+ expandedLines.reserve(tokenizedLines.size());
+
+ for (unsigned i = 0; i < tokenizedLines.size(); i++) {
+ const auto& tokenizedLine = tokenizedLines.at(i);
+ runOperation(expandedOps, std::optional<std::vector<LineTokens>>, expandPseudoOp, tokenizedLine);
+ if (expandedOps) {
+ for (const auto& expandedOp : expandedOps.value()) {
+ /** @note: Original source line is kept for all resulting lines after pseudo-op expantion */
+ expandedLines.push_back({expandedOp, tokenizedLine.source_line, {}});
+ }
+ } else {
+ expandedLines.push_back(tokenizedLine);
+ }
+ }
+
+ if (errors.size() != 0) {
+ return {errors};
+ } else {
+ return {expandedLines};
+ }
+}
+
+std::variant<Assembler::Errors, Assembler::NoPassResult> Assembler::pass2(std::vector<SourceLine>& tokenizedLines,
+ Symbols& symbols) const {
+ Errors errors;
+ for (auto& line : tokenizedLines) {
+ runOperation(splitLine, SymbolLinePair, splitSymbolFromLine, line);
+ const auto& [symbol, rest] = splitLine;
+ if (!symbol.isEmpty()) {
+ if (symbols.count(symbol)) {
+ errors.push_back({line.source_line, "Symbol '" + symbol + "' is already defined."});
+ }
+ line.symbol = symbol;
+ }
+ }
+
+ if (errors.size() != 0) {
+ return {errors};
+ } else {
+ return {NoPassResult()};
+ }
+}
+
+std::variant<Assembler::Errors, QByteArray> Assembler::pass3(const Program& tokenizedLines,
+ const SymbolMap& symbolMap) const {
+ QByteArray program;
+ Errors errors;
+ for (const auto& line : tokenizedLines) {
+ runOperation(machineCode, QByteArray, assembleInstruction, line, symbolMap);
+ program.append(machineCode);
+ }
+ if (errors.size() != 0) {
+ return {errors};
+ } else {
+ return {program};
+ }
+}
+
+std::variant<Assembler::Error, Assembler::LineTokens> Assembler::tokenize(const QString& line) const {
+ const static auto splitter = QRegularExpression(
+ R"(\t|\((?=x(?:[1-2]\d|3[0-1]|\d)|t[0-6]|a[0-7]|s(?:1[0-1]|\d)|[sgt]p|zero)|(?:x(?:[1-2]\d|3[0-1]|\d)|t[0-6]|a[0-7]|s(?:1[0-1]|\d)|[sgt]p|zero)\K\))");
+
+ return line.split(splitter);
+}
+
+std::variant<Assembler::Error, std::pair<QString, Assembler::LineTokens>>
+Assembler::splitSymbolFromLine(const SourceLine& line) const {
+ if (line.tokens.size() == 0) {
+ return std::pair<QString, Assembler::LineTokens>(QString(), line.tokens);
+ }
+
+ const bool hasSymbol =
+ std::any_of(line.tokens.begin(), line.tokens.end(), [](const auto& s) { return s.contains(':'); });
+
+ const auto& firstToken = line.tokens.at(0);
+
+ if (hasSymbol && !firstToken.contains(':')) {
+ return Error(line.source_line, "Line must start with symbol");
+ }
+ if (firstToken.count(':') > 1) {
+ return Error(line.source_line, "Multiple instances of ':' in label");
+ }
+
+ // Valid symbol - parse the symbol and remaining tokens
+ auto remainingTokens = line.tokens;
+ remainingTokens.removeFirst();
+ auto symbol = firstToken;
+ symbol.remove(':');
+ return std::pair<QString, Assembler::LineTokens>(symbol, remainingTokens);
+}
+
+} // namespace Ripes

ADD
src/assembler/assembler.h
+#pragma once
+
+#include "isainfo.h"
+
+#include <set>
+#include <variant>
+
+namespace Ripes {
+
+class Assembler {
+public:
+ using Symbol = QString;
+ using Symbols = std::set<Symbol>;
+ using SymbolMap = std::map<Symbol, unsigned>;
+ using LineTokens = QStringList;
+ struct SourceLine {
+ LineTokens tokens;
+ unsigned source_line = 0;
+ QString symbol;
+ };
+ using SymbolLinePair = std::pair<Symbol, LineTokens>;
+ using Program = std::vector<SourceLine>;
+ using NoPassResult = std::monostate;
+
+ // An error is defined as a reference to a source line index + an error string
+ using Error = std::pair<unsigned, QString>;
+ using Errors = std::vector<Error>;
+ /**
+ * @brief The Result struct
+ * An assembly result is determined to be valid iff errors is empty.
+ */
+ struct Result {
+ Errors errors;
+ QByteArray program;
+ };
+
+ Result assemble(const QString& program) const;
+
+private:
+ /**
+ * @brief pass0
+ * Line tokenization and source line recording
+ */
+ std::variant<Errors, Program> pass0(const QStringList& program) const;
+
+ /**
+ * @brief pass1
+ * Pseudo-op expansion. If @return errors is empty, pass succeeded.
+ */
+ std::variant<Errors, Program> pass1(const Program& tokenizedLines) const;
+ /**
+ * @brief pass2
+ * Symbol recording. If @return errors is empty, pass succeeded.
+ */
+ std::variant<Errors, NoPassResult> pass2(Program& tokenizedLines, Symbols& symbols) const;
+ /**
+ * @brief pass3
+ * Machine code translation. If @return errors is empty, pass succeeded.
+ */
+ std::variant<Errors, QByteArray> pass3(const Program& tokenizedLines, const SymbolMap& symbolMap) const;
+
+protected:
+ virtual std::variant<Error, std::optional<std::vector<LineTokens>>>
+ expandPseudoOp(const SourceLine& line) const = 0;
+ virtual std::variant<Error, LineTokens> tokenize(const QString& line) const;
+ virtual std::variant<Error, QByteArray> assembleInstruction(const SourceLine& instruction,
+ const SymbolMap& symbols) const;
+ /**
+ * @brief splitSymbol
+ * @returns a pair consisting of a symbol and the the input @p line tokens where the symbol has been removed.
+ */
+ virtual std::variant<Error, SymbolLinePair> splitSymbolFromLine(const SourceLine& line) const = 0;
+};
+
+} // namespace Ripes

ADD
src/assembler/rv32i_assembler.cpp
+#include "rv32i_assembler.h"
+
+#include <QByteArray>
+#include <algorithm>
+
+namespace Ripes {
+
+namespace {
+inline QByteArray uint32ToByteArr(uint32_t in) {
+ const char bytes[] = {static_cast<char>(in & 0xff), static_cast<char>((in >> 8) & 0xff),
+ static_cast<char>((in >> 16) & 0xff), static_cast<char>((in >> 24) & 0xff)};
+ return QByteArray::fromRawData(bytes, 4);
+}
+
+namespace instrType {
+enum {
+ LUI = 0b0110111,
+ JAL = 0b1101111,
+ JALR = 0b1100111,
+ BRANCH = 0b1100011,
+ LOAD = 0b0000011,
+ STORE = 0b0100011,
+ OP_IMM = 0b0010011,
+ OP = 0b0110011,
+ ECALL = 0b1110011,
+ AUIPC = 0b0010111,
+ INVALID = 0b0
+};
+}
+
+const static QMap<QString, uint32_t> ABInames{
+ {"zero", 0}, {"ra", 1}, {"sp", 2}, {"gp", 3}, {"tp", 4}, {"t0", 5}, {"t1", 6}, {"t2", 7},
+ {"s0", 8}, {"a0", 10}, {"a1", 11}, {"a2", 12}, {"a3", 13}, {"a4", 14}, {"a5", 15}, {"a6", 16},
+ {"a7", 17}, {"s1", 9}, {"s2", 18}, {"s3", 19}, {"s4", 20}, {"s5", 21}, {"s6", 22}, {"s7", 23},
+ {"s8", 24}, {"s9", 25}, {"s10", 26}, {"s11", 27}, {"t3", 28}, {"t4", 29}, {"t5", 30}, {"t6", 31}};
+
+uint32_t getRegisterNumber(const QString& reg, bool& success) {
+ // Converts a textual representation of a register to its numeric value
+ QString regRes = reg;
+ success = true;
+ if (reg[0] == 'x') {
+ regRes.remove('x');
+ return regRes.toInt(&success, 10);
+ } else if (ABInames.contains(reg)) {
+ return ABInames[reg];
+ }
+ success = false;
+ return 0;
+}
+
+/** @brief
+ * Wraps a functor with the expectancy of a line containing @p N tokens.
+ */
+template <int N, typename T_funct, typename T_ret, typename... Args>
+T_funct expectNTokens(const T_funct& functor) {
+ return [=](const Assembler::SourceLine& line, Args... others) {
+ if (line.tokens.size() != N) {
+ return std::variant<Assembler::Error, T_ret>({Assembler::Error(
+ line.source_line, "Expected " + QString::number(N) + " tokens but got " + line.tokens.size())});
+ } else {
+ return functor(line, others...);
+ }
+ };
+}
+
+template <uint32_t funct3, uint32_t funct7>
+std::variant<Assembler::Error, QByteArray> assembleOpInstruction(const Assembler::SourceLine& line) {
+ bool success;
+ std::vector<uint32_t> regs(3);
+ for (size_t token_idx = 1; token_idx <= regs.size(); token_idx++) {
+ const auto& regToken = line.tokens[token_idx];
+ const uint32_t reg = getRegisterNumber(regToken, success);
+ if (!success) {
+ return {Assembler::Error(line.source_line, "Unknown register '" + regToken + "'")};
+ }
+ regs.push_back(reg);
+ }
+ return uint32ToByteArr(instrType::OP | funct3 << 12 | funct7 << 25 | regs[0] << 7 | regs[1] << 15 | regs[2] << 20);
+}
+
+} // namespace
+
+RV32I_Assembler::RV32I_Assembler(const std::set<Extensions>& extensions) {
+ enableExtI();
+ for (const auto& extension : extensions) {
+ switch (extension) {
+ case Extensions::M:
+ enableExtM();
+ break;
+ case Extensions::F:
+ enableExtF();
+ break;
+ default:
+ assert(false && "Unhandled ISA extension");
+ }
+ }
+}
+
+std::variant<Assembler::Error, std::optional<std::vector<Assembler::LineTokens>>>
+RV32I_Assembler::expandPseudoOp(const SourceLine& line) const {
+ if (line.tokens.empty()) {
+ return {};
+ }
+ const auto& opcode = line.tokens.at(0);
+ if (m_pseudoOpExpanders.count(opcode) == 0) {
+ return {};
+ }
+ return m_pseudoOpExpanders.at(opcode)(line);
+}
+
+std::variant<Assembler::Error, QByteArray>
+RV32I_Assembler::assembleInstruction(const SourceLine& line, const Assembler::SymbolMap& symbols) const {
+ if (line.tokens.empty()) {
+ return {QByteArray()};
+ }
+ const auto& opcode = line.tokens.at(0);
+ if (m_assemblers.count(opcode)) {
+ return {Error(line.source_line, "Unknown opcode '" + opcode + "'")};
+ }
+ return {m_assemblers.at(opcode)(line, symbols)};
+}
+
+void RV32I_Assembler::addPseudoOpExpanderFunctor(const QString& opcode, PseudoOpFunctor functor) {
+ if (m_pseudoOpExpanders.count(opcode)) {
+ assert(false && "Pseudo-op expander already registerred for opcode");
+ }
+ m_pseudoOpExpanders[opcode] = functor;
+}
+
+void RV32I_Assembler::addAssemblerFunctor(const QString& opcode, AssemblerFunctor functor) {
+ if (m_assemblers.count(opcode)) {
+ assert(false && "Assembler already registerred for opcode");
+ }
+ m_assemblers[opcode] = functor;
+}
+
+void RV32I_Assembler::enableExtI() {
+ // Pseudo-op functors
+ addPseudoOpExpanderFunctor("nop", expectNTokens<1, PseudoOpFunctor, PseudoOpFunctorRetT>([](const SourceLine&) {
+ return std::vector<Assembler::LineTokens>{QString("addi x0 x0 0").split(' ')};
+ }));
+
+ // Assembler functors
+}
+
+void RV32I_Assembler::enableExtM() {
+ // Pseudo-op functors
+
+ // Assembler functors
+
+ addAssemblerFunctor("mul", expectNTokens<3, AssemblerFunctor, AssemblerFunctorRetT, const Assembler::SymbolMap&>(
+ [](const SourceLine& line, const Assembler::SymbolMap&) {
+ return assembleOpInstruction<0b000, 0b0000001>(line);
+ }));
+
+ addAssemblerFunctor("mulh", expectNTokens<3, AssemblerFunctor, AssemblerFunctorRetT, const Assembler::SymbolMap&>(
+ [](const SourceLine& line, const Assembler::SymbolMap&) {
+ return assembleOpInstruction<0b001, 0b0000001>(line);
+ }));
+ addAssemblerFunctor("mulhsu", expectNTokens<3, AssemblerFunctor, AssemblerFunctorRetT, const Assembler::SymbolMap&>(
+ [](const SourceLine& line, const Assembler::SymbolMap&) {
+ return assembleOpInstruction<0b011, 0b0000001>(line);
+ }));
+}
+void RV32I_Assembler::enableExtF() {
+ // Pseudo-op functors
+
+ // Assembler functors
+}
+
+} // namespace Ripes

ADD
src/assembler/rv32i_assembler.h
+#pragma once
+
+#include "assembler.h"
+
+#include <functional>
+
+namespace Ripes {
+
+class RV32I_Assembler : public Assembler {
+public:
+ enum class Extensions { M, F };
+ RV32I_Assembler(const std::set<Extensions>& extensions);
+
+protected:
+ std::variant<Error, std::optional<std::vector<LineTokens>>> expandPseudoOp(const SourceLine& line) const override;
+ std::variant<Error, QByteArray> assembleInstruction(const SourceLine& line,
+ const SymbolMap& symbols) const override;
+
+public:
+ /**
+ * @brief m_pseudoOpExpanders
+ * A map of (pseudo)opcodes => pseudo-op expander functors
+ */
+ using PseudoOpFunctorRetT = std::optional<std::vector<Assembler::LineTokens>>;
+ using PseudoOpFunctor = std::function<std::variant<Assembler::Error, PseudoOpFunctorRetT>(const SourceLine&)>;
+ std::map<QString, PseudoOpFunctor> m_pseudoOpExpanders;
+ void addPseudoOpExpanderFunctor(const QString& opcode, PseudoOpFunctor functor);
+
+ /**
+ * @brief m_assemblers
+ * a map of opcodes => instruction assembler functors
+ */
+ using AssemblerFunctorRetT = QByteArray;
+ using AssemblerFunctor = std::function<std::variant<Assembler::Error, AssemblerFunctorRetT>(
+ const SourceLine&, const Assembler::SymbolMap&)>;
+ std::map<QString, AssemblerFunctor> m_assemblers;
+ void addAssemblerFunctor(const QString& opcode, AssemblerFunctor functor);
+
+ /**
+ * Extension enablers
+ * Calling an extension enabler will register the appropriate assemblers and pseudo-op expander functors with
+ * the assembler.
+ */
+ void enableExtI();
+ void enableExtM();
+ void enableExtF();
+};
+
+} // namespace Ripes

[Assembler] Initial commit for new assembler infrastructure