diff --git a/src/tri27/emu/decoder.zig b/src/tri27/emu/decoder.zig index 0c34e9025c..20859eac19 100644 --- a/src/tri27/emu/decoder.zig +++ b/src/tri27/emu/decoder.zig @@ -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, }; @@ -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, }; @@ -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)); diff --git a/src/tri27/emu/executor.zig b/src/tri27/emu/executor.zig index ff661c007b..14ac18ef4c 100644 --- a/src/tri27/emu/executor.zig +++ b/src/tri27/emu/executor.zig @@ -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]; @@ -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)); diff --git a/src/tri27/emu/loader.zig b/src/tri27/emu/loader.zig index 860f0222c0..caf37277a8 100644 --- a/src/tri27/emu/loader.zig +++ b/src/tri27/emu/loader.zig @@ -87,22 +87,24 @@ 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; @@ -110,10 +112,13 @@ pub fn load(cpu: *cpu_state.CPUState, code: []const u8, constants: []const f64) }, 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 @@ -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; }, @@ -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; @@ -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]); +} diff --git a/src/tri27/emu/specs/tbin_format.md b/src/tri27/emu/specs/tbin_format.md index 9ce28ea804..e713138ec0 100644 --- a/src/tri27/emu/specs/tbin_format.md +++ b/src/tri27/emu/specs/tbin_format.md @@ -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 ``` @@ -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: ``` @@ -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) diff --git a/src/tri27/emu/tri_cpu.zig b/src/tri27/emu/tri_cpu.zig index 9ccef907a9..e4cfe3d92c 100644 --- a/src/tri27/emu/tri_cpu.zig +++ b/src/tri27/emu/tri_cpu.zig @@ -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 }; } diff --git a/src/tri27/emu/tri_emu_main.zig b/src/tri27/emu/tri_emu_main.zig index 95a9bb0206..4d82ece84c 100644 --- a/src/tri27/emu/tri_emu_main.zig +++ b/src/tri27/emu/tri_emu_main.zig @@ -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, };