Skip to content
Merged
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
25 changes: 23 additions & 2 deletions src/tri27/emu/decoder.zig
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ pub fn decode(word: u32) Instruction {

// Determine if instruction has immediate or src2
const has_imm = switch (opcode) {
.LD, .ST, .LDI, .STI, .LD_IMM, .PHI_CONST, .PI_CONST, .E_CONST, .JMP, .JZ, .JNZ, .JGT, .JLT, .CALL, .RET, .SHL, .SHR, .STR_LOAD, .STR_CONCAT, .STR_PRINT, .FILE_READ, .FILE_WRITE, .FILE_EXISTS => true,
.LD, .ST, .LDI, .STI, .LD_IMM, .PHI_CONST, .PI_CONST, .E_CONST, .JMP, .JZ, .JNZ, .JGT, .JLT, .CALL, .RET, .SHL, .SHR, .SACR, .STR_LOAD, .STR_CONCAT, .STR_PRINT, .FILE_READ, .FILE_WRITE, .FILE_EXISTS => true,
else => false,
};

Expand Down Expand Up @@ -190,7 +190,7 @@ pub fn encode(inst: Instruction) u32 {
};

const has_imm = switch (inst.opcode) {
.LD, .ST, .LDI, .STI, .LD_IMM, .PHI_CONST, .PI_CONST, .E_CONST, .JMP, .JZ, .JNZ, .JGT, .JLT, .CALL, .RET, .SHL, .SHR, .STR_LOAD, .STR_CONCAT, .STR_PRINT, .FILE_READ, .FILE_WRITE, .FILE_EXISTS => true,
.LD, .ST, .LDI, .STI, .LD_IMM, .PHI_CONST, .PI_CONST, .E_CONST, .JMP, .JZ, .JNZ, .JGT, .JLT, .CALL, .RET, .SHL, .SHR, .SACR, .STR_LOAD, .STR_CONCAT, .STR_PRINT, .FILE_READ, .FILE_WRITE, .FILE_EXISTS => true,
else => false,
};

Expand Down Expand Up @@ -353,6 +353,27 @@ test "encoder: LDI roundtrip with positive 15-bit immediate" {
try std.testing.expect(decoded.has_imm);
}

test "encoder: SACR roundtrip with every sacred operation mode" {
// The decoder must read the SACR mode from the immediate field, and the
// encoder must write it there; otherwise every decoded SACR runs mode 1.
for ([_]i16{ 1, 2, 3, 4 }) |mode| {
const inst = Instruction{
.opcode = .SACR,
.dst = 4,
.src1 = 1,
.immediate = mode,
.has_imm = true,
};
const word = encode(inst);
const decoded = decode(word);
try std.testing.expectEqual(Opcode.SACR, decoded.opcode);
try std.testing.expectEqual(@as(u8, 4), decoded.dst);
try std.testing.expectEqual(@as(u8, 1), decoded.src1);
try std.testing.expectEqual(mode, decoded.immediate);
try std.testing.expect(decoded.has_imm);
}
}

test "decoder: getOpcodeName" {
try std.testing.expectEqualStrings("NOP", getOpcodeName(Opcode.NOP));
try std.testing.expectEqualStrings("ADD", getOpcodeName(Opcode.ADD));
Expand Down
34 changes: 32 additions & 2 deletions src/tri27/emu/executor.zig
Original file line number Diff line number Diff line change
Expand Up @@ -550,8 +550,10 @@ pub fn execute(cpu: *CPUState, inst: Instruction, memory: []align(8) u8) ExecErr
return ExecError.StackOverflow;
}

// Push old IP to stack (word-aligned)
const ip_bytes = std.mem.asBytes(&cpu.pc);
// Push the address of the instruction AFTER the call, so RET resumes
// past the call site instead of looping on the CALL itself.
const return_addr = cpu.pc + 1;
const ip_bytes = std.mem.asBytes(&return_addr);
memory[cpu.sp] = ip_bytes[0];
memory[cpu.sp + 1] = ip_bytes[1];
memory[cpu.sp + 2] = ip_bytes[2];
Expand Down Expand Up @@ -1097,6 +1099,34 @@ test "execute HALT" {
try std.testing.expectEqual(@as(u32, 0), cpu.pc); // IP not advanced
}

test "CALL pushes the next address, RET resumes after the call" {
const allocator = std.testing.allocator;
var cpu = try CPUState.init(allocator);
defer cpu.deinit();
cpu.pc = 5; // the CALL sits at word 5
cpu.sp = 0;

const memory = cpu.getBytesMut();

// CALL at word 5 jumps to word 10; RET there must return to word 6.
try execute(&cpu, Instruction{
.opcode = .CALL,
.immediate = 10,
.has_imm = true,
}, memory);
try std.testing.expectEqual(@as(u32, 10), cpu.pc);
try std.testing.expectEqual(@as(u32, 4), cpu.sp);
// The pushed return address is the word after the CALL, not the CALL itself.
const pushed = std.mem.readInt(u32, memory[0..4], .little);
try std.testing.expectEqual(@as(u32, 6), pushed);

try execute(&cpu, Instruction{
.opcode = .RET,
}, memory);
try std.testing.expectEqual(@as(u32, 6), cpu.pc);
try std.testing.expectEqual(@as(u32, 0), cpu.sp);
}

test "estimateCycles" {
try std.testing.expectEqual(@as(u64, 1), estimateCycles(.NOP));
try std.testing.expectEqual(@as(u64, 1), estimateCycles(.LD_IMM));
Expand Down
104 changes: 87 additions & 17 deletions src/tri27/emu/loader.zig
Original file line number Diff line number Diff line change
Expand Up @@ -87,33 +87,38 @@ pub fn load(cpu: *cpu_state.CPUState, code: []const u8, constants: []const f64)
// Move offset past section header (type=1 + padding=1 + size=2 = 4 bytes)
offset += 4;

// Container padding: the two bytes tri_asm.zig writes after the CODE
// header so the payload starts at byte 12, the word-3 boundary the
// fetch reads at pc * 4. The writer and the loader agree on the
// twelve-byte container header.
if (offset + 2 > code.len) return LoadError.Truncated;
offset += 2;

if (offset + size > code.len) return LoadError.Truncated;
if (offset + size > cpu.memory_len) return LoadError.DataTooLarge;

// Copy code to CPU memory starting at word 3 (skip 12-byte header)
// One code word per four-byte fetch unit: repack the payload into the
// byte-addressed memory at byte 12 + i*4, so run's fetch at pc * 4
// reads exactly the word that was written, starting at pc = 3.
const code_data = code[offset .. offset + size];
// Pack 4 bytes per word (little-endian)
var word_idx: usize = 3; // Start at word 3 (PC=3)
const bytes = cpu.getBytesMut();
if (12 + size > bytes.len) return LoadError.DataTooLarge;
var i: usize = 0;
while (i + 4 <= size) : (i += 4) {
const b0 = code_data[i];
const b1 = code_data[i + 1];
const b2 = code_data[i + 2];
const b3 = code_data[i + 3];
const word_value: u64 = @as(u64, b0) | (@as(u64, b1) << 8) | (@as(u64, b2) << 16) | (@as(u64, b3) << 24);
cpu.memory[word_idx] = tri_memory.Word{ .word_value = @bitCast(word_value) };
word_idx += 1;
@memcpy(bytes[12 + i .. 12 + i + 4], code_data[i .. i + 4]);
}
code_size = size;

offset += size;
},

2 => { // CONSTANTS section
if (offset + 1 > code.len) return LoadError.Truncated;
// Documented layout: id (1) + count (1) + count * 8 bytes.
// The values come from the constants array the caller passes; the
// file bytes are skipped.
if (offset + 2 > code.len) return LoadError.Truncated;

const num_constants = code[offset];
offset += 1;
const num_constants = code[offset + 1];
offset += 2;

// Load constants from constants array (passed as parameter)
// Store as u64 in Word format for compatibility
Expand All @@ -128,7 +133,7 @@ pub fn load(cpu: *cpu_state.CPUState, code: []const u8, constants: []const f64)
}

// Skip constant data in file (already passed via constants array)
const data_size = num_constants * 8; // 8 bytes per f64
const data_size = @as(usize, num_constants) * 8; // 8 bytes per f64
if (offset + data_size > code.len) return LoadError.Truncated;
offset += data_size;
},
Expand Down Expand Up @@ -168,8 +173,10 @@ pub fn load(cpu: *cpu_state.CPUState, code: []const u8, constants: []const f64)
}

