Skip to content

Commit bd021da

Browse files
author
NEXUS Bot
committed
Sprint 0.3: Bytecode VM — 32 opcodes, executor loop, disassembler, validator
- Complete 32-opcode stack machine interpreter - 8-byte fixed instruction format (packed struct) - All arithmetic: ADD/SUB/MUL/DIV/NEG/ABS/MIN/MAX/CLAMP - Comparisons: EQ/LT/GT/LTE/GTE (float) - Logic: AND/OR/XOR/NOT (bitwise) - I/O: READ_PIN, WRITE_PIN, READ_TIMER_MS - Control: JUMP, JUMP_IF_FALSE, JUMP_IF_TRUE, CALL/RET - Syscalls: HALT, PID_COMPUTE, RECORD_SNAPSHOT, EMIT_EVENT - Instruction encoder helpers - Disassembler (human-readable output) - Bytecode validator (jump bounds, alignment, opcode range) - NaN/Infinity guards on actuator registers - Zero-heap: all memory statically allocated (5312 bytes) - CLAMP_F uses float16 encoding for two bounds in operand2 - PID controller with anti-windup integral clamping - 52 passing unit tests (all 10 spec vectors + 42 additional)
1 parent ac73c07 commit bd021da

File tree

6 files changed

+2387
-152
lines changed

6 files changed

+2387
-152
lines changed

firmware/nexus_vm/include/vm.h

Lines changed: 50 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
11
/**
22
* @file vm.h
3-
* @brief NEXUS Bytecode VM - Core data structures and constants.
43
*
54
* Stack machine with 256-entry stack, float32 arithmetic,
65
* 8-byte fixed instructions, and deterministic execution.
76
* Zero heap allocation - all memory is statically sized.
7+
#include <stdint.h>
8+
*
9+
* VM state total: ~5.4 KB (well within 512KB SRAM budget on ESP32-S3).
810
*/
911

1012
#ifndef NEXUS_VM_H
1113
#define NEXUS_VM_H
1214

1315
#include <stdint.h>
1416
#include <stdbool.h>
15-
#include <string.h>
17+
#include <stddef.h>
1618
#include "instruction.h"
1719

