diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 0000000..195f8a5 --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,152 @@ +{ + "version": 3, + "configurePresets": [ + { + "name": "linux-base", + "hidden": true, + "generator": "Unix Makefiles", + "binaryDir": "${sourceDir}/build/${presetName}", + "installDir": "${sourceDir}/install/${presetName}", + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Linux" + } + }, + { + "name": "clang-debug", + "displayName": "Clang debug", + "inherits": [ + "linux-base" + ], + "hidden": false, + "cacheVariables": { + "CMAKE_INSTALL_PREFIX": "${sourceDir}/out/install/${presetName}", + "CMAKE_BUILD_TYPE": "Debug", + "CMAKE_CXX_COMPILER": "clang++", + "CMAKE_EXPORT_COMPILE_COMMANDS": "On", + "ENABLE_PY_BINDINGS": "On", + "BUILD_DOCS": "Off" + } + }, + { + "name": "clang-release", + "displayName": "Clang release", + "inherits": [ + "linux-base" + ], + "hidden": false, + "cacheVariables": { + "CMAKE_INSTALL_PREFIX": "${sourceDir}/out/install/${presetName}", + "CMAKE_BUILD_TYPE": "Release", + "CMAKE_CXX_COMPILER": "clang++", + "ENABLE_PY_BINDINGS": "On", + "BUILD_DOCS": "Off" + } + }, + { + "name": "clang-sanitize", + "displayName": "Clang sanitize", + "inherits": [ + "linux-base" + ], + "hidden": false, + "cacheVariables": { + "CMAKE_INSTALL_PREFIX": "${sourceDir}/out/install/${presetName}", + "CMAKE_BUILD_TYPE": "Release", + "CMAKE_CXX_COMPILER": "clang++", + "USE_SANITIZER": "undefined,address,leak", + "USE_STATIC_ANALYZER": "clang-tidy", + "BUILD_DOCS": "Off" + } + }, + { + "name": "clang-coverage", + "displayName": "Clang release with coverage", + "inherits": [ + "linux-base" + ], + "hidden": false, + "cacheVariables": { + "CMAKE_INSTALL_PREFIX": "${sourceDir}/out/install/${presetName}", + "CMAKE_BUILD_TYPE": "Release", + "CMAKE_CXX_COMPILER": "clang++-21", + "CODE_COVERAGE": "ON", + "BUILD_DOCS": "Off" + } + }, + { + "name": "gcc-debug", + "displayName": "GCC debug", + "inherits": [ + "linux-base" + ], + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "CMAKE_CXX_COMPILER": "g++", + "ENABLE_PY_BINDINGS": "On", + "BUILD_DOCS": "Off" + } + }, + { + "name": "gcc-release", + "displayName": "GCC release", + "inherits": [ + "linux-base" + ], + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "CMAKE_CXX_COMPILER": "g++", + "ENABLE_PY_BINDINGS": "On", + "BUILD_DOCS": "Off" + } + }, + { + "name": "macos-base", + "hidden": true, + "generator": "Unix Makefiles", + "binaryDir": "${sourceDir}/build/${presetName}", + "installDir": "${sourceDir}/install/${presetName}", + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Darwin" + } + }, + { + "name": "macos-debug", + "displayName": "macOS debug", + "inherits": [ + "macos-base" + ], + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "CMAKE_EXPORT_COMPILE_COMMANDS": "On", + "ENABLE_PY_BINDINGS": "On", + "BUILD_DOCS": "Off" + } + }, + { + "name": "macos-release", + "displayName": "macOS Release", + "inherits": [ + "macos-base" + ], + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release", + "ENABLE_PY_BINDINGS": "On", + "BUILD_DOCS": "Off" + } + }, + { + "name": "docs", + "displayName": "Documentation", + "hidden": false, + "generator": "Ninja", + "binaryDir": "${sourceDir}/build/${presetName}", + "cacheVariables": { + "BUILD_DOCS": "On" + } + } + ] +} diff --git a/hex.cpp b/hex.cpp index cfa2db2..b8b9ffe 100644 --- a/hex.cpp +++ b/hex.cpp @@ -47,6 +47,10 @@ const char *hex::oprInstrEnumToStr(OprInstr oprInstr) { return "SUB"; case SVC: return "SVC"; + case IN: + return "IN"; + case OUT: + return "OUT"; default: return "UNKNOWN"; } diff --git a/hex.hpp b/hex.hpp index 60f85d2..c012ce1 100644 --- a/hex.hpp +++ b/hex.hpp @@ -25,7 +25,7 @@ enum Instr { NFIX = 0xF, }; -enum OprInstr { BRB = 0x0, ADD = 0x1, SUB = 0x2, SVC = 0x3 }; +enum OprInstr { BRB = 0x0, ADD = 0x1, SUB = 0x2, SVC = 0x3, IN = 0x4, OUT = 0x5 }; enum class Syscall { EXIT = 0, WRITE = 1, READ = 2, NUM_VALUES }; diff --git a/hexasm.hpp b/hexasm.hpp index 2b842ea..56b14b0 100644 --- a/hexasm.hpp +++ b/hexasm.hpp @@ -79,6 +79,8 @@ enum class Token { SVC, ADD, SUB, + IN, + OUT, OPR, IDENTIFIER, END_OF_FILE, @@ -138,6 +140,10 @@ static const char *tokenEnumStr(Token token) { return "ADD"; case Token::SUB: return "SUB"; + case Token::IN: + return "IN"; + case Token::OUT: + return "OUT"; case Token::OPR: return "OPR"; case Token::IDENTIFIER: @@ -208,6 +214,10 @@ static hex::OprInstr tokenToOprInstr(Token token) { return hex::OprInstr::ADD; case Token::SUB: return hex::OprInstr::SUB; + case Token::IN: + return hex::OprInstr::IN; + case Token::OUT: + return hex::OprInstr::OUT; default: throw std::runtime_error( std::string("unexpected operand instrucion token: ") + @@ -437,14 +447,14 @@ class InstrOp : public Directive { public: InstrOp(Token token, Token opcode) : Directive(token), opcode(opcode) { if (opcode != Token::BRB && opcode != Token::ADD && opcode != Token::SUB && - opcode != Token::SVC) { + opcode != Token::SVC && opcode != Token::IN && opcode != Token::OUT) { throw InvalidOprError(opcode); } } InstrOp(Location location, Token token, Token opcode) : Directive(location, token), opcode(opcode) { if (opcode != Token::BRB && opcode != Token::ADD && opcode != Token::SUB && - opcode != Token::SVC) { + opcode != Token::SVC && opcode != Token::IN && opcode != Token::OUT) { throw InvalidOprError(location, opcode); } } @@ -512,6 +522,7 @@ class Lexer { table.insert("BRZ", Token::BRZ); table.insert("DATA", Token::DATA); table.insert("FUNC", Token::FUNC); + table.insert("IN", Token::IN); table.insert("LDAC", Token::LDAC); table.insert("LDAI", Token::LDAI); table.insert("LDAM", Token::LDAM); @@ -520,6 +531,7 @@ class Lexer { table.insert("LDBI", Token::LDBI); table.insert("LDBM", Token::LDBM); table.insert("OPR", Token::OPR); + table.insert("OUT", Token::OUT); table.insert("PROC", Token::PROC); table.insert("STAI", Token::STAI); table.insert("STAM", Token::STAM); @@ -962,9 +974,8 @@ class CodeGen { } } - /// Emit the binary. - void emitBin(std::string outputFilename) { - std::fstream outputFile(outputFilename, std::ios::out | std::ios::binary); + /// Emit a complete image (size-word + program + debug info) to a stream. + void emitImage(std::ostream &outputFile) { // The first four bytes are the remaining binary size. uint32_t programSizeWords = programSizeBytes >> 2; outputFile.write(reinterpret_cast(&programSizeWords), @@ -972,7 +983,12 @@ class CodeGen { // Emit the program. emitProgramBin(outputFile); emitDebugInfo(outputFile); - // Done. + } + + /// Emit the binary. + void emitBin(std::string outputFilename) { + std::fstream outputFile(outputFilename, std::ios::out | std::ios::binary); + emitImage(outputFile); outputFile.close(); } }; diff --git a/hexsim.cpp b/hexsim.cpp index 5102d38..6c4d007 100644 --- a/hexsim.cpp +++ b/hexsim.cpp @@ -56,13 +56,16 @@ int main(int argc, const char *argv[]) { help(argv); return 1; } - hexsim::Processor p(std::cin, std::cout, maxCycles); - p.setTracing(trace); - p.load(filename, dumpBinary); if (dumpBinary) { + // Dumping inspects a single image directly. + hexsim::Processor p(std::cin, std::cout, maxCycles); + p.load(filename, true); return 0; } - return p.run(); + hexsim::System system(std::cin, std::cout, maxCycles); + system.setTracing(trace); + system.loadNetwork(filename); + return system.run(); } catch (std::exception &e) { std::cerr << "Error: " << e.what() << "\n"; return 1; diff --git a/hexsim.hpp b/hexsim.hpp index c099e7b..bb15ddc 100644 --- a/hexsim.hpp +++ b/hexsim.hpp @@ -11,12 +11,30 @@ #include #include #include +#include +#include +#include #include "hex.hpp" #include "hexsimio.hpp" namespace hexsim { +/// Outcome of executing a single instruction. +enum class StepResult { RUNNING, HALTED, BLOCKED }; + +class Processor; + +/// A point-to-point synchronous channel connecting two processors. Holds the +/// rendezvous state and (when a writer is parked) the value in flight. +struct Channel { + enum class State { IDLE, WRITER_WAITING, READER_WAITING }; + State state = State::IDLE; + uint32_t value = 0; + Processor *writer = nullptr; + Processor *reader = nullptr; +}; + class Processor { // Constants. @@ -47,6 +65,12 @@ class Processor { bool tracing; int exitCode; + // Multi-processor network state. + unsigned id = 0; + StepResult status = StepResult::RUNNING; + std::array links{}; // link slot -> channel (null if unwired) + unsigned blockedSlot = 0; // slot this processor is blocked on + // State for tracing. uint32_t lastPC; size_t cycles; @@ -80,18 +104,18 @@ class Processor { void setTracing(bool value) { tracing = value; } void setTruncateInputs(bool value) { truncateInputs = value; } - void load(const char *filename, bool dumpContents = false) { - // Load the binary file. - std::streampos fileSize; - std::ifstream file(filename, std::ios::binary); - - // Get length of file. - file.seekg(0, std::ios::end); - fileSize = file.tellg(); - file.seekg(0, std::ios::beg); + void setId(unsigned value) { id = value; } + void setLink(unsigned slot, Channel *channel) { links[slot] = channel; } + StepResult getStatus() const { return status; } + int getExitCode() const { return exitCode; } + unsigned getBlockedSlot() const { return blockedSlot; } - // Check the file length matches. - unsigned remainingFileSize = static_cast(fileSize) - 4; + /// Load a single image (size-word + code + optional debug info) from a + /// stream. imageSizeBytes is the total number of bytes the image occupies. + /// Returns the program size in bytes. + unsigned loadFromStream(std::istream &file, unsigned imageSizeBytes) { + // Check the image length matches. + unsigned remainingFileSize = imageSizeBytes - 4; remainingFileSize = (remainingFileSize + 3U) & ~3U; // Round up to multiple of 4. unsigned programSize; @@ -133,6 +157,18 @@ class Processor { } } + return programSize; + } + + /// Load a binary file as this processor's single image. + void load(const char *filename, bool dumpContents = false) { + std::ifstream file(filename, std::ios::binary); + file.seekg(0, std::ios::end); + auto fileSize = file.tellg(); + file.seekg(0, std::ios::beg); + unsigned programSize = + loadFromStream(file, static_cast(fileSize)); + // Print the contents of the binary. if (dumpContents) { out << "Read " << std::to_string(programSize) << " bytes\n"; @@ -268,106 +304,337 @@ class Processor { } } - int run() { - while (running && (maxCycles > 0 ? cycles <= maxCycles : true)) { - instr = (memory[pc >> 2] >> ((pc & 0x3) << 3)) & 0xFF; - lastPC = pc; - pc = pc + 1; - oreg = oreg | (instr & 0xF); - instrEnum = static_cast((instr >> 4) & 0xF); + /// Advance past the instruction just executed (commit PC, clear oreg). + void advanceInstr() { + lastPC = pc; + pc = pc + 1; + oreg = 0; + cycles++; + } + + /// Resume a partner that was parked on a blocking channel operation: advance + /// it past the instruction and mark it runnable again. + void unblockAdvance() { + advanceInstr(); + status = StepResult::RUNNING; + } + + void traceChannel(hex::OprInstr opr, unsigned slot) { + out << fmt::format("{:<6d} {:<6d} {:<4} {:<2d} {} channel {}\n", cycles, pc, + instrEnumToStr(hex::Instr::OPR), (instr & 0xF), + oprInstrEnumToStr(opr), slot); + } + + /// Execute a channel IN/OUT operation, performing the rendezvous if the + /// partner is already waiting, otherwise parking this processor (PC is not + /// advanced so the operation completes when the partner arrives). + StepResult stepChannel(hex::OprInstr opr) { + unsigned slot = breg; + if (slot >= links.size() || links[slot] == nullptr) { + throw std::runtime_error(fmt::format( + "processor {}: unwired channel slot {} at pc {:#08x}", id, slot, pc)); + } + Channel *c = links[slot]; + if (opr == hex::OprInstr::OUT) { + if (c->state == Channel::State::READER_WAITING) { + c->reader->areg = areg; + c->reader->unblockAdvance(); + c->state = Channel::State::IDLE; + c->reader = nullptr; + if (tracing) { + traceChannel(opr, slot); + } + advanceInstr(); + return status = StepResult::RUNNING; + } + c->state = Channel::State::WRITER_WAITING; + c->value = areg; + c->writer = this; + blockedSlot = slot; + return status = StepResult::BLOCKED; + } + // IN. + if (c->state == Channel::State::WRITER_WAITING) { + areg = c->value; + c->writer->unblockAdvance(); + c->state = Channel::State::IDLE; + c->writer = nullptr; if (tracing) { - trace(instr, instrEnum); + traceChannel(opr, slot); } - switch (instrEnum) { - case hex::Instr::LDAM: - areg = memory[oreg]; - oreg = 0; - break; - case hex::Instr::LDBM: - breg = memory[oreg]; - oreg = 0; - break; - case hex::Instr::STAM: - memory[oreg] = areg; - oreg = 0; - break; - case hex::Instr::LDAC: - areg = oreg; - oreg = 0; - break; - case hex::Instr::LDBC: - breg = oreg; - oreg = 0; - break; - case hex::Instr::LDAP: - areg = pc + oreg; - oreg = 0; - break; - case hex::Instr::LDAI: - areg = memory[areg + oreg]; - oreg = 0; - break; - case hex::Instr::LDBI: - breg = memory[breg + oreg]; + advanceInstr(); + return status = StepResult::RUNNING; + } + c->state = Channel::State::READER_WAITING; + c->reader = this; + blockedSlot = slot; + return status = StepResult::BLOCKED; + } + + /// Execute a single instruction. Returns the resulting status. + StepResult step() { + if (status != StepResult::RUNNING) { + return status; + } + instr = (memory[pc >> 2] >> ((pc & 0x3) << 3)) & 0xFF; + oreg = oreg | (instr & 0xF); + instrEnum = static_cast((instr >> 4) & 0xF); + // Channel operations may block, so they manage the PC themselves. + if (instrEnum == hex::Instr::OPR) { + auto oprInstr = static_cast(oreg); + if (oprInstr == hex::OprInstr::IN || oprInstr == hex::OprInstr::OUT) { + return stepChannel(oprInstr); + } + } + lastPC = pc; + pc = pc + 1; + if (tracing) { + trace(instr, instrEnum); + } + switch (instrEnum) { + case hex::Instr::LDAM: + areg = memory[oreg]; + oreg = 0; + break; + case hex::Instr::LDBM: + breg = memory[oreg]; + oreg = 0; + break; + case hex::Instr::STAM: + memory[oreg] = areg; + oreg = 0; + break; + case hex::Instr::LDAC: + areg = oreg; + oreg = 0; + break; + case hex::Instr::LDBC: + breg = oreg; + oreg = 0; + break; + case hex::Instr::LDAP: + areg = pc + oreg; + oreg = 0; + break; + case hex::Instr::LDAI: + areg = memory[areg + oreg]; + oreg = 0; + break; + case hex::Instr::LDBI: + breg = memory[breg + oreg]; + oreg = 0; + break; + case hex::Instr::STAI: + memory[breg + oreg] = areg; + oreg = 0; + break; + case hex::Instr::BR: + pc = pc + oreg; + oreg = 0; + break; + case hex::Instr::BRZ: + if (areg == 0) { + pc = pc + oreg; + } + oreg = 0; + break; + case hex::Instr::BRN: + if ((int)areg < 0) { + pc = pc + oreg; + } + oreg = 0; + break; + case hex::Instr::PFIX: + oreg = oreg << 4; + break; + case hex::Instr::NFIX: + oreg = 0xFFFFFF00 | (oreg << 4); + break; + case hex::Instr::OPR: + switch (static_cast(oreg)) { + case hex::OprInstr::BRB: + pc = breg; oreg = 0; break; - case hex::Instr::STAI: - memory[breg + oreg] = areg; + case hex::OprInstr::ADD: + areg = areg + breg; oreg = 0; break; - case hex::Instr::BR: - pc = pc + oreg; + case hex::OprInstr::SUB: + areg = areg - breg; oreg = 0; break; - case hex::Instr::BRZ: - if (areg == 0) { - pc = pc + oreg; + case hex::OprInstr::SVC: + syscall(); + if (tracing) { + traceSyscall(); } - oreg = 0; break; - case hex::Instr::BRN: - if ((int)areg < 0) { - pc = pc + oreg; + default: + throw std::runtime_error("invalid OPR: " + std::to_string(oreg)); + }; + oreg = 0; + break; + default: + throw std::runtime_error("invalid instruction"); + } + cycles++; + if (!running) { + status = StepResult::HALTED; + } + return status; + } + + int run() { + while (status != StepResult::HALTED && + (maxCycles > 0 ? cycles <= maxCycles : true)) { + step(); + } + return exitCode; + } +}; + +/// Magic number identifying a network container file ("HEXN"). +static const uint32_t NETWORK_MAGIC = 0x4E584548; + +/// A fixed network of processors connected by point-to-point channels. Boots +/// all processors at reset and steps them round-robin until they all halt. +class System { + std::vector> procs; + std::vector> channels; + std::istream ∈ + std::ostream &out; + size_t maxCycles; + bool tracing = false; + // Default to truncating character inputs, matching the hardware and xhexb.x + // behaviour. Tests may enable sign-extension to exercise negative values. + bool truncateInputs = true; + int exitCode = 0; + bool haveExit = false; + +public: + System(std::istream &in, std::ostream &out, size_t maxCycles = 0) + : in(in), out(out), maxCycles(maxCycles) {} + + void setTracing(bool value) { tracing = value; } + void setTruncateInputs(bool value) { truncateInputs = value; } + + /// Load a network container, or fall back to a single-processor system if the + /// file is a plain image (no network magic). + void loadNetwork(const char *filename) { + std::ifstream file(filename, std::ios::binary); + if (!file) { + throw std::runtime_error(std::string("could not open file: ") + filename); + } + file.seekg(0, std::ios::end); + auto fileSize = file.tellg(); + file.seekg(0, std::ios::beg); + + uint32_t magic; + file.read(reinterpret_cast(&magic), sizeof(uint32_t)); + if (magic != NETWORK_MAGIC) { + // Plain single image: rewind and load the whole file as one processor. + file.seekg(0, std::ios::beg); + addProcessor(file, static_cast(fileSize), 0); + return; + } + + uint32_t numProcessors; + uint32_t numEdges; + file.read(reinterpret_cast(&numProcessors), sizeof(uint32_t)); + file.read(reinterpret_cast(&numEdges), sizeof(uint32_t)); + + struct Edge { + uint32_t procA, slotA, procB, slotB; + }; + std::vector edges(numEdges); + for (auto &e : edges) { + file.read(reinterpret_cast(&e.procA), sizeof(uint32_t)); + file.read(reinterpret_cast(&e.slotA), sizeof(uint32_t)); + file.read(reinterpret_cast(&e.procB), sizeof(uint32_t)); + file.read(reinterpret_cast(&e.slotB), sizeof(uint32_t)); + } + + // Read each embedded image into its own processor. + for (uint32_t i = 0; i < numProcessors; i++) { + uint32_t imageSize; + file.read(reinterpret_cast(&imageSize), sizeof(uint32_t)); + std::vector buffer(imageSize); + file.read(buffer.data(), imageSize); + std::istringstream imageStream(std::string(buffer.begin(), buffer.end()), + std::ios::binary); + addProcessor(imageStream, imageSize, i); + } + + // Wire up the channels. + for (auto &e : edges) { + auto channel = std::make_unique(); + procs[e.procA]->setLink(e.slotA, channel.get()); + procs[e.procB]->setLink(e.slotB, channel.get()); + channels.push_back(std::move(channel)); + } + } + + /// Run the network round-robin until all processors halt. Returns the exit + /// code of the first processor to exit. Throws on deadlock. + int run() { + if (procs.empty()) { + return 0; + } + size_t ticks = 0; + while (true) { + // Step every runnable processor once. + for (auto &p : procs) { + if (p->getStatus() == StepResult::RUNNING) { + p->step(); } - oreg = 0; - break; - case hex::Instr::PFIX: - oreg = oreg << 4; - break; - case hex::Instr::NFIX: - oreg = 0xFFFFFF00 | (oreg << 4); - break; - case hex::Instr::OPR: - switch (static_cast(oreg)) { - case hex::OprInstr::BRB: - pc = breg; - oreg = 0; - break; - case hex::OprInstr::ADD: - areg = areg + breg; - oreg = 0; - break; - case hex::OprInstr::SUB: - areg = areg - breg; - oreg = 0; - break; - case hex::OprInstr::SVC: - syscall(); - if (tracing) { - traceSyscall(); + } + // Record the exit code of the first (lowest-id) halted processor. + if (!haveExit) { + for (auto &p : procs) { + if (p->getStatus() == StepResult::HALTED) { + exitCode = p->getExitCode(); + haveExit = true; + break; } - break; - default: - throw std::runtime_error("invalid OPR: " + std::to_string(oreg)); - }; - oreg = 0; - break; - default: - throw std::runtime_error("invalid instruction"); + } + } + // Termination and deadlock detection. + bool allHalted = true; + bool anyRunning = false; + for (auto &p : procs) { + auto s = p->getStatus(); + allHalted = allHalted && s == StepResult::HALTED; + anyRunning = anyRunning || s == StepResult::RUNNING; + } + if (allHalted) { + return exitCode; + } + if (!anyRunning) { + std::string msg = "deadlock detected:"; + for (size_t i = 0; i < procs.size(); i++) { + if (procs[i]->getStatus() == StepResult::BLOCKED) { + msg += fmt::format(" processor {} blocked on channel slot {};", i, + procs[i]->getBlockedSlot()); + } + } + throw std::runtime_error(msg); + } + ticks++; + if (maxCycles > 0 && ticks > maxCycles) { + return exitCode; } - cycles++; } - return exitCode; + } + +private: + void addProcessor(std::istream &image, unsigned imageSize, unsigned id) { + auto p = std::make_unique(in, out, maxCycles); + p->setId(id); + p->setTracing(tracing); + p->setTruncateInputs(truncateInputs); + p->loadFromStream(image, imageSize); + procs.push_back(std::move(p)); } }; diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index 6be8164..9703fff 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -8,7 +8,8 @@ add_executable( asm_programs.cpp x_features.cpp x_programs.cpp - dis_features.cpp) + dis_features.cpp + sim_features.cpp) target_link_libraries(UnitTests PRIVATE Catch2::Catch2WithMain fmt::fmt) diff --git a/tests/unit/TestContext.hpp b/tests/unit/TestContext.hpp index 3835670..86122ca 100644 --- a/tests/unit/TestContext.hpp +++ b/tests/unit/TestContext.hpp @@ -183,12 +183,12 @@ struct TestContext { std::istringstream simInBuffer(input); simOutBuffer.str(""); simOutBuffer.clear(); - // Run the program. - hexsim::Processor processor(simInBuffer, simOutBuffer); - processor.load(path.c_str()); - processor.setTracing(trace); - processor.setTruncateInputs(false); - return processor.run(); + // Run the program (single image or multi-processor network container). + hexsim::System system(simInBuffer, simOutBuffer); + system.setTracing(trace); + system.setTruncateInputs(false); + system.loadNetwork(path.c_str()); + return system.run(); } /// Run an X program from a file. diff --git a/tests/unit/dis_features.cpp b/tests/unit/dis_features.cpp index d52df3d..094699d 100644 --- a/tests/unit/dis_features.cpp +++ b/tests/unit/dis_features.cpp @@ -79,6 +79,22 @@ TEST_CASE("[dis_features] opr_sub_instructions") { REQUIRE(output.find("SVC") != std::string::npos); } +TEST_CASE("[dis_features] in_out_opcodes") { + std::vector program = {0xD4, 0xD5}; + hexdis::DebugInfo debugInfo; + std::ostringstream out; + hexdis::disassemble(program, out, debugInfo, false); + REQUIRE(out.str() == " 0x0000 d4 IN\n 0x0001 d5 OUT\n"); +} + +TEST_CASE("[dis_features] in_out_assemble_roundtrip") { + TestContext ctx; + auto output = assembleAndDisassemble(ctx, "OPR IN\n" + "OPR OUT\n"); + REQUIRE(output.find("IN") != std::string::npos); + REQUIRE(output.find("OUT") != std::string::npos); +} + TEST_CASE("[dis_features] pfix_extended_operand") { TestContext ctx; // LDAC 32 requires PFIX 2, LDAC 0 -> operand becomes 0x20 = 32. diff --git a/tests/unit/sim_features.cpp b/tests/unit/sim_features.cpp new file mode 100644 index 0000000..f49a1af --- /dev/null +++ b/tests/unit/sim_features.cpp @@ -0,0 +1,183 @@ +#include "TestContext.hpp" +#include "hexsim.hpp" +#include +#include + +//===---------------------------------------------------------------------===// +// Unit tests for the multi-processor simulator (channels + network container). +//===---------------------------------------------------------------------===// + +/// Assemble an assembly source string to a binary file and return its bytes. +static std::vector assembleToBytes(const std::string &asmSrc, + const std::string &name) { + hexasm::Lexer lexer; + hexasm::Parser parser(lexer); + lexer.loadBuffer(asmSrc); + auto tree = parser.parseProgram(); + hexasm::CodeGen codeGen(tree); + fs::path path(CURRENT_BINARY_DIRECTORY); + path /= name; + codeGen.emitBin(path.c_str()); + std::ifstream file(path.string(), std::ios::binary); + file.seekg(0, std::ios::end); + auto size = file.tellg(); + file.seekg(0, std::ios::beg); + std::vector bytes(size); + file.read(bytes.data(), size); + return bytes; +} + +/// A single channel wiring edge for a network container. +struct TestEdge { + uint32_t procA, slotA, procB, slotB; +}; + +/// Write a network container from a set of image byte-blobs and edges. +static std::string writeContainer(const std::vector> &images, + const std::vector &edges, + const std::string &name) { + fs::path path(CURRENT_BINARY_DIRECTORY); + path /= name; + std::ofstream out(path.string(), std::ios::binary); + auto writeU32 = [&](uint32_t v) { + out.write(reinterpret_cast(&v), sizeof(uint32_t)); + }; + writeU32(hexsim::NETWORK_MAGIC); + writeU32(static_cast(images.size())); + writeU32(static_cast(edges.size())); + for (const auto &e : edges) { + writeU32(e.procA); + writeU32(e.slotA); + writeU32(e.procB); + writeU32(e.slotB); + } + for (const auto &image : images) { + writeU32(static_cast(image.size())); + out.write(image.data(), image.size()); + } + out.close(); + return path.string(); +} + +// A processor that sends `value` on channel slot 0 then exits 0. +static std::string senderProgram(int value) { + return "BR start\n" + "DATA 16383 # sp\n" + "start\n" + "LDAC " + + std::to_string(value) + + "\n" // areg <- value + "LDBC 0\n" // breg <- channel slot 0 + "OPR OUT\n" // send areg on channel 0 + "LDAC 0\n" + "LDBM 1\n" // breg <- sp + "STAI 2\n" // sp[2] <- 0 (exit code) + "LDAC 0\n" // areg <- 0 (EXIT) + "OPR SVC\n"; +} + +// A processor that reads a word from channel slot 0, writes it to simout, +// exits. +static std::string receiverProgram() { + return "BR start\n" + "DATA 16383 # sp\n" + "start\n" + "LDBC 0\n" // breg <- channel slot 0 + "OPR IN\n" // areg <- value from channel 0 + "LDBM 1\n" // breg <- sp + "STAI 2\n" // sp[2] <- areg (value to write) + "LDAC 0\n" // areg <- 0 (stream id) + "LDBM 1\n" // breg <- sp + "STAI 3\n" // sp[3] <- 0 (simout) + "LDAC 1\n" // areg <- 1 (WRITE) + "OPR SVC\n" // + "LDAC 0\n" + "LDBM 1\n" + "STAI 2\n" // sp[2] <- 0 (exit code) + "LDAC 0\n" // areg <- 0 (EXIT) + "OPR SVC\n"; +} + +// A processor that just reads from channel slot 0 then exits (used for +// deadlock). +static std::string readerOnlyProgram() { + return "BR start\n" + "DATA 16383 # sp\n" + "start\n" + "LDBC 0\n" + "OPR IN\n" + "LDAC 0\n" + "LDBM 1\n" + "STAI 2\n" + "LDAC 0\n" + "OPR SVC\n"; +} + +TEST_CASE("[sim_features] single_image_fallback") { + // A plain image (no network magic) loads as a one-processor system. + TestContext ctx; + auto bytes = assembleToBytes(ctx.readFile(ctx.getAsmTestPath("exit0.S")), + "sim_exit0.bin"); + fs::path path(CURRENT_BINARY_DIRECTORY); + path /= "sim_exit0.bin"; + std::istringstream in; + std::ostringstream out; + hexsim::System system(in, out); + system.loadNetwork(path.string().c_str()); + REQUIRE(system.run() == 0); +} + +TEST_CASE("[sim_features] rendezvous_writer_first") { + // Processor order means the writer (proc 0) is stepped before the reader. + TestContext ctx; + auto sender = assembleToBytes(senderProgram(65), "sim_sender.bin"); + auto receiver = assembleToBytes(receiverProgram(), "sim_receiver.bin"); + auto file = writeContainer({sender, receiver}, {{0, 0, 1, 0}}, "sim_wf.bin"); + std::istringstream in; + std::ostringstream out; + hexsim::System system(in, out); + system.loadNetwork(file.c_str()); + REQUIRE(system.run() == 0); + REQUIRE(out.str() == "A"); +} + +TEST_CASE("[sim_features] rendezvous_reader_first") { + // Reader (proc 0) is stepped before the writer (proc 1): blocks then resumes. + TestContext ctx; + auto receiver = assembleToBytes(receiverProgram(), "sim_receiver2.bin"); + auto sender = assembleToBytes(senderProgram(66), "sim_sender2.bin"); + auto file = writeContainer({receiver, sender}, {{0, 0, 1, 0}}, "sim_rf.bin"); + std::istringstream in; + std::ostringstream out; + hexsim::System system(in, out); + system.loadNetwork(file.c_str()); + REQUIRE(system.run() == 0); + REQUIRE(out.str() == "B"); +} + +TEST_CASE("[sim_features] deadlock_detected") { + // Two processors that both try to read: neither can ever proceed. + TestContext ctx; + auto r0 = assembleToBytes(readerOnlyProgram(), "sim_r0.bin"); + auto r1 = assembleToBytes(readerOnlyProgram(), "sim_r1.bin"); + auto file = writeContainer({r0, r1}, {{0, 0, 1, 0}}, "sim_dl.bin"); + std::istringstream in; + std::ostringstream out; + hexsim::System system(in, out); + system.loadNetwork(file.c_str()); + REQUIRE_THROWS_WITH(system.run(), + Catch::Matchers::ContainsSubstring("deadlock")); +} + +TEST_CASE("[sim_features] unwired_slot_runtime_error") { + // A sender wired with no channel on slot 0 raises a runtime error. + TestContext ctx; + auto sender = assembleToBytes(senderProgram(1), "sim_unwired.bin"); + auto file = writeContainer({sender}, {}, "sim_unwired_net.bin"); + std::istringstream in; + std::ostringstream out; + hexsim::System system(in, out); + system.loadNetwork(file.c_str()); + REQUIRE_THROWS_WITH(system.run(), + Catch::Matchers::ContainsSubstring("unwired channel")); +} diff --git a/tests/unit/x_features.cpp b/tests/unit/x_features.cpp index af6ed46..5d9939d 100644 --- a/tests/unit/x_features.cpp +++ b/tests/unit/x_features.cpp @@ -1,5 +1,6 @@ #include "TestContext.hpp" #include +#include #include #include #include @@ -1257,7 +1258,7 @@ TEST_CASE("token_error_string") { TEST_CASE("token_error_unexpected_char") { TestContext ctx; - auto program = "val foo = ?"; + auto program = "val foo = @"; REQUIRE_THROWS_AS(ctx.asmXProgramSrc(program), xcmp::TokenError); } @@ -1453,3 +1454,166 @@ TEST_CASE("semantics_non_const_array_length_error") { REQUIRE_THROWS_AS(ctx.asmXProgramSrc(program), xcmp::NonConstArrayLengthError); } + +//===---------------------------------------------------------------------===// +// Message passing: chan, par, !, ? (front-end) +//===---------------------------------------------------------------------===// + +TEST_CASE("chan_par_pling_query_tokens") { + TestContext ctx; + auto output = ctx.tokeniseXProgramSrc("chan par ! ?").str(); + REQUIRE(output == "chan\npar\n!\n?\nEOF\n"); +} + +TEST_CASE("chan_declaration_and_formal_tree") { + TestContext ctx; + auto output = ctx.treeXProgramSrc("chan c;\n" + "proc main(chan d) is skip") + .str(); + REQUIRE(output.find("chandecl c") != std::string::npos); + REQUIRE(output.find("chanformal d") != std::string::npos); +} + +TEST_CASE("par_statement_tree") { + TestContext ctx; + auto output = ctx.treeXProgramSrc( + "proc source(chan c) is skip\n" + "proc sink(chan c) is skip\n" + "proc main() is chan a; par { source(a); sink(a) }") + .str(); + REQUIRE(output.find("parstmt") != std::string::npos); + REQUIRE(output.find("call source") != std::string::npos); + REQUIRE(output.find("call sink") != std::string::npos); +} + +TEST_CASE("par_branch_must_be_call_error") { + TestContext ctx; + auto program = "proc sink(chan c) is skip\n" + "proc main() is chan a; par { skip; sink(a) }"; + REQUIRE_THROWS_AS(ctx.treeXProgramSrc(program), xcmp::ParserTokenError); +} + +TEST_CASE("out_statement_tree") { + TestContext ctx; + auto output = ctx.treeXProgramSrc("proc worker(chan c) is c ! 42\n" + "proc main() is skip") + .str(); + REQUIRE(output.find("outstmt") != std::string::npos); + REQUIRE(output.find("number 42") != std::string::npos); +} + +TEST_CASE("in_statement_tree") { + TestContext ctx; + auto output = ctx.treeXProgramSrc("proc worker(chan c) is var v; c ? v\n" + "proc main() is skip") + .str(); + REQUIRE(output.find("instmt") != std::string::npos); + REQUIRE(output.find("varref v") != std::string::npos); +} + +TEST_CASE("in_statement_array_target_tree") { + TestContext ctx; + auto output = ctx.treeXProgramSrc("array a[4];\n" + "proc worker(chan c) is c ? a[0]\n" + "proc main() is skip") + .str(); + REQUIRE(output.find("instmt") != std::string::npos); + REQUIRE(output.find("arraysubscript a") != std::string::npos); +} + +TEST_CASE("message_passing_ring_tree") { + TestContext ctx; + // A reused worker proc placed on a two-processor ring exercises chan + // declarations/formals, par, ! and ? together. + auto output = ctx.treeXProgramSrc( + "proc worker(chan in, chan out) is var v; " + "{ in ? v; out ! v }\n" + "proc main() is chan a; chan b; " + "par { worker(a, b); worker(b, a) }") + .str(); + REQUIRE(output.find("parstmt") != std::string::npos); + REQUIRE(output.find("instmt") != std::string::npos); + REQUIRE(output.find("outstmt") != std::string::npos); + REQUIRE(output.find("chandecl a") != std::string::npos); + REQUIRE(output.find("chandecl b") != std::string::npos); + REQUIRE(output.find("chanformal in") != std::string::npos); + REQUIRE(output.find("chanformal out") != std::string::npos); +} + +TEST_CASE("out_statement_codegen") { + TestContext ctx; + auto asmText = ctx.asmXProgramSrc("proc worker(chan c) is c ! 42\n" + "proc main() is skip", + true) + .str(); + REQUIRE(asmText.find("OPR OUT") != std::string::npos); +} + +TEST_CASE("in_statement_codegen") { + TestContext ctx; + auto asmText = ctx.asmXProgramSrc("proc worker(chan c) is var v; c ? v\n" + "proc main() is skip", + true) + .str(); + REQUIRE(asmText.find("OPR IN") != std::string::npos); +} + +TEST_CASE("message_passing_run_pipeline") { + TestContext ctx; + // source sends 'A' to sink over channel c; sink writes it to simout. + auto program = "val put = 1;\n" + "proc putval(val c) is put(c, 0)\n" + "proc source(chan out) is out ! 65\n" + "proc sink(chan in) is var v; { in ? v; putval(v) }\n" + "proc main() is chan c; par { source(c); sink(c) }"; + REQUIRE(ctx.runXProgramSrc(program) == 0); + REQUIRE(ctx.simOutBuffer.str() == "A"); +} + +TEST_CASE("message_passing_run_relay") { + TestContext ctx; + // A 3-stage pipeline: source -> relay -> sink, sink prints the relayed char. + auto program = "val put = 1;\n" + "proc putval(val c) is put(c, 0)\n" + "proc source(chan out) is out ! 90\n" + "proc relay(chan in, chan out) is var v; " + "{ in ? v; out ! v }\n" + "proc sink(chan in) is var v; { in ? v; putval(v) }\n" + "proc main() is chan a; chan b; " + "par { source(a); relay(a, b); sink(b) }"; + REQUIRE(ctx.runXProgramSrc(program) == 0); + REQUIRE(ctx.simOutBuffer.str() == "Z"); +} + +TEST_CASE("message_passing_run_deadlock") { + TestContext ctx; + // Both workers read before writing, so the ring deadlocks at runtime. + auto program = "proc worker(chan in, chan out) is var v; " + "{ in ? v; out ! v }\n" + "proc main() is chan a; chan b; " + "par { worker(a, b); worker(b, a) }"; + REQUIRE_THROWS_WITH(ctx.runXProgramSrc(program), + Catch::Matchers::ContainsSubstring("deadlock")); +} + +TEST_CASE("message_passing_channel_endpoint_error") { + TestContext ctx; + // Channel a is used by only one process. + auto program = "proc reader(chan c) is var v; c ? v\n" + "proc main() is chan a; par { reader(a) }"; + REQUIRE_THROWS_AS(ctx.asmXProgramSrc(program), xcmp::NetworkError); +} + +TEST_CASE("message_passing_channel_direction_error") { + TestContext ctx; + // Channel a has two readers and no writer. + auto program = "proc reader(chan c) is var v; c ? v\n" + "proc main() is chan a; par { reader(a); reader(a) }"; + REQUIRE_THROWS_AS(ctx.asmXProgramSrc(program), xcmp::NetworkError); +} + +TEST_CASE("message_passing_run_pipe_x_file") { + TestContext ctx; + REQUIRE(ctx.runXProgramFile(ctx.getXTestPath("pipe.x")) == 0); + REQUIRE(ctx.simOutBuffer.str() == "P"); +} diff --git a/tests/x/pipe.x b/tests/x/pipe.x new file mode 100644 index 0000000..67d650a --- /dev/null +++ b/tests/x/pipe.x @@ -0,0 +1,18 @@ +val put = 1; + +proc putval(val c) is put(c, 0) + +proc source(chan out) is out ! 'P' + +proc relay(chan in, chan out) is + var v; + { in ? v; out ! v } + +proc sink(chan in) is + var v; + { in ? v; putval(v) } + +proc main() is + chan a; + chan b; + par { source(a); relay(a, b); sink(b) } diff --git a/xcmp.hpp b/xcmp.hpp index 15fd69d..aec478a 100644 --- a/xcmp.hpp +++ b/xcmp.hpp @@ -12,6 +12,8 @@ #include #include #include +#include +#include #include #include #include @@ -73,6 +75,10 @@ enum class Token { LE, GR, GE, + CHAN, + PAR, + PLING, + QUERY, END_OF_FILE }; @@ -158,6 +164,14 @@ inline const char *tokenEnumStr(Token token) { return ">"; case Token::GE: return ">="; + case Token::CHAN: + return "chan"; + case Token::PAR: + return "par"; + case Token::PLING: + return "!"; + case Token::QUERY: + return "?"; case Token::END_OF_FILE: return "END_OF_FILE"; default: @@ -237,6 +251,13 @@ struct InvalidSyscallError : public Error { : Error(location, fmt::format("invalid syscall: {}", sysCallId)) {} }; +/// Errors detected when building the static process network from a top-level +/// par. +struct NetworkError : public Error { + NetworkError(Location location, std::string message) + : Error(location, message) {} +}; + //===---------------------------------------------------------------------===// // Lexer //===---------------------------------------------------------------------===// @@ -276,6 +297,7 @@ class Lexer { void declareKeywords() { table.insert("and", Token::AND); table.insert("array", Token::ARRAY); + table.insert("chan", Token::CHAN); table.insert("do", Token::DO); table.insert("else", Token::ELSE); table.insert("false", Token::FALSE); @@ -283,6 +305,7 @@ class Lexer { table.insert("if", Token::IF); table.insert("is", Token::IS); table.insert("or", Token::OR); + table.insert("par", Token::PAR); table.insert("proc", Token::PROC); table.insert("return", Token::RETURN); table.insert("skip", Token::SKIP); @@ -437,6 +460,14 @@ class Lexer { readChar(); token = Token::SEMICOLON; break; + case '!': + readChar(); + token = Token::PLING; + break; + case '?': + readChar(); + token = Token::QUERY; + break; case ',': readChar(); token = Token::COMMA; @@ -589,6 +620,7 @@ class Expr; class ArrayDecl; class ValDecl; class VarDecl; +class ChanDecl; class BinaryOpExpr; class UnaryOpExpr; class StringExpr; @@ -602,6 +634,7 @@ class VarFormal; class ArrayFormal; class ProcFormal; class FuncFormal; +class ChanFormal; class SkipStatement; class StopStatement; class ReturnStatement; @@ -610,6 +643,9 @@ class WhileStatement; class SeqStatement; class CallStatement; class AssStatement; +class ParStatement; +class OutStatement; +class InStatement; /// A visitor base class for the AST. class AstVisitor { @@ -653,6 +689,8 @@ class AstVisitor { virtual void visitPost(ArrayDecl &) {} virtual void visitPre(VarDecl &) {} virtual void visitPost(VarDecl &) {} + virtual void visitPre(ChanDecl &) {} + virtual void visitPost(ChanDecl &) {} virtual void visitPre(ValDecl &) {} virtual void visitPost(ValDecl &) {} virtual void visitPre(BinaryOpExpr &) {} @@ -681,6 +719,8 @@ class AstVisitor { virtual void visitPost(ProcFormal &) {} virtual void visitPre(FuncFormal &) {} virtual void visitPost(FuncFormal &) {} + virtual void visitPre(ChanFormal &) {} + virtual void visitPost(ChanFormal &) {} virtual void visitPre(SkipStatement &) {} virtual void visitPost(SkipStatement &) {} virtual void visitPre(StopStatement &) {} @@ -697,6 +737,12 @@ class AstVisitor { virtual void visitPost(CallStatement &) {} virtual void visitPre(AssStatement &) {} virtual void visitPost(AssStatement &) {} + virtual void visitPre(ParStatement &) {} + virtual void visitPost(ParStatement &) {} + virtual void visitPre(OutStatement &) {} + virtual void visitPost(OutStatement &) {} + virtual void visitPre(InStatement &) {} + virtual void visitPost(InStatement &) {} }; /// AST node base class. @@ -916,6 +962,15 @@ class VarDecl : public Decl { } }; +class ChanDecl : public Decl { +public: + ChanDecl(Location location, std::string name) : Decl(location, name) {} + virtual void accept(AstVisitor *visitor) override { + visitor->visitPre(*this); + visitor->visitPost(*this); + } +}; + class ArrayDecl : public Decl { std::unique_ptr expr; @@ -991,6 +1046,15 @@ class FuncFormal : public Formal { } }; +class ChanFormal : public Formal { +public: + ChanFormal(Location location, std::string name) : Formal(location, name) {} + virtual void accept(AstVisitor *visitor) override { + visitor->visitPre(*this); + visitor->visitPost(*this); + } +}; + // Statement ================================================================ // struct Statement : public AstNode { @@ -1133,6 +1197,67 @@ class AssStatement : public Statement { const std::unique_ptr &getRHS() { return RHS; } }; +class ParStatement : public Statement { + std::vector> branches; + +public: + ParStatement(Location location, + std::vector> branches) + : Statement(location), branches(std::move(branches)) {} + virtual void accept(AstVisitor *visitor) override { + visitor->visitPre(*this); + for (auto &branch : branches) { + branch->accept(visitor); + } + visitor->visitPost(*this); + } + std::vector> &getBranches() { return branches; } +}; + +class OutStatement : public Statement { + std::unique_ptr channel, value; + +public: + OutStatement(Location location, std::unique_ptr channel, + std::unique_ptr value) + : Statement(location), channel(std::move(channel)), + value(std::move(value)) {} + virtual void accept(AstVisitor *visitor) override { + visitor->visitPre(*this); + if (visitor->shouldRecurseStmts()) { + channel->accept(visitor); + replaceExpr(channel, visitor); + value->accept(visitor); + replaceExpr(value, visitor); + } + visitor->visitPost(*this); + } + const std::unique_ptr &getChannel() { return channel; } + const std::unique_ptr &getValue() { return value; } +}; + +class InStatement : public Statement { + std::unique_ptr channel, target; + +public: + InStatement(Location location, std::unique_ptr channel, + std::unique_ptr target) + : Statement(location), channel(std::move(channel)), + target(std::move(target)) {} + virtual void accept(AstVisitor *visitor) override { + visitor->visitPre(*this); + if (visitor->shouldRecurseStmts()) { + channel->accept(visitor); + replaceExpr(channel, visitor); + target->accept(visitor); + replaceExpr(target, visitor); + } + visitor->visitPost(*this); + } + const std::unique_ptr &getChannel() { return channel; } + const std::unique_ptr &getTarget() { return target; } +}; + // Procedures and functions ================================================= // class Proc : public AstNode { @@ -1168,6 +1293,10 @@ class Proc : public AstNode { std::vector> &getFormals() { return formals; } std::vector> &getDecls() { return decls; } const std::unique_ptr &getStatement() { return statement; } + void setStatement(std::unique_ptr stmt) { + statement = std::move(stmt); + } + void clearDecls() { decls.clear(); } }; class Program : public AstNode { @@ -1190,6 +1319,8 @@ class Program : public AstNode { visitor->exitProgram(); visitor->visitPost(*this); } + std::vector> &getGlobalDecls() { return globalDecls; } + std::vector> &getProcDecls() { return procDecls; } }; // AST printer visitor ===================================================== // @@ -1235,6 +1366,11 @@ class AstPrinter : public AstVisitor { outs << fmt::format("vardecl {}{}\n", decl.getName(), locString(decl)); }; void visitPost(VarDecl &decl) override {} + void visitPre(ChanDecl &decl) override { + indent(); + outs << fmt::format("chandecl {}{}\n", decl.getName(), locString(decl)); + }; + void visitPost(ChanDecl &decl) override {} void visitPre(ValDecl &decl) override { indent(); outs << fmt::format("valdecl {}{}\n", decl.getName(), locString(decl)); @@ -1324,6 +1460,12 @@ class AstPrinter : public AstVisitor { locString(formal)); }; void visitPost(FuncFormal &formal) override {}; + void visitPre(ChanFormal &formal) override { + indent(); + outs << fmt::format("chanformal {}{}\n", formal.getName(), + locString(formal)); + }; + void visitPost(ChanFormal &formal) override {}; void visitPre(SkipStatement &stmt) override { indent(); outs << fmt::format("skipstmt{}\n", locString(stmt)); @@ -1376,6 +1518,24 @@ class AstPrinter : public AstVisitor { indentCount++; }; void visitPost(AssStatement &stmt) override { indentCount--; }; + void visitPre(ParStatement &stmt) override { + indent(); + outs << fmt::format("parstmt{}\n", locString(stmt)); + indentCount++; + }; + void visitPost(ParStatement &stmt) override { indentCount--; }; + void visitPre(OutStatement &stmt) override { + indent(); + outs << fmt::format("outstmt{}\n", locString(stmt)); + indentCount++; + }; + void visitPost(OutStatement &stmt) override { indentCount--; }; + void visitPre(InStatement &stmt) override { + indent(); + outs << fmt::format("instmt{}\n", locString(stmt)); + indentCount++; + }; + void visitPost(InStatement &stmt) override { indentCount--; }; }; //===---------------------------------------------------------------------===// @@ -1576,6 +1736,12 @@ class Parser { expect(Token::SEMICOLON); return std::make_unique(location, name); } + case Token::CHAN: { + lexer.getNextToken(); + auto name = parseIdentifier(); + expect(Token::SEMICOLON); + return std::make_unique(location, name); + } case Token::ARRAY: { lexer.getNextToken(); auto name = parseIdentifier(); @@ -1599,7 +1765,8 @@ class Parser { std::vector> parseLocalDecls() { std::vector> decls; while (lexer.getLastToken() == Token::VAL || - lexer.getLastToken() == Token::VAR) { + lexer.getLastToken() == Token::VAR || + lexer.getLastToken() == Token::CHAN) { decls.push_back(parseDecl()); } return decls; @@ -1615,6 +1782,7 @@ class Parser { std::vector> decls; while (lexer.getLastToken() == Token::VAL || lexer.getLastToken() == Token::VAR || + lexer.getLastToken() == Token::CHAN || lexer.getLastToken() == Token::ARRAY) { decls.push_back(parseDecl()); } @@ -1650,6 +1818,9 @@ class Parser { case Token::VAR: lexer.getNextToken(); return std::make_unique(location, parseIdentifier()); + case Token::CHAN: + lexer.getNextToken(); + return std::make_unique(location, parseIdentifier()); case Token::ARRAY: lexer.getNextToken(); return std::make_unique(location, parseIdentifier()); @@ -1723,6 +1894,23 @@ class Parser { expect(Token::END); return std::make_unique(location, std::move(body)); } + case Token::PAR: { + lexer.getNextToken(); + expect(Token::BEGIN); + // Branches are separated by ";", consistent with the other "{ }" blocks. + auto branches = parseStatements(); + expect(Token::END); + // Each par branch must be a procedure call (the entry process for one + // processor). + for (auto &branch : branches) { + if (!dynamic_cast(branch.get())) { + throw ParserTokenError(branch->getLocation(), + "par branch must be a procedure call", + lexer.getLastToken()); + } + } + return std::make_unique(location, std::move(branches)); + } case Token::IDENTIFIER: { auto element = parseElement(); // Procedure call @@ -1731,6 +1919,18 @@ class Parser { static_cast(element.release())); return std::make_unique(location, std::move(callExpr)); } + // Channel output: "!" + if (lexer.getLastToken() == Token::PLING) { + lexer.getNextToken(); + return std::make_unique(location, std::move(element), + parseExpr()); + } + // Channel input: "?" + if (lexer.getLastToken() == Token::QUERY) { + lexer.getNextToken(); + return std::make_unique(location, std::move(element), + parseElement()); + } // Assignment expect(Token::ASS); return std::make_unique(location, std::move(element), @@ -1777,7 +1977,8 @@ class Parser { // Declarations std::vector> decls; if (lexer.getLastToken() == Token::VAL || - lexer.getLastToken() == Token::VAR) { + lexer.getLastToken() == Token::VAR || + lexer.getLastToken() == Token::CHAN) { decls = parseLocalDecls(); } auto statement = parseStatement(); @@ -1815,7 +2016,7 @@ class Parser { // Symbol table. //===---------------------------------------------------------------------===// -enum class SymbolType { VAL, VAR, ARRAY, FUNC, PROC }; +enum class SymbolType { VAL, VAR, ARRAY, FUNC, PROC, CHAN }; class Symbol; @@ -1927,6 +2128,12 @@ class CreateSymbols : public AstVisitor { getCurrentScope(), decl.getName())); } + void visitPre(ChanDecl &decl) { + symbolTable.insert(std::make_pair(getCurrentScope(), decl.getName()), + std::make_unique(SymbolType::CHAN, &decl, + getCurrentScope(), + decl.getName())); + } void visitPre(ValDecl &decl) { symbolTable.insert(std::make_pair(getCurrentScope(), decl.getName()), std::make_unique(SymbolType::VAL, &decl, @@ -1963,6 +2170,12 @@ class CreateSymbols : public AstVisitor { getCurrentScope(), formal.getName())); } + void visitPre(ChanFormal &formal) { + symbolTable.insert(std::make_pair(getCurrentScope(), formal.getName()), + std::make_unique(SymbolType::CHAN, &formal, + getCurrentScope(), + formal.getName())); + } }; //===---------------------------------------------------------------------===// @@ -2361,6 +2574,8 @@ class CodeBuffer { void genADD() { genOPR(hexasm::Token::ADD); } void genSUB() { genOPR(hexasm::Token::SUB); } void genSVC() { genOPR(hexasm::Token::SVC); } + void genIN() { genOPR(hexasm::Token::IN); } + void genOUT() { genOPR(hexasm::Token::OUT); } /// Code generation visitors ---------------------------------------------/// @@ -2704,6 +2919,52 @@ class CodeBuffer { assert(0 && "unexpected target of assignment statement"); } } + + void visitPost(OutStatement &stmt) { + // Channel output c ! e: materialise e in areg, the channel slot in breg, + // then OPR OUT. + cb.genExpr(stmt.getValue(), currentScope); + cb.genExpr(stmt.getChannel(), currentScope, Reg::B); + cb.genOUT(); + } + + void visitPost(InStatement &stmt) { + if (auto *varRef = dynamic_cast(stmt.getTarget().get())) { + // Channel input c ? v: load the channel slot in breg, OPR IN leaves the + // received word in areg, then store it to the target variable. + cb.genExpr(stmt.getChannel(), currentScope, Reg::B); + cb.genIN(); + auto symbol = st.lookup(std::make_pair(currentScope, varRef->getName()), + stmt.getLocation()); + if (symbol->getScope().empty()) { + cb.genSTAM(symbol->getGlobalLabel()); + } else { + cb.genLDBM(SP_OFFSET); + cb.genSTAI_FB(cb.getCurrentFrame(), symbol->getStackOffset()); + } + } else if (auto *arraySub = dynamic_cast( + stmt.getTarget().get())) { + // Channel input into an array element c ? a[i]: compute the element + // address and stash it, perform the input, then store areg there. + cb.genExpr(arraySub->getExpr(), currentScope); + cb.genVar(Reg::B, + st.lookup(std::make_pair(currentScope, arraySub->getName()), + arraySub->getLocation())); + cb.genADD(); + auto stackOffset = cb.getCurrentFrame()->getOffset(); + cb.getCurrentFrame()->incOffset(1); + cb.genLDBM(SP_OFFSET); + cb.genSTAI_FB(cb.getCurrentFrame(), -stackOffset); + cb.genExpr(stmt.getChannel(), currentScope, Reg::B); + cb.genIN(); + cb.genLDBM(SP_OFFSET); + cb.genLDBI_FB(cb.getCurrentFrame(), -stackOffset); + cb.genSTAI(0); + cb.getCurrentFrame()->decOffset(1); + } else { + assert(0 && "unexpected target of channel input statement"); + } + } }; /// Generate code for an expression using the ExprCodeGen visitor. @@ -2985,6 +3246,9 @@ class FormalLocations : public AstVisitor { void visitPost(ArrayFormal &formal) { assignLocation(formal); } void visitPost(ProcFormal &formal) { assignLocation(formal); } void visitPost(FuncFormal &formal) { assignLocation(formal); } + // A channel formal is a one-word value (the link slot index), passed by + // value exactly like a val formal. + void visitPost(ChanFormal &formal) { assignLocation(formal); } }; /// Assign stack locations to local variables, starting from the base of the @@ -3371,6 +3635,156 @@ class ReportMemoryInfo : public AstVisitor { } }; +//===---------------------------------------------------------------------===// +// Static process network construction. +//===---------------------------------------------------------------------===// + +namespace network { + +/// "HEXN" network-container magic. Must match hexsim::NETWORK_MAGIC. +const uint32_t CONTAINER_MAGIC = 0x4E584548; + +/// A channel endpoint: the processor that touches a channel, the link slot it +/// assigned to it, and how it uses it. +struct Endpoint { + unsigned proc; + unsigned slot; + bool isWriter; + bool isReader; +}; + +/// A wiring edge connecting two processor link slots. +struct Edge { + uint32_t procA, slotA, procB, slotB; +}; + +/// One processor in the network: the entry proc it runs and how many channels +/// (link slots, assigned in argument order) it uses. +struct ProcessorInfo { + std::string entryProc; + unsigned numChannels; +}; + +struct Network { + std::vector processors; + std::vector edges; +}; + +/// Collect the names of channel formals used for output (writers) and input +/// (readers) within a procedure body. +class ChannelDirections : public AstVisitor { +public: + std::set writers; + std::set readers; + void visitPre(OutStatement &stmt) override { + if (auto *vr = dynamic_cast(stmt.getChannel().get())) { + writers.insert(vr->getName()); + } + } + void visitPre(InStatement &stmt) override { + if (auto *vr = dynamic_cast(stmt.getChannel().get())) { + readers.insert(vr->getName()); + } + } +}; + +/// Find a top-level procedure by name (nullptr if absent). +inline Proc *findProc(Program &program, const std::string &name) { + for (auto &proc : program.getProcDecls()) { + if (proc->getName() == name) { + return proc.get(); + } + } + return nullptr; +} + +/// Return main's statement if it is a top-level par, otherwise nullptr. +inline ParStatement *getTopLevelPar(Program &program) { + auto *mainProc = findProc(program, "main"); + if (!mainProc) { + return nullptr; + } + return dynamic_cast(mainProc->getStatement().get()); +} + +/// Analyse the static network described by main's par: one processor per +/// branch, channels wired by shared arguments and assigned link slots in +/// argument order. Validates endpoint counts, direction and the 4-link limit. +inline Network analyseNetwork(Program &program, ParStatement &par) { + Network net; + std::map> channels; + unsigned procIdx = 0; + for (auto &branch : par.getBranches()) { + // The parser guarantees each branch is a procedure call. + auto *call = static_cast(branch.get())->getCall(); + auto *entry = findProc(program, call->getName()); + if (!entry) { + throw NetworkError(call->getLocation(), + fmt::format("par branch calls unknown procedure {}", + call->getName())); + } + ChannelDirections dirs; + entry->getStatement()->accept(&dirs); + auto &args = call->getArgs(); + auto &formals = entry->getFormals(); + if (args.size() > 4) { + throw NetworkError(call->getLocation(), + fmt::format("process {} exceeds the 4-channel link " + "limit", + call->getName())); + } + if (args.size() != formals.size()) { + throw NetworkError( + call->getLocation(), + fmt::format("call to {} has the wrong number of arguments", + call->getName())); + } + for (unsigned argIdx = 0; argIdx < args.size(); argIdx++) { + auto *vr = dynamic_cast(args[argIdx].get()); + if (!vr) { + throw NetworkError(call->getLocation(), + "par branch arguments must be channels"); + } + const std::string &formalName = formals[argIdx]->getName(); + Endpoint ep; + ep.proc = procIdx; + ep.slot = argIdx; + ep.isWriter = dirs.writers.count(formalName) > 0; + ep.isReader = dirs.readers.count(formalName) > 0; + channels[vr->getName()].push_back(ep); + } + net.processors.push_back( + {call->getName(), static_cast(args.size())}); + procIdx++; + } + // Validate channels and build wiring edges. + for (auto &entry : channels) { + const std::string &name = entry.first; + auto &eps = entry.second; + if (eps.size() != 2) { + throw NetworkError( + par.getLocation(), + fmt::format("channel {} must connect exactly two processes", name)); + } + unsigned writers = 0; + unsigned readers = 0; + for (auto &ep : eps) { + writers += ep.isWriter ? 1 : 0; + readers += ep.isReader ? 1 : 0; + } + if (writers != 1 || readers != 1) { + throw NetworkError(par.getLocation(), + fmt::format("channel {} must have exactly one writer " + "and one reader", + name)); + } + net.edges.push_back({eps[0].proc, eps[0].slot, eps[1].proc, eps[1].slot}); + } + return net; +} + +} // namespace network + //===---------------------------------------------------------------------===// // Driver. //===---------------------------------------------------------------------===// @@ -3391,6 +3805,87 @@ class Driver { Parser parser; std::ostream &outStream; + /// Read a whole file into a string. + static std::string readFileToString(const std::string &filename) { + std::ifstream file(filename, std::ios::binary); + if (!file) { + throw std::runtime_error("could not open file: " + filename); + } + std::ostringstream buffer; + buffer << file.rdbuf(); + return buffer.str(); + } + + /// Compile a single processor image: re-parse the source, replace main with a + /// call to the processor's entry proc passing its link-slot indices (0..n-1) + /// as constants, run the full pipeline and return the image bytes. + std::string compileProcessorImage(const std::string &source, + const network::ProcessorInfo &proc) { + Lexer imageLexer; + Parser imageParser(imageLexer); + imageLexer.loadBuffer(source); + auto tree = imageParser.parseProgram(); + auto *mainProc = network::findProc(*tree, "main"); + Location loc = mainProc->getLocation(); + std::vector> args; + for (unsigned i = 0; i < proc.numChannels; i++) { + args.push_back(std::make_unique(loc, i)); + } + auto call = + std::make_unique(loc, proc.entryProc, std::move(args)); + mainProc->clearDecls(); + mainProc->setStatement( + std::make_unique(loc, std::move(call))); + + SymbolTable symbolTable; + CreateSymbols createSymbols(symbolTable); + tree->accept(&createSymbols); + ConstProp constProp(symbolTable); + tree->accept(&constProp); + OptimiseExpr optimiseExpr; + tree->accept(&optimiseExpr); + CodeGen codeGen(symbolTable); + tree->accept(&codeGen); + LowerDirectives lowerDirectives(symbolTable, codeGen); + OptimiseDirectives optimiseDirectives(symbolTable, + lowerDirectives.getCodeBuffer()); + hexasm::CodeGen asmCodeGen(optimiseDirectives.getInstrs()); + std::ostringstream out; + asmCodeGen.emitImage(out); + return out.str(); + } + + /// Emit a network container: magic, processor count, edges, then each + /// processor's image (size-prefixed standard single-image binary). + void emitNetworkContainer(const std::string &source, + const network::Network &net, + const std::string &filename) { + std::vector images; + for (auto &proc : net.processors) { + images.push_back(compileProcessorImage(source, proc)); + } + std::ofstream out(filename, std::ios::binary); + if (!out) { + throw std::runtime_error("could not open output file: " + filename); + } + auto writeU32 = [&](uint32_t value) { + out.write(reinterpret_cast(&value), sizeof(uint32_t)); + }; + writeU32(network::CONTAINER_MAGIC); + writeU32(static_cast(images.size())); + writeU32(static_cast(net.edges.size())); + for (auto &e : net.edges) { + writeU32(e.procA); + writeU32(e.slotA); + writeU32(e.procB); + writeU32(e.slotB); + } + for (auto &image : images) { + writeU32(static_cast(image.size())); + out.write(image.data(), image.size()); + } + } + public: Driver(std::ostream &outStream) : parser(lexer), outStream(outStream) {} @@ -3431,6 +3926,16 @@ class Driver { return 0; } + // A top-level par in main compiles to a multi-processor network container. + if (action == DriverAction::EMIT_BINARY) { + if (auto *par = network::getTopLevelPar(*tree)) { + auto net = network::analyseNetwork(*tree, *par); + std::string source = inputIsFilename ? readFileToString(input) : input; + emitNetworkContainer(source, net, outputBinaryFilename); + return 0; + } + } + // Optimise expressions. OptimiseExpr optimiseExpr; tree->accept(&optimiseExpr); diff --git a/xrun.cpp b/xrun.cpp index cae49a3..3bec37d 100644 --- a/xrun.cpp +++ b/xrun.cpp @@ -51,10 +51,10 @@ int main(int argc, char *argv[]) { } if (driver.runCatchExceptions(xcmp::DriverAction::EMIT_BINARY, inputFilename, true, "a.bin", false) == 0) { - hexsim::Processor processor(std::cin, std::cout, maxCycles); - processor.setTracing(trace); - processor.load("a.bin"); - processor.run(); + hexsim::System system(std::cin, std::cout, maxCycles); + system.setTracing(trace); + system.loadNetwork("a.bin"); + return system.run(); } } catch (const std::exception &e) { std::cerr << fmt::format("Error: {}\n", e.what());