// Initialize CPU state
// Instructions start at word 0
cpu.pc = 0;
// The twelve-byte container header (magic, version, section count, the CODE
// section header and its two padding bytes) ends at the first instruction,
// word 3 — the address run fetches at pc * 4.
cpu.pc = 3;
cpu.sp = @as(u32, @intCast(cpu.memory_len - 1));
cpu.fp = 0;
cpu.instructions_executed = 0;
Expand All @@ -182,3 +189,66 @@ pub fn load(cpu: *cpu_state.CPUState, code: []const u8, constants: []const f64)
const total_constants = code[4];
_ = total_constants;
}

// ═════════════════════════════════════════════════════════════════════════════════════════
// TESTS
// ═════════════════════════════════════════════════════════════════════════════════════════

test "load: the writer's twelve-byte container lands at word 3 with pc = 3" {
// The container tri_asm.zig writes: magic, version 1, one section, the CODE
// header, two padding bytes, then two instruction words.
const container = [_]u8{
0x32, 0x49, 0x52, 0x54, // magic
0x01, // version
0x01, // section count
0x01, 0x00, 0x08, 0x00, // CODE: id, padding, size 8
0x00, 0x00, // container padding
0xaa, 0xbb, 0xcc, 0xdd, // word 0
0x11, 0x22, 0x33, 0x44, // word 1
};

var cpu = try cpu_state.CPUState.init(std.testing.allocator);
defer cpu.deinit();

try load(&cpu, &container, &[_]f64{});

// pc starts at the first instruction word, and the fetch at pc * 4 reads
// exactly the two written words.
try std.testing.expectEqual(@as(u32, 3), cpu.pc);
const bytes = cpu.getBytes();
try std.testing.expectEqual(@as(u8, 0xaa), bytes[12]);
try std.testing.expectEqual(@as(u8, 0xbb), bytes[13]);
try std.testing.expectEqual(@as(u8, 0xcc), bytes[14]);
try std.testing.expectEqual(@as(u8, 0xdd), bytes[15]);
try std.testing.expectEqual(@as(u8, 0x11), bytes[16]);
try std.testing.expectEqual(@as(u8, 0x44), bytes[19]);
// Words 0..2 keep the zeros of a fresh CPU: no padding leaks into the code.
for (0..12) |i| try std.testing.expectEqual(@as(u8, 0), bytes[i]);
}