1820
#ifdef __cplusplus
@@ -31,7 +33,8 @@ extern "C" {
3133
#define VM_PID_COUNT 8
3234
#define VM_SNAPSHOT_COUNT 16
3335
#define VM_EVENT_RING_SIZE 32
34-
#define VM_MAX_CYCLE_BUDGET 100000
36+
#define VM_MAX_CYCLE_BUDGET 10000
37+
#define VM_MAX_BYTECODE_SIZE (100 * 1024)
3538

3639
/* ===================================================================
3740
* Error Codes
@@ -50,22 +53,18 @@ typedef enum {
5053
ERR_INVALID_SYSCALL,
5154
ERR_INVALID_PID,
5255
ERR_DIVISION_BY_ZERO,
53-
ERR_NAN_IN_ACTUATOR,
5456
} vm_error_t;
5557

5658
/* ===================================================================
5759
* PID Controller State (32 bytes per instance)
5860
* =================================================================== */
5961

6062
typedef struct {
61-
float kp;
62-
float ki;
63-
float kd;
63+
float Kp, Ki, Kd;
6464
float integral;
6565
float prev_error;
6666
float integral_limit;
67-
float output_min;
68-
float output_max;
67+
float output_min, output_max;
6968
} pid_state_t;
7069

7170
/* ===================================================================
@@ -90,96 +89,87 @@ typedef struct {
9089
uint16_t event_data;
9190
} vm_event_t;
9291

93-
/* ===================================================================
94-
* Call Stack Entry (6 bytes)
95-
* =================================================================== */
96-
97-
typedef struct {
98-
uint32_t return_addr;
99-
uint16_t frame_pointer;
100-
} vm_call_frame_t;
101-
10292
/* ===================================================================
10393
* VM State - Complete interpreter state
10494
* =================================================================== */
10595

10696
typedef struct {
107-
/* Data stack */
10897
uint32_t stack[VM_STACK_SIZE];
10998
uint16_t sp;
110-
111-
/* Program counter */
11299
uint32_t pc;
113-
114-
/* Memory regions */
115100
uint32_t vars[VM_VAR_COUNT];
116101
uint32_t sensors[VM_SENSOR_COUNT];
117102
uint32_t actuators[VM_ACTUATOR_COUNT];
118-
119-
/* Flags and counters */
120103
uint32_t flags;
121104
uint32_t cycle_count;
122105
uint32_t cycle_budget;
123106
uint32_t tick_count_ms;
124107
float tick_period_sec;
125108

126-
/* Call stack */
127-
vm_call_frame_t call_stack[VM_CALL_STACK_SIZE];
109+
struct {
110+
uint32_t return_addr;
111+
uint16_t frame_pointer;
112+
} call_stack[VM_CALL_STACK_SIZE];
128113
uint16_t csp;
129114

130-
/* PID controllers */
131115
pid_state_t pid[VM_PID_COUNT];
132116

133-
/* Snapshots */
134117
vm_snapshot_t snapshots[VM_SNAPSHOT_COUNT];
135118
uint8_t next_snapshot;
136119

137-
/* Event ring buffer */
138120
vm_event_t events[VM_EVENT_RING_SIZE];
139121
uint16_t event_head;
140122
uint16_t event_tail;
141123

142-
/* State */
143124
vm_error_t last_error;
144125
bool halted;
145126
const uint8_t *bytecode;
146127
uint32_t bytecode_size;
147128
} vm_state_t;
148129

130+
_Static_assert(sizeof(vm_state_t) < 6000, "vm_state_t must be under 6KB");
131+
149132
/* ===================================================================
150-
* VM API Functions
133+
* Core VM API
151134
* =================================================================== */
152135

153-
/**
154-
* @brief Initialize VM state to safe defaults.
155-
* @param vm Pointer to VM state structure.
156-
* @return VM_OK on success, error code on failure.
157-
*/
158-
vm_error_t vm_init(vm_state_t *vm);
159-
160-
/**
161-
* @brief Load bytecode into the VM.
162-
* @param vm Pointer to VM state.
163-
* @param bytecode Pointer to bytecode buffer.
164-
* @param size Size of bytecode in bytes (must be multiple of 8).
165-
* @return VM_OK on success, error code on failure.
166-
*/
167-
vm_error_t vm_load_bytecode(vm_state_t *vm, const uint8_t *bytecode, uint32_t size);
136+
vm_error_t vm_init(vm_state_t *vm, const uint8_t *bytecode, uint32_t size);
137+
vm_error_t vm_execute(vm_state_t *vm);
138+
vm_error_t vm_tick(vm_state_t *vm);
139+
vm_error_t vm_set_sensor(vm_state_t *vm, uint8_t idx, float value);
140+
float vm_get_actuator(vm_state_t *vm, uint8_t idx);
141+
vm_error_t vm_execute_instruction(vm_state_t *vm, const nexus_instruction_t *instr);
142+
vm_error_t vm_validate(const uint8_t *bytecode, uint32_t size);
143+
void vm_disassemble(const uint8_t *bytecode, uint32_t size, char *out, size_t out_max);
168144

169-
/**
170-
* @brief Execute one VM tick (fetch-decode-execute loop).
171-
* @param vm Pointer to VM state.
172-
* @return VM_OK on normal completion, error on fault.
173-
*/
174-
vm_error_t vm_execute_tick(vm_state_t *vm);
145+
/* ===================================================================
146+
* Instruction Encoder Helpers
147+
* =================================================================== */
175148

176-
/**
177-
* @brief Validate bytecode before execution.
178-
* @param bytecode Pointer to bytecode buffer.
179-
* @param size Size of bytecode in bytes.
180-
* @return VM_OK if valid, error code if invalid.
181-
*/
182-
vm_error_t vm_validate_bytecode(const uint8_t *bytecode, uint32_t size);
149+
void encode_instruction(uint8_t *buf, uint8_t opcode, uint8_t flags,
150+
uint16_t operand1, uint32_t operand2);
151+
void encode_push_f32(uint8_t *buf, float value);
152+
void encode_push_i8(uint8_t *buf, int8_t value);
153+
void encode_push_i16(uint8_t *buf, int16_t value);
154+
void encode_jump(uint8_t *buf, uint32_t target, bool is_call);
155+
void encode_ret(uint8_t *buf);
156+
void encode_syscall(uint8_t *buf, uint8_t syscall_id);
157+
void encode_pid_compute(uint8_t *buf, uint8_t pid_index);
158+
void encode_record_snapshot(uint8_t *buf);
159+
void encode_emit_event(uint8_t *buf, uint16_t event_id, uint16_t event_data);
160+
void encode_clamp_f(uint8_t *buf, float lo, float hi);
161+
void encode_read_pin(uint8_t *buf, uint16_t pin);
162+
void encode_write_pin(uint8_t *buf, uint16_t pin);
163+
void encode_halt(uint8_t *buf);
164+
void encode_nop(uint8_t *buf);
165+
void encode_pop(uint8_t *buf);
166+
void encode_dup(uint8_t *buf);
167+
void encode_swap(uint8_t *buf);
168+
void encode_rot(uint8_t *buf);
169+
void encode_read_timer_ms(uint8_t *buf);
170+
void encode_jump_if_true(uint8_t *buf, uint32_t target);
171+
void encode_jump_if_false(uint8_t *buf, uint32_t target);
172+
void encode_alu(uint8_t *buf, uint8_t opcode);
183173

184174
#ifdef __cplusplus
185175
}

