From 5c284e3f45aa9dd62ccc8dbc431d7724e5e3a059 Mon Sep 17 00:00:00 2001 From: Pratheesh Gangadhar Date: Thu, 30 Jul 2026 09:10:27 +0530 Subject: [PATCH] fix(codegen): keep conditional branches exclusive --- .github/workflows/simulator_tests.yml | 30 +++++++ .../pru_register_allocator.js | 36 ++++++++- .../conditional_block.syscfg.js | 6 +- scripts/test_headless_sim.js | 80 +++++++++++++++++++ 4 files changed, 148 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/simulator_tests.yml create mode 100644 scripts/test_headless_sim.js diff --git a/.github/workflows/simulator_tests.yml b/.github/workflows/simulator_tests.yml new file mode 100644 index 0000000..deb53fb --- /dev/null +++ b/.github/workflows/simulator_tests.yml @@ -0,0 +1,30 @@ +name: Simulator Tests + +on: + pull_request: + paths: + - ".metadata/sysconfig/.meta/pru_blocks/common/simulation/**" + - ".metadata/sysconfig/.meta/pru_blocks/common/register_allocation/**" + - "scripts/test_headless_sim.js" + - ".github/workflows/simulator_tests.yml" + push: + branches: [main, master] + paths: + - ".metadata/sysconfig/.meta/pru_blocks/common/simulation/**" + - ".metadata/sysconfig/.meta/pru_blocks/common/register_allocation/**" + - "scripts/test_headless_sim.js" + - ".github/workflows/simulator_tests.yml" + workflow_dispatch: + +permissions: + contents: read + +jobs: + headless: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + - run: node scripts/test_headless_sim.js diff --git a/.metadata/sysconfig/.meta/pru_blocks/common/register_allocation/pru_register_allocator.js b/.metadata/sysconfig/.meta/pru_blocks/common/register_allocation/pru_register_allocator.js index d224604..ac38909 100644 --- a/.metadata/sysconfig/.meta/pru_blocks/common/register_allocation/pru_register_allocator.js +++ b/.metadata/sysconfig/.meta/pru_blocks/common/register_allocation/pru_register_allocator.js @@ -23,6 +23,8 @@ let cycleBudgetMap = new Map(); let groupMetadata; let loopCounterRegisters = {}; // Track loop counters: {instanceName: {byteOffset, numBytes}} let groupReturnAddrByteOffset = -1; // Single allocated return address register for all group blocks +let pushInstructionDepth = 0; +let pendingConditionalMergeLabels = []; /** * Returns PRU registers allocation summary @@ -463,6 +465,23 @@ function collectInputPredecessors(instance) { * @returns {number|string} Number of bytes allocated to output port 1 if available, label for conditional blocks, or -1 if allocation failed */ function pushInstruction(instance, parentInstance) { + pushInstructionDepth++; + try { + return pushInstructionImpl(instance, parentInstance); + } finally { + pushInstructionDepth--; + if (pushInstructionDepth === 0) { + for (const pending of pendingConditionalMergeLabels) { + addToPruRegisterAllocationSummary( + pending.label, "0", pending.instance, pending.instance.$name, 0 + ); + } + pendingConditionalMergeLabels = []; + } + } +} + +function pushInstructionImpl(instance, parentInstance) { // Early return if instance is null if (instance === null) { return; @@ -496,6 +515,17 @@ function pushInstruction(instance, parentInstance) { // label line now — before any input processing — so that all inputs // for this branch are emitted AFTER the label, not before it. if (typeof label === "string" && label !== "") { + const conditionalState = moduleInstanceRegisterMap[prevPortInstanceName]; + if (conditionalState?.mergeLabel && !conditionalState.mergeScheduled) { + addToPruRegisterAllocationSummary( + "", `QBA ${conditionalState.mergeLabel}`, instance, instanceName, 1 + ); + pendingConditionalMergeLabels.push({ + label: conditionalState.mergeLabel, + instance: instance["prev"][0]["inst"] + }); + conditionalState.mergeScheduled = true; + } addToPruRegisterAllocationSummary(label, "0", instance, instanceName, 0); label = 0; } @@ -793,6 +823,8 @@ function allocatePruRegisters() { pruByteArray = new Array(totalBytes).fill(0); loopCounterRegisters = {}; // Reset loop counter tracking groupReturnAddrByteOffset = -1; // Reset group return address register + pushInstructionDepth = 0; + pendingConditionalMergeLabels = []; let loopBlockInstanceNames = []; let groupBlockInstanceNames = []; groupMetadata = []; @@ -843,6 +875,8 @@ function allocatePruRegisters() { moduleInstanceRegisterMap[instanceName] = { "conditionCalculated": 0, "label": instanceName, + "mergeLabel": `${instanceName}_END`, + "mergeScheduled": false, "numOfBytesReqByOutput1": 0, "peakCycles": 0, "moduleName": moduleName, @@ -1157,4 +1191,4 @@ exports = { getGroupMetadata: () => groupMetadata, getRegisterAllocationComments, getBlockRegisterAllocation -} \ No newline at end of file +} diff --git a/.metadata/sysconfig/.meta/pru_blocks/program_control/conditional_block.syscfg.js b/.metadata/sysconfig/.meta/pru_blocks/program_control/conditional_block.syscfg.js index e93f8ae..ab07ef5 100644 --- a/.metadata/sysconfig/.meta/pru_blocks/program_control/conditional_block.syscfg.js +++ b/.metadata/sysconfig/.meta/pru_blocks/program_control/conditional_block.syscfg.js @@ -99,7 +99,7 @@ scripting.connect(prev_block, "next", if_else1, "prev"); 5. **Port Names**: True path uses "T" port, False path uses "F" port (displayed as t_next/f_next). -6. **Terminate Each Branch with a Flow Control Block**: The code generator places the FALSE path immediately after the branch instruction, with the TRUE path at the branch target label. If the FALSE path has no explicit terminator, execution falls through into the TRUE path — causing both branches to execute regardless of the condition. Always end each branch (T and F) with a Flow Control block (HALT or END) to prevent this fall-through. +6. **Exclusive Branches**: The code generator inserts a merge jump between the fall-through and target branches, so exactly one connected branch executes. Flow Control blocks are only needed when a branch should halt or end the program. `; } @@ -170,7 +170,7 @@ Implements conditional logic (IF/ELSE statements) to control program flow based - The conditional check happens instantly (1 cycle) - Code on both branches is generated, only one path executes at runtime - This block does not produce an output value - it only controls flow -- **Always terminate each branch (T and F) with a Flow Control block**: The FALSE path falls through to the TRUE path in the generated assembly unless explicitly stopped. Without a terminator on the FALSE branch, both branches execute sequentially regardless of the condition result. +- The generator emits an unconditional jump to a merge label after the fall-through branch, preventing execution from continuing into the other branch. ### Terminology - **Conditional branching**: Changing program flow based on a condition @@ -311,4 +311,4 @@ exports = { }] }, }, -} \ No newline at end of file +} diff --git a/scripts/test_headless_sim.js b/scripts/test_headless_sim.js new file mode 100644 index 0000000..00841e7 --- /dev/null +++ b/scripts/test_headless_sim.js @@ -0,0 +1,80 @@ +#!/usr/bin/env node + +const fs = require("fs"); +const path = require("path"); +const vm = require("vm"); + +const simDir = path.join( + __dirname, + "../.metadata/sysconfig/.meta/pru_blocks/common/simulation" +); +const cache = {}; + +function loadScript(file) { + const sandbox = { + Array, + JSON, + Math, + Uint8Array, + console, + eval, + isNaN, + parseInt, + exports: {}, + system: { + getScript(modulePath) { + const name = path.basename(modulePath); + cache[name] ||= loadScript(path.join(simDir, name)); + return cache[name]; + } + } + }; + vm.createContext(sandbox); + vm.runInContext(fs.readFileSync(file, "utf8"), sandbox, { filename: file }); + return sandbox.exports; +} + +const pruCore = loadScript(path.join(simDir, "pru_core.js")); + +function run(lines, cycles = 20) { + const instructions = []; + const labels = []; + for (const sourceLine of lines) { + const line = sourceLine.trim(); + if (line.endsWith(":")) { + instructions.push("0"); + labels.push(line.slice(0, -1)); + } else { + instructions.push(line); + labels.push(0); + } + } + return pruCore.simulatePruInstructions(instructions, labels, cycles).pruState; +} + +function verifyConditionalMerge() { + const program = (left, right) => [ + `ldi R0, ${left}`, + `ldi R1, ${right}`, + // PRU QBGT branches when operand 2 is greater than operand 1. + "qbgt true_branch, R1, R0", + "add R2, R2, 1", + "qba conditional_end", + "true_branch:", + "add R3, R3, 1", + "conditional_end:", + "halt" + ]; + + const taken = run(program(10, 5)); + const fallthrough = run(program(5, 10)); + if (taken.registers[2] !== 0 || taken.registers[3] !== 1) { + throw new Error("taken path executed more than the true branch"); + } + if (fallthrough.registers[2] !== 1 || fallthrough.registers[3] !== 0) { + throw new Error("fall-through path executed more than the false branch"); + } +} + +verifyConditionalMerge(); +console.log("PASS conditional merge: true and false paths remain exclusive");