test "load: the CONSTANTS parser reads the id, then the count, then count * 8 bytes" {
// Documented layout: id (2) + count (1) + eight bytes, then the CODE section
// with its header and padding.
const container = [_]u8{
0x32, 0x49, 0x52, 0x54, // magic
0x01, // version
0x02, // section count
0x02, 0x01, // CONSTANTS: id, count 1
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // the constant's eight bytes
0x01, 0x00, 0x04, 0x00, // CODE: id, padding, size 4
0x00, 0x00, // container padding
0x01, 0x02, 0x03, 0x04, // one instruction word
};

var cpu = try cpu_state.CPUState.init(std.testing.allocator);
defer cpu.deinit();

try load(&cpu, &container, &[_]f64{0.0});

// The loader consumed exactly id + count + eight bytes: the code section that
// follows is found and packed at byte 12.
try std.testing.expectEqual(@as(u32, 3), cpu.pc);
const bytes = cpu.getBytes();
try std.testing.expectEqual(@as(u8, 0x01), bytes[12]);
try std.testing.expectEqual(@as(u8, 0x04), bytes[15]);
}
57 changes: 37 additions & 20 deletions src/tri27/emu/specs/tbin_format.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,15 @@

## Header Layout (12 bytes)

The container `tri_asm.zig` writes and `loader.load` reads: a six-byte main
header, a four-byte CODE section header, and two padding bytes. The first
instruction word starts at byte 12, which is word 3 of the fetch at `pc * 4`.

| Offset | Size | Field | Description |
|--------|------|-------|-------------|
| 0x00 | 4 | Magic | `0x54524932` ("2IRT" little-endian) |
| 0x04 | 4 | Version | Format version (current: 0x00010001) |
| 0x08 | 4 | Section count | Number of sections (current: 1 for code only) |
| 0x04 | 1 | Version | Format version (current: 1) |
| 0x05 | 1 | Section count | Number of sections |

### Magic Number
```
Expand All @@ -22,25 +26,25 @@ ASCII: "2IRT" (stored little-endian)
Bytes: [0x32, 0x49, 0x52, 0x54]
```