firmware/nexus_vm/vm_core.c

Lines changed: 126 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,161 @@
11
/**
22
* @file vm_core.c
3-
* @brief NEXUS Bytecode VM - Fetch-decode-execute loop (stub).
3+
* @brief NEXUS Bytecode VM - Fetch-decode-execute loop and core API.
44
*
5-
* Implements the core interpreter loop that fetches instructions,
6-
* decodes opcodes, and dispatches to per-opcode implementations.
5+
* Implements vm_init, vm_execute, vm_tick, vm_set_sensor, vm_get_actuator.
6+
* The execute loop fetches 8-byte instructions, dispatches to the
7+
* per-opcode handler in vm_opcodes.c, and manages cycle budgets.
78
*/
89

910
#include "vm.h"
11+
#include "opcodes.h"
1012
#include <string.h>
1113

12-
vm_error_t vm_init(vm_state_t *vm) {
13-
if (!vm) {
14-
return ERR_INVALID_OPERAND;
15-
}
16-
memset(vm, 0, sizeof(vm_state_t));
17-
vm->cycle_budget = VM_MAX_CYCLE_BUDGET;
18-
vm->bytecode = NULL;
19-
vm->bytecode_size = 0;
20-
vm->halted = false;
21-
vm->last_error = VM_OK;
22-
return VM_OK;
14+
/* ===================================================================
15+
* Helpers
16+
* =================================================================== */
17+
18+
/** Check if a float (as uint32 bits) is NaN or Infinity. */
19+
static inline bool vm_is_nan_or_inf_u32(uint32_t bits) {
20+
return (bits & 0x7F800000u) == 0x7F800000u;
21+
}
22+
23+
static inline bool vm_is_nan_or_inf(float f) {
24+
uint32_t bits;
25+
memcpy(&bits, &f, sizeof(bits));
26+
return vm_is_nan_or_inf_u32(bits);
2327
}
2428

25-
vm_error_t vm_load_bytecode(vm_state_t *vm, const uint8_t *bytecode, uint32_t size) {
26-
if (!vm || !bytecode) {
29+
/* ===================================================================
30+
* vm_init - Initialize VM state and optionally load bytecode
31+
* =================================================================== */
32+
33+
vm_error_t vm_init(vm_state_t *vm, const uint8_t *bytecode, uint32_t size) {
34+
if (!vm) {
2735
return ERR_INVALID_OPERAND;
2836
}
29-
if (size % 8 != 0) {
37+
if (bytecode != NULL && size > 0 && size % 8 != 0) {
3038
return ERR_INVALID_OPERAND;
3139
}
40+
41+
memset(vm, 0, sizeof(vm_state_t));
42+
vm->cycle_budget = VM_MAX_CYCLE_BUDGET;
3243
vm->bytecode = bytecode;
3344
vm->bytecode_size = size;
34-
vm->pc = 0;
3545
vm->halted = false;
3646
vm->last_error = VM_OK;
3747
return VM_OK;
3848
}
3949

40-
vm_error_t vm_execute_tick(vm_state_t *vm) {
50+
/* ===================================================================
51+
* vm_execute - Main fetch-decode-execute loop
52+
*
53+
* Runs until halted, error, cycle budget exceeded, or PC falls off
54+
* the end of bytecode. Each instruction consumes exactly 1 cycle
55+
* budget unit (regardless of opcode timing).
56+
* =================================================================== */
57+
58+
vm_error_t vm_execute(vm_state_t *vm) {
4159
if (!vm || !vm->bytecode) {
4260
return ERR_INVALID_OPERAND;
4361
}
4462

45-
vm->cycle_count = 0;
46-
vm->halted = false;
47-
4863
while (!vm->halted && vm->cycle_count < vm->cycle_budget) {
49-
/* TODO: Fetch instruction at PC */
50-
/* TODO: Decode opcode and flags */
51-
/* TODO: Dispatch to vm_opcodes_execute() */
52-
/* TODO: Advance PC by 8 bytes */
64+
/* Bounds check */
65+
if (vm->pc + 8 > vm->bytecode_size) {
66+
vm->halted = true;
67+
break;
68+
}
69+
70+
/* Fetch 8-byte instruction */
71+
nexus_instruction_t instr;
72+
memcpy(&instr, vm->bytecode + vm->pc, sizeof(instr));
73+
uint8_t opcode = instr.opcode;
74+
75+
/* Execute instruction */
76+
vm_error_t err = vm_execute_instruction(vm, &instr);
5377
vm->cycle_count++;
78+
79+
if (err != VM_OK) {
80+
vm->last_error = err;
81+
vm->halted = true;
82+
return err;
83+
}
84+
85+
/* Branch instructions manage PC themselves (set to target or advance).
86+
* Non-branch instructions need PC advanced by 8. */
87+
if (opcode != OP_JUMP && opcode != OP_JUMP_IF_FALSE && opcode != OP_JUMP_IF_TRUE) {
88+
vm->pc += 8;
89+
}
5490
}
5591

56-
if (vm->cycle_count >= vm->cycle_budget && !vm->halted) {
92+
if (!vm->halted) {
5793
vm->last_error = ERR_CYCLE_BUDGET_EXCEEDED;
5894
vm->halted = true;
95+
return ERR_CYCLE_BUDGET_EXCEEDED;
5996
}
6097

6198
return vm->last_error;
6299
}
100+
101+
/* ===================================================================
102+
* vm_tick - Execute one tick (init + execute + post-tick)
103+
*
104+
* 1. Reset execution state (PC, SP, halted, cycles)
105+
* 2. Run vm_execute()
106+
* 3. Post-tick: NaN/Infinity guard on all actuator registers
107+
* =================================================================== */
108+
109+
vm_error_t vm_tick(vm_state_t *vm) {
110+
if (!vm) {
111+
return ERR_INVALID_OPERAND;
112+
}
113+
114+
/* Reset execution state */
115+
vm->pc = 0;
116+
vm->sp = 0;
117+
vm->csp = 0;
118+
vm->halted = false;
119+
vm->cycle_count = 0;
120+
vm->last_error = VM_OK;
121+
122+
/* Execute */
123+
vm_error_t err = vm_execute(vm);
124+
125+
/* Post-tick: NaN/Infinity guard on all actuator registers */
126+
for (uint16_t i = 0; i < VM_ACTUATOR_COUNT; i++) {
127+
if (vm_is_nan_or_inf_u32(vm->actuators[i])) {
128+
vm->actuators[i] = 0;
129+
}
130+
}
131+
132+
return err;
133+
}
134+
135+
/* ===================================================================
136+
* vm_set_sensor - Set a sensor register value
137+
* =================================================================== */
138+
139+
vm_error_t vm_set_sensor(vm_state_t *vm, uint8_t idx, float value) {
140+
if (!vm) {
141+
return ERR_INVALID_OPERAND;
142+
}
143+
if (idx >= VM_SENSOR_COUNT) {
144+
return ERR_INVALID_OPERAND;
145+
}
146+
memcpy(&vm->sensors[idx], &value, sizeof(float));
147+
return VM_OK;
148+
}
149+
150+
/* ===================================================================
151+
* vm_get_actuator - Read an actuator register value
152+
* =================================================================== */
153+
154+
float vm_get_actuator(vm_state_t *vm, uint8_t idx) {
155+
if (!vm || idx >= VM_ACTUATOR_COUNT) {
156+
return 0.0f;
157+
}
158+
float val;
159+
memcpy(&val, &vm->actuators[idx], sizeof(float));
160+
return val;
161+
}

0 commit comments

Comments
 (0)