From d379167073f5ebfac626b6eb7506ae539d6c1d69 Mon Sep 17 00:00:00 2001 From: James Hanlon Date: Mon, 25 May 2026 10:17:46 +0100 Subject: [PATCH 01/13] Add CMakePresets.json --- CMakePresets.json | 152 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 CMakePresets.json 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" + } + } + ] +} From 5bbf9d53b470f9bb48baf4cd28e080eceb60bee7 Mon Sep 17 00:00:00 2001 From: James Hanlon Date: Fri, 29 May 2026 09:38:42 +0100 Subject: [PATCH 02/13] Add IN/OUT operations to the Hex ISA enum --- hex.cpp | 4 ++++ hex.hpp | 2 +- tests/unit/dis_features.cpp | 8 ++++++++ 3 files changed, 13 insertions(+), 1 deletion(-) 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/tests/unit/dis_features.cpp b/tests/unit/dis_features.cpp index d52df3d..4ab5c95 100644 --- a/tests/unit/dis_features.cpp +++ b/tests/unit/dis_features.cpp @@ -79,6 +79,14 @@ 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] pfix_extended_operand") { TestContext ctx; // LDAC 32 requires PFIX 2, LDAC 0 -> operand becomes 0x20 = 32. From f3b59da54ce8c289a924fc549ea720a20edc295d Mon Sep 17 00:00:00 2001 From: James Hanlon Date: Fri, 29 May 2026 09:44:18 +0100 Subject: [PATCH 03/13] Assemble IN/OUT channel instructions --- hexasm.hpp | 16 ++++++++++++++-- tests/unit/dis_features.cpp | 8 ++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/hexasm.hpp b/hexasm.hpp index 2b842ea..dd84220 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); diff --git a/tests/unit/dis_features.cpp b/tests/unit/dis_features.cpp index 4ab5c95..094699d 100644 --- a/tests/unit/dis_features.cpp +++ b/tests/unit/dis_features.cpp @@ -87,6 +87,14 @@ TEST_CASE("[dis_features] in_out_opcodes") { 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. From e708b95caef4edbd8ce59e7597e2893563522237 Mon Sep 17 00:00:00 2001 From: James Hanlon Date: Fri, 29 May 2026 09:54:27 +0100 Subject: [PATCH 04/13] Add multi-core simulator with channel rendezvous Refactor Processor::run() into a step() method returning a StepResult (RUNNING/HALTED/BLOCKED) and extract image loading into loadFromStream(). Add a Channel type and per-processor link slots, and implement OPR IN/OUT as a synchronous point-to-point rendezvous: the second party to arrive transfers one word and unblocks the parked partner; a blocked operation does not advance the PC. Add a System class that loads a network container (magic HEXN, wiring edges, embedded per-processor images), boots all processors, and steps them round-robin until they all halt. Records the first processor's exit code, detects deadlock when no processor can proceed, and backstops the 4-link limit with an unwired-slot runtime error. A plain single image still loads via a fallback path. --- hexsim.hpp | 457 ++++++++++++++++++++++++++++-------- tests/unit/CMakeLists.txt | 3 +- tests/unit/sim_features.cpp | 183 +++++++++++++++ 3 files changed, 545 insertions(+), 98 deletions(-) create mode 100644 tests/unit/sim_features.cpp diff --git a/hexsim.hpp b/hexsim.hpp index c099e7b..6f882e3 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,333 @@ 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; + 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; } + + /// 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(false); + 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/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")); +} From c1d46297208a2b63db5448162ffa4dfd7a926aa0 Mon Sep 17 00:00:00 2001 From: James Hanlon Date: Fri, 29 May 2026 09:58:59 +0100 Subject: [PATCH 05/13] Run binaries through the multi-core System in hexsim --- hexsim.cpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) 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; From 6230a32a2b88fcf7f6cb523f86efc623066ccfbb Mon Sep 17 00:00:00 2001 From: James Hanlon Date: Fri, 29 May 2026 10:33:26 +0100 Subject: [PATCH 06/13] Lex chan, par, ! and ? tokens in the X language --- tests/unit/x_features.cpp | 10 ++++++++++ xcmp.hpp | 22 ++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/tests/unit/x_features.cpp b/tests/unit/x_features.cpp index af6ed46..06a2590 100644 --- a/tests/unit/x_features.cpp +++ b/tests/unit/x_features.cpp @@ -1453,3 +1453,13 @@ 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"); +} diff --git a/xcmp.hpp b/xcmp.hpp index 15fd69d..b423000 100644 --- a/xcmp.hpp +++ b/xcmp.hpp @@ -73,6 +73,10 @@ enum class Token { LE, GR, GE, + CHAN, + PAR, + PLING, + QUERY, END_OF_FILE }; @@ -158,6 +162,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: @@ -276,6 +288,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 +296,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 +451,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; From 887253c439d97c19ddd8fdbc4c570b27b403de4d Mon Sep 17 00:00:00 2001 From: James Hanlon Date: Fri, 29 May 2026 10:39:16 +0100 Subject: [PATCH 07/13] Parse chan declarations and formals in the X language Add ChanDecl and ChanFormal AST nodes (parsed as global/local decls and formal parameters), register them as CHAN symbols in CreateSymbols so channel references resolve, and print them in the AST printer. Also adds the visitor scaffolding for the par/in/out statement nodes that follow. Update token_error_unexpected_char to use '@' since '?' is now a valid token. --- tests/unit/x_features.cpp | 11 +++++- xcmp.hpp | 74 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 81 insertions(+), 4 deletions(-) diff --git a/tests/unit/x_features.cpp b/tests/unit/x_features.cpp index 06a2590..4f824e8 100644 --- a/tests/unit/x_features.cpp +++ b/tests/unit/x_features.cpp @@ -1257,7 +1257,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); } @@ -1463,3 +1463,12 @@ TEST_CASE("chan_par_pling_query_tokens") { 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); +} diff --git a/xcmp.hpp b/xcmp.hpp index b423000..540e0a0 100644 --- a/xcmp.hpp +++ b/xcmp.hpp @@ -611,6 +611,7 @@ class Expr; class ArrayDecl; class ValDecl; class VarDecl; +class ChanDecl; class BinaryOpExpr; class UnaryOpExpr; class StringExpr; @@ -624,6 +625,7 @@ class VarFormal; class ArrayFormal; class ProcFormal; class FuncFormal; +class ChanFormal; class SkipStatement; class StopStatement; class ReturnStatement; @@ -632,6 +634,9 @@ class WhileStatement; class SeqStatement; class CallStatement; class AssStatement; +class ParStatement; +class OutStatement; +class InStatement; /// A visitor base class for the AST. class AstVisitor { @@ -675,6 +680,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 &) {} @@ -703,6 +710,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 &) {} @@ -719,6 +728,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. @@ -938,6 +953,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; @@ -1013,6 +1037,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 { @@ -1257,6 +1290,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)); @@ -1346,6 +1384,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)); @@ -1598,6 +1642,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(); @@ -1621,7 +1671,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; @@ -1637,6 +1688,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()); } @@ -1672,6 +1724,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()); @@ -1799,7 +1854,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(); @@ -1837,7 +1893,7 @@ class Parser { // Symbol table. //===---------------------------------------------------------------------===// -enum class SymbolType { VAL, VAR, ARRAY, FUNC, PROC }; +enum class SymbolType { VAL, VAR, ARRAY, FUNC, PROC, CHAN }; class Symbol; @@ -1949,6 +2005,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, @@ -1985,6 +2047,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())); + } }; //===---------------------------------------------------------------------===// From c2a9cce8e97b1e77f6e5c46057cdcbbc4d286c47 Mon Sep 17 00:00:00 2001 From: James Hanlon Date: Fri, 29 May 2026 10:41:39 +0100 Subject: [PATCH 08/13] Parse the X par statement Add a ParStatement AST node holding a list of branch statements, each of which must be a procedure call (rejected with a clear error otherwise), printed by the AST printer. par { p() q() ... } composes one process per branch. --- tests/unit/x_features.cpp | 19 ++++++++++++++++++ xcmp.hpp | 42 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/tests/unit/x_features.cpp b/tests/unit/x_features.cpp index 4f824e8..fd10c69 100644 --- a/tests/unit/x_features.cpp +++ b/tests/unit/x_features.cpp @@ -1472,3 +1472,22 @@ TEST_CASE("chan_declaration_and_formal_tree") { 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); +} diff --git a/xcmp.hpp b/xcmp.hpp index 540e0a0..0aeff05 100644 --- a/xcmp.hpp +++ b/xcmp.hpp @@ -1188,6 +1188,23 @@ 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; } +}; + // Procedures and functions ================================================= // class Proc : public AstNode { @@ -1442,6 +1459,12 @@ 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--; }; }; //===---------------------------------------------------------------------===// @@ -1800,6 +1823,25 @@ class Parser { expect(Token::END); return std::make_unique(location, std::move(body)); } + case Token::PAR: { + lexer.getNextToken(); + expect(Token::BEGIN); + std::vector> branches; + while (lexer.getLastToken() != Token::END) { + auto branchLocation = lexer.getLocation(); + auto branch = parseStatement(); + // Each par branch must be a procedure call (the entry process for one + // processor). + if (!dynamic_cast(branch.get())) { + throw ParserTokenError(branchLocation, + "par branch must be a procedure call", + lexer.getLastToken()); + } + branches.push_back(std::move(branch)); + } + expect(Token::END); + return std::make_unique(location, std::move(branches)); + } case Token::IDENTIFIER: { auto element = parseElement(); // Procedure call From d0c2618480bd5d2fa3123367985aec6233fe6477 Mon Sep 17 00:00:00 2001 From: James Hanlon Date: Fri, 29 May 2026 10:44:46 +0100 Subject: [PATCH 09/13] Parse X channel output (!) and input (?) statements Add OutStatement (channel ! expr) and InStatement (channel ? lvalue) AST nodes, parsed in statement position after an identifier element, and printed by the AST printer. Includes a reused-worker ring tree test exercising chan/par/!/? together. --- tests/unit/x_features.cpp | 47 +++++++++++++++++++++++++++ xcmp.hpp | 68 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+) diff --git a/tests/unit/x_features.cpp b/tests/unit/x_features.cpp index fd10c69..535bd8a 100644 --- a/tests/unit/x_features.cpp +++ b/tests/unit/x_features.cpp @@ -1491,3 +1491,50 @@ TEST_CASE("par_branch_must_be_call_error") { "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); +} diff --git a/xcmp.hpp b/xcmp.hpp index 0aeff05..7589768 100644 --- a/xcmp.hpp +++ b/xcmp.hpp @@ -1205,6 +1205,50 @@ class ParStatement : public Statement { 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 { @@ -1465,6 +1509,18 @@ class AstPrinter : public AstVisitor { 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--; }; }; //===---------------------------------------------------------------------===// @@ -1850,6 +1906,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), From 4f8b1fed183c6f5c7ad3f57639ccf7ca38aee022 Mon Sep 17 00:00:00 2001 From: James Hanlon Date: Fri, 29 May 2026 11:04:02 +0100 Subject: [PATCH 10/13] Generate code for chan formals and !/? statements Treat a chan formal as a one-word val parameter (the link slot index) in FormalLocations, and generate OPR OUT for c ! e (value in areg, slot in breg) and OPR IN for c ? target (slot in breg, received word stored to a variable or array element). --- tests/unit/x_features.cpp | 18 ++++++++++++++ xcmp.hpp | 51 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/tests/unit/x_features.cpp b/tests/unit/x_features.cpp index 535bd8a..425d6a4 100644 --- a/tests/unit/x_features.cpp +++ b/tests/unit/x_features.cpp @@ -1538,3 +1538,21 @@ TEST_CASE("message_passing_ring_tree") { 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); +} diff --git a/xcmp.hpp b/xcmp.hpp index 7589768..726b750 100644 --- a/xcmp.hpp +++ b/xcmp.hpp @@ -2561,6 +2561,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 ---------------------------------------------/// @@ -2904,6 +2906,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. @@ -3185,6 +3233,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 From 95d0562fedd14c019aaada945ff137fcbc3d2a5d Mon Sep 17 00:00:00 2001 From: James Hanlon Date: Fri, 29 May 2026 11:39:36 +0100 Subject: [PATCH 11/13] Compile a top-level par into a multi-processor network container Add the X compiler back-end for message passing. A program whose main is a top-level par compiles to a network container instead of a single image: - network::analyseNetwork builds the static process graph from main's par (one processor per branch), assigns each processor's channels to link slots in argument order, and validates endpoint counts, writer/reader direction and the 4-link limit (NetworkError). - Each processor image is produced by re-parsing the source and replacing main with a call to the branch's entry proc passing its slot indices as constants, then running the existing single-core pipeline. chan formals compile as val parameters, so proc bodies stay generic and reusable. - hexasm::CodeGen::emitImage emits a full image (size word + program + debug) to a stream so images can be embedded in the container. - The container is loaded and run by hexsim::System; xrun and the unit test harness run via System (single images use the loader fallback). Adds tests/x/pipe.x and end-to-end tests covering a pipeline, a relay chain, runtime deadlock detection and the network validation errors. --- hexasm.hpp | 12 +- tests/unit/TestContext.hpp | 11 +- tests/unit/x_features.cpp | 61 +++++++++ tests/x/pipe.x | 18 +++ xcmp.hpp | 256 +++++++++++++++++++++++++++++++++++++ xrun.cpp | 8 +- 6 files changed, 352 insertions(+), 14 deletions(-) create mode 100644 tests/x/pipe.x diff --git a/hexasm.hpp b/hexasm.hpp index dd84220..56b14b0 100644 --- a/hexasm.hpp +++ b/hexasm.hpp @@ -974,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), @@ -984,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/tests/unit/TestContext.hpp b/tests/unit/TestContext.hpp index 3835670..9cca243 100644 --- a/tests/unit/TestContext.hpp +++ b/tests/unit/TestContext.hpp @@ -183,12 +183,11 @@ 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.loadNetwork(path.c_str()); + return system.run(); } /// Run an X program from a file. diff --git a/tests/unit/x_features.cpp b/tests/unit/x_features.cpp index 425d6a4..3a49ae1 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 @@ -1556,3 +1557,63 @@ TEST_CASE("in_statement_codegen") { .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..cc60c01 --- /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 726b750..77ee52f 100644 --- a/xcmp.hpp +++ b/xcmp.hpp @@ -12,6 +12,8 @@ #include #include #include +#include +#include #include #include #include @@ -249,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 //===---------------------------------------------------------------------===// @@ -1284,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 { @@ -1306,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 ===================================================== // @@ -3622,6 +3637,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. //===---------------------------------------------------------------------===// @@ -3642,6 +3807,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) {} @@ -3682,6 +3928,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()); From e2aa9b51c124c5155decbec48a9a49385a998a38 Mon Sep 17 00:00:00 2001 From: James Hanlon Date: Sat, 30 May 2026 09:12:55 +0100 Subject: [PATCH 12/13] Default System inputs to truncated, fixing the standalone simulator hexsim now runs via System, which hardcoded sign-extended char inputs. xhexb.x relies on truncated chars, so EOF read back as 0xFFFFFFFF instead of 0xFF and was reported as an illegal character (breaking tests.py:test_x_compiler_sim). Make System::setTruncateInputs configurable, defaulting to true (matching the hardware/xhexb); TestContext opts into sign-extension for its negative-value tests. --- hexsim.hpp | 6 +++++- tests/unit/TestContext.hpp | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/hexsim.hpp b/hexsim.hpp index 6f882e3..bb15ddc 100644 --- a/hexsim.hpp +++ b/hexsim.hpp @@ -506,6 +506,9 @@ class System { 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; @@ -514,6 +517,7 @@ class System { : 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). @@ -628,7 +632,7 @@ class System { auto p = std::make_unique(in, out, maxCycles); p->setId(id); p->setTracing(tracing); - p->setTruncateInputs(false); + p->setTruncateInputs(truncateInputs); p->loadFromStream(image, imageSize); procs.push_back(std::move(p)); } diff --git a/tests/unit/TestContext.hpp b/tests/unit/TestContext.hpp index 9cca243..86122ca 100644 --- a/tests/unit/TestContext.hpp +++ b/tests/unit/TestContext.hpp @@ -186,6 +186,7 @@ struct TestContext { // 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(); } From b1ef55d49f749c698657cee9093d111724780fed Mon Sep 17 00:00:00 2001 From: James Hanlon Date: Sat, 30 May 2026 09:22:44 +0100 Subject: [PATCH 13/13] Require semicolons between par branches par { a(); b(); c() } now separates branches with ; like the other { } blocks, by reusing parseStatements. Updates the tests and pipe.x. --- tests/unit/x_features.cpp | 14 +++++++------- tests/x/pipe.x | 2 +- xcmp.hpp | 16 +++++++--------- 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/tests/unit/x_features.cpp b/tests/unit/x_features.cpp index 3a49ae1..5d9939d 100644 --- a/tests/unit/x_features.cpp +++ b/tests/unit/x_features.cpp @@ -1479,7 +1479,7 @@ TEST_CASE("par_statement_tree") { 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) }") + "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); @@ -1489,7 +1489,7 @@ TEST_CASE("par_statement_tree") { 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) }"; + "proc main() is chan a; par { skip; sink(a) }"; REQUIRE_THROWS_AS(ctx.treeXProgramSrc(program), xcmp::ParserTokenError); } @@ -1529,7 +1529,7 @@ TEST_CASE("message_passing_ring_tree") { "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) }") + "par { worker(a, b); worker(b, a) }") .str(); REQUIRE(output.find("parstmt") != std::string::npos); REQUIRE(output.find("instmt") != std::string::npos); @@ -1565,7 +1565,7 @@ TEST_CASE("message_passing_run_pipeline") { "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) }"; + "proc main() is chan c; par { source(c); sink(c) }"; REQUIRE(ctx.runXProgramSrc(program) == 0); REQUIRE(ctx.simOutBuffer.str() == "A"); } @@ -1580,7 +1580,7 @@ TEST_CASE("message_passing_run_relay") { "{ 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) }"; + "par { source(a); relay(a, b); sink(b) }"; REQUIRE(ctx.runXProgramSrc(program) == 0); REQUIRE(ctx.simOutBuffer.str() == "Z"); } @@ -1591,7 +1591,7 @@ TEST_CASE("message_passing_run_deadlock") { 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) }"; + "par { worker(a, b); worker(b, a) }"; REQUIRE_THROWS_WITH(ctx.runXProgramSrc(program), Catch::Matchers::ContainsSubstring("deadlock")); } @@ -1608,7 +1608,7 @@ 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) }"; + "proc main() is chan a; par { reader(a); reader(a) }"; REQUIRE_THROWS_AS(ctx.asmXProgramSrc(program), xcmp::NetworkError); } diff --git a/tests/x/pipe.x b/tests/x/pipe.x index cc60c01..67d650a 100644 --- a/tests/x/pipe.x +++ b/tests/x/pipe.x @@ -15,4 +15,4 @@ proc sink(chan in) is proc main() is chan a; chan b; - par { source(a) relay(a, b) sink(b) } + par { source(a); relay(a, b); sink(b) } diff --git a/xcmp.hpp b/xcmp.hpp index 77ee52f..aec478a 100644 --- a/xcmp.hpp +++ b/xcmp.hpp @@ -1897,20 +1897,18 @@ class Parser { case Token::PAR: { lexer.getNextToken(); expect(Token::BEGIN); - std::vector> branches; - while (lexer.getLastToken() != Token::END) { - auto branchLocation = lexer.getLocation(); - auto branch = parseStatement(); - // Each par branch must be a procedure call (the entry process for one - // processor). + // 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(branchLocation, + throw ParserTokenError(branch->getLocation(), "par branch must be a procedure call", lexer.getLastToken()); } - branches.push_back(std::move(branch)); } - expect(Token::END); return std::make_unique(location, std::move(branches)); } case Token::IDENTIFIER: {