### Version Format
- Bits 0-15: Minor version
- Bits 16-31: Major version
- Current: `0x00010001` = v1.1

## Code Section

The code section immediately follows the header and contains:

### Section Header (4 bytes)
| Offset | Size | Field | Description |
|--------|------|-------|-------------|
| 0x00 | 1 | Section ID | 0x04 = Code section |
| 0x01 | 1 | Version | Section format version (current: 1) |
| 0x02 | 1 | Flags | Reserved (0 for now) |
| 0x03 | 1 | Padding | Reserved (0) |
| 0x00 | 1 | Section ID | 1 = Code section |
| 0x01 | 1 | Padding | Reserved (0) |
| 0x02 | 2 | Size | Payload size in bytes (u16 little-endian) |

### Container Padding (2 bytes)
Two reserved bytes after the CODE section header align the payload to byte 12
(word 3). The loader skips them; a payload written without them loads shifted
by two bytes.

### Code Data (4 bytes per instruction)
All instructions are 32-bit words stored in **little-endian** byte order.
The loader packs each four payload bytes into one four-byte fetch unit at
byte `12 + i * 4`, so `run`'s fetch at `pc * 4` reads exactly one written word
per instruction, starting at `pc = 3`.

Byte layout:
```
Expand Down Expand Up @@ -101,20 +105,33 @@ Binary representation:
```
Header:
[32 49 52 54] # Magic "2IRT"
[01 00 01 00] # Version 1.1
[01 00 00 00] # 1 section
[01] # Version 1
[01] # 1 section

Code section header:
[04 01 00 00] # Section ID 0x04, v1
Code section header + padding:
[01 00 08 00] # Section ID 1, padding, size 8 (u16 little-endian)
[00 00] # Container padding to byte 12

Instructions (little-endian):
Instructions (little-endian) at byte 12:
LDI t0, 42 -> 0x0000002A # opcode=LDI, dst=t0, imm=42
HALT -> 0x01000000 # opcode=HALT
HALT -> 0x0000004D # opcode=HALT

Full file (20 bytes):
32 49 52 54 01 00 01 00 01 00 00 00 04 01 00 00 2A 00 00 00 01 00 00 00
32 49 52 54 01 01 01 00 08 00 00 00 2A 00 00 00 4D 00 00 00
```

## Constants Section

| Offset | Size | Field | Description |
|--------|------|-------|-------------|
| 0x00 | 1 | Section ID | 2 = Constants |
| 0x01 | 1 | Count | Number of f64 constants |
| 0x02 | 8 * count | Data | One eight-byte value per constant |

The loader reads the id, then the count, then skips `count * 8` payload bytes;
the values themselves come from the `constants` array the caller passes, and
only the first three reach `cpu.f`.

## File Extension
- Source: `.tri` (assembly)
- Object: `.tbin` (binary executable)
Expand Down
4 changes: 2 additions & 2 deletions src/tri27/emu/tri_cpu.zig
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ pub const Trit27 = struct {
const sum = self.trits + other.trits;
// Calculate carry using 3-trit balanced ternary arithmetic
const half_carry = sum >> 54; // Carry for 27-trit values
// Reduce result modulo 3^27 and add carry
const base: i64 = 19683; // 3^27
// Reduce result modulo 3^9 and add carry
const base: i64 = 19683; // 3^9
const result = (sum % base) + half_carry;
return .{ .trits = result };
}
Expand Down
6 changes: 3 additions & 3 deletions src/tri27/emu/tri_emu_main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -125,11 +125,11 @@ pub fn runEmulator(tbin_path: []const u8, options: *const Options, allocator: st
const Loader = @import("loader.zig");
try Loader.load(&cpu, file_content, &[_]f64{});

const inst_count: u32 = @intCast((file_content.len - 10) / 4);
const code_sz: u32 = @intCast(file_content.len - 10);
const inst_count: u32 = @intCast((file_content.len - 12) / 4);
const code_sz: u32 = @intCast(file_content.len - 12);

const load_result = .{
.entry_point = cpu.pc, // loader sets pc = 0
.entry_point = cpu.pc, // loader sets pc = 3 (word 3, the first instruction)
.instruction_count = inst_count,
.code_size = code_sz,
};
Expand Down
Loading