Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .github/workflows/simulator_tests.yml
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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 = [];
Expand Down Expand Up @@ -843,6 +875,8 @@ function allocatePruRegisters() {
moduleInstanceRegisterMap[instanceName] = {
"conditionCalculated": 0,
"label": instanceName,
"mergeLabel": `${instanceName}_END`,
"mergeScheduled": false,
"numOfBytesReqByOutput1": 0,
"peakCycles": 0,
"moduleName": moduleName,
Expand Down Expand Up @@ -1157,4 +1191,4 @@ exports = {
getGroupMetadata: () => groupMetadata,
getRegisterAllocationComments,
getBlockRegisterAllocation
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
`;
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -311,4 +311,4 @@ exports = {
}]
},
},
}
}
80 changes: 80 additions & 0 deletions scripts/test_headless_sim.js
Original file line number Diff line number Diff line change
@@ -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");