Run a program
You will learn
How five instructions and one three-way jump make a loop that ends itself.
The widget's machine has three 9-trit registers and five instructions: SET, ADD, DEC, BR3 and HALT. Sum 1..n sets r1 to n, adds it into r0, decrements, and BR3 jumps back while r1 is positive; at 0 it falls through to HALT. For n = 5 it halts with r0 = 15. The lesson spec describes the .tbin container a TRI-27 program ships in, as the Trinity loader reads it.
Try it
Step the sum program clock by clock for n = 3; then run 7 x n for n = 12 and count the clocks.

Two short programs on three 9-trit registers: sum 1..n and multiply by repeated adds. Step a clock at a time and watch the trits change.
specs/isa/tri27_bytecode.t27
// SPDX-License-Identifier: Apache-2.0
; specs/isa/tri27_bytecode.t27 -- the .tbin container as the pinned Trinity loader reads it
; S05 of gHashTag/trinity#988 (gHashTag/t27#3567). The consumer has one loader,
; src/tri27/emu/loader.zig at 976df517, and two assemblers that write two different containers;
; three documents describe a third and a fourth. This file states the container the loader
; accepts and every rule by which it rejects a file -- truncated, wrong magic, unsupported
; version, unknown section, code that does not fit memory, no code at all -- as functions the C
; backend executes, and records what the loader does with what it accepts (where the code lands,
; where the entry point is left). tools/trinity_tri27.py composes these functions into a loader
; and replays the loader vectors of conformance/trinity/tri27_programs.json. ASCII only (L3).
; phi^2 + 1/phi^2 = 3 | TRINITY
module Tri27Bytecode;
pub const KIND : str = "isa-bytecode";
pub const ID : str = "isa/tri27_bytecode";
pub const NAME : str = "The .tbin container of the pinned Trinity TRI-27 loader";
pub const CONSUMER_REPO : str = "gHashTag/trinity";
pub const PINNED_REVISION : str = "976df517419f918f05b78e73767e2fdf4aad14be";
pub const OWNER_MODULE : str = "src/tri27/emu/loader.zig";
pub const OWNER_LINES : str = "MAGIC :10; VERSION :13; SectionType :16-21; LoadError :24-32; validateMagic :35-48; validateVersion :51-55; load :58-184";
pub const WRITER_MODULE : str = "src/tri27/emu/tri_asm.zig";
pub const WRITER_LINES : str = "header :633-645, size patched :686-689, label addresses start at word 3 :600";
; --- The header -----------------------------------------------------------------------------
pub const MAGIC : u32 = 1414678834;
pub const MAGIC_HEX : str = "0x54524932";
pub const MAGIC_BYTES : [4]u32 = [50, 73, 82, 84];
pub const MAGIC_TEXT : str = "2IRT in file order; the source comment says TRI2";
pub const VERSION : u32 = 1;
pub const HEADER_BYTES : u32 = 6;
pub const HEADER_LAYOUT : str = "magic (four bytes, little-endian u32) + version (one byte) + section count (one byte)";
; --- Sections -------------------------------------------------------------------------------
pub const SECTION_CODE : u32 = 1;
pub const SECTION_CONSTANTS : u32 = 2;
pub const SECTION_DATA : u32 = 3;
pub const SECTION_BSS : u32 = 4;
pub const SECTION_NAMES : [4]str = ["CODE", "CONSTANTS", "DATA", "BSS"];
pub const CODE_HEADER_BYTES : u32 = 4;
pub const CODE_HEADER_LAYOUT : str = "id (1) + padding (1) + size (u16 little-endian); the payload follows; four payload bytes make one instruction word, a trailing group shorter than four bytes is dropped";
pub const CONSTANTS_HEADER_BYTES : u32 = 1;
pub const CONSTANTS_SKIP : u32 = 16;
pub const CONSTANTS_LAYOUT : str = "id (1) + sixteen skipped bytes: the loader reads the count from the byte at its offset, which is the section id 2, so it always skips 2 * 8 bytes; a count byte written after the id is the first skipped byte; the values come from the f64 array the caller passes and only the first three reach cpu.f as the high sixteen bits of bits 32..47";
pub const DATA_HEADER_BYTES : u32 = 3;
pub const DATA_LAYOUT : str = "id (1) + size (u16 little-endian); each payload byte is written to one memory word starting at word index code_size (a byte count used as a word index)";
pub const BSS_HEADER_BYTES : u32 = 3;
pub const BSS_LAYOUT : str = "id (1) + size (u16 little-endian); the size is read and ignored";
pub const CODE_START_WORD : u32 = 3;
pub const CODE_PLACEMENT : str = "the code payload is repacked four bytes per word from word 3 on; words 0..2 keep the zeros of a fresh CPU";
; --- What the loader leaves behind ----------------------------------------------------------
pub const LOAD_PC : u32 = 0;
pub const LOAD_SP : u32 = 19682;
pub const LOAD_FP : u32 = 0;
pub const MEMORY_WORDS : u32 = 19683;
pub const ENTRY_RULE : str = "pc = 0 after load, so three NOP words (the zeros of words 0..2) run before the code; CPUState.init and resetExecution set pc = 3 instead, and `tri tri27 run` copies the whole file to byte 0 and starts at 3";
; --- Rejections ----------------------------------------------------------------------------
pub const LOAD_ERROR_NAMES : [7]str = ["InvalidMagic", "InvalidVersion", "CorruptHeader", "Truncated", "SectionMissing", "InvalidSection", "DataTooLarge"];
pub const LOAD_ERROR_RETURNED : [7]bool = [true, true, false, true, true, true, true];
pub const LOAD_OK : u32 = 0;
pub const LOAD_TRUNCATED : u32 = 1;
pub const LOAD_INVALID_MAGIC : u32 = 2;
pub const LOAD_INVALID_VERSION : u32 = 3;
pub const LOAD_INVALID_SECTION : u32 = 4;
pub const LOAD_DATA_TOO_LARGE : u32 = 5;
pub const LOAD_SECTION_MISSING : u32 = 6;
pub const LOAD_STATUS_NAMES : [7]str = ["ok", "Truncated", "InvalidMagic", "InvalidVersion", "InvalidSection", "DataTooLarge", "SectionMissing"];
pub const REJECTION_ORDER : str = "a file shorter than six bytes is Truncated before the magic is read; magic before version; each section header and payload against the file length before the memory; the memory check compares the FILE offset plus the size with the memory word count; SectionMissing is decided after every section, once pc and sp have already been set";
; --- The writer's container -----------------------------------------------------------------
pub const WRITER_HEADER_BYTES : u32 = 12;
pub const WRITER_HEADER : [12]u32 = [50, 73, 82, 84, 1, 1, 1, 0, 0, 0, 0, 0];
pub const WRITER_HEADER_RULE : str = "magic, version 1, one section, CODE id, padding, size (patched into bytes 8 and 9), two padding bytes; the code follows at byte 12 and its labels count from word 3; the loader takes the payload from byte 10, so the two padding bytes become the first two bytes of the first loaded word and every loaded word is shifted by two bytes";
pub const LOADER_NATIVE_HEADER_BYTES : u32 = 10;
pub const LOADER_NATIVE_RULE : str = "a container the loader reads as written: the six-byte header, the four-byte CODE header, the code; no writer in the tree produces it";
; --- Findings ------------------------------------------------------------------------------
pub const FINDING_COUNT : u32 = 8;
pub const FINDINGS : [8]str = ["src/tri27/emu/asm_parser.zig, the assembler behind `tri tri27 assemble`, writes no header at all; `tri tri27 run` then starts at word 3 and skips the first three instructions", "src/tri27/emu/specs/tbin_format.md describes a twelve-byte header of three u32 fields, version 0x00010001 and CODE id 4; the loader reads six bytes, version 1 and CODE id 1", "docs/tri27_cli.md says a program has no header and starts at byte 0; that is true of asm_parser.zig's output and false of the loader's expectations", "the loader's memory check adds the file offset to the section size before comparing with the word count 19683, so the largest accepted code payload depends on where it sits in the file", "CorruptHeader is declared and never returned; `total_constants = code[4]` reads the version byte and discards it", "no .tbin file and no trace file exists in the consumer's tree at the pin; the only containers ever loaded are the ones tri_asm.zig writes inside the tests", "the writer and the loader disagree by two bytes: tri_asm.zig pads its header to twelve bytes, loader.load reads the code from byte ten, so a written container loads shifted and its first word decodes from two padding bytes; the owner's tests never load a written container (they copy it to byte 0 and run from word 3)", "the CONSTANTS parser reads the section id as the constant count and skips sixteen bytes, so the documented layout (id, count, count * 8 bytes) is misread: with one constant the loader lands nine bytes into the code that follows"];
pub const ENABLED : bool = true;
; --- Rules ------------------------------------------------------------------------------------
pub fn magic_word(b0: u32, b1: u32, b2: u32, b3: u32) -> u32 {
return b0 | (b1 << 8) | (b2 << 16) | (b3 << 24);
}
pub fn magic_ok(b0: u32, b1: u32, b2: u32, b3: u32) -> bool {
return magic_word(b0, b1, b2, b3) == MAGIC;
}
pub fn version_ok(version: u32) -> bool {
return version == VERSION;
}
pub fn header_status(len: u32, b0: u32, b1: u32, b2: u32, b3: u32, version: u32) -> u32 {
if (len < HEADER_BYTES) { return LOAD_TRUNCATED; }
if (magic_ok(b0, b1, b2, b3) == false) { return LOAD_INVALID_MAGIC; }
if (version_ok(version) == false) { return LOAD_INVALID_VERSION; }
return LOAD_OK;
}
pub fn section_kind_ok(id: u32) -> bool {
if (id == SECTION_CODE) { return true; }
if (id == SECTION_CONSTANTS) { return true; }
if (id == SECTION_DATA) { return true; }
if (id == SECTION_BSS) { return true; }
return false;
}
pub fn section_header_bytes(id: u32) -> u32 {
if (id == SECTION_CODE) { return CODE_HEADER_BYTES; }
if (id == SECTION_CONSTANTS) { return CONSTANTS_HEADER_BYTES; }
if (id == SECTION_DATA) { return DATA_HEADER_BYTES; }
if (id == SECTION_BSS) { return BSS_HEADER_BYTES; }
return 0;
}
pub fn fits_file(offset: u32, bytes: u32, len: u32) -> bool {
return offset + bytes <= len;
}
pub fn size_field(lo: u32, hi: u32) -> u32 {
return lo | (hi << 8);
}
pub fn code_fits_memory(offset_after_header: u32, size: u32, memory_words: u32) -> bool {
return offset_after_header + size <= memory_words;
}
pub fn code_words(size: u32) -> u32 {
return size / 4;
}
pub fn code_word_index(i: u32) -> u32 {
return CODE_START_WORD + i;
}
pub fn constants_payload(count_byte: u32) -> u32 {
return count_byte * 8;
}
pub fn constants_skip_after_id() -> u32 {
return constants_payload(SECTION_CONSTANTS);
}
pub fn data_word_index(code_size: u32, i: u32) -> u32 {
return code_size + i;
}
pub fn data_fits_memory(code_size: u32, size: u32, memory_words: u32) -> bool {
return code_size + size <= memory_words;
}
pub fn code_present(code_size: u32) -> bool {
return code_size != 0;
}
invariant the_writer_header_is_the_loader_header_plus_a_code_header_plus_padding { WRITER_HEADER_BYTES == HEADER_BYTES + CODE_HEADER_BYTES + 2 }
invariant the_loader_stack_pointer_is_the_last_word { LOAD_SP == MEMORY_WORDS - 1 }
invariant the_writer_header_exceeds_the_loader_header_by_two { WRITER_HEADER_BYTES == LOADER_NATIVE_HEADER_BYTES + 2 }
invariant the_magic_is_its_bytes { MAGIC == 50 + 73 * 256 + 82 * 65536 + 84 * 16777216 }
test the_magic_is_read_little_endian {
assert magic_word(50, 73, 82, 84) == MAGIC;
assert magic_ok(50, 73, 82, 84) == true;
assert magic_ok(84, 82, 73, 50) == false;
}
test the_header_is_rejected_in_order {
assert header_status(5, 50, 73, 82, 84, 1) == LOAD_TRUNCATED;
assert header_status(6, 84, 82, 73, 50, 1) == LOAD_INVALID_MAGIC;
assert header_status(6, 50, 73, 82, 84, 2) == LOAD_INVALID_VERSION;
assert header_status(6, 50, 73, 82, 84, 0) == LOAD_INVALID_VERSION;
assert header_status(12, 50, 73, 82, 84, 1) == LOAD_OK;
}
test only_four_section_kinds_exist {
assert section_kind_ok(1) == true;
assert section_kind_ok(4) == true;
assert section_kind_ok(0) == false;
assert section_kind_ok(5) == false;
assert section_header_bytes(SECTION_CODE) == 4;
assert section_header_bytes(SECTION_DATA) == 3;
assert section_header_bytes(9) == 0;
}
test code_is_packed_four_bytes_per_word_from_word_three {
assert size_field(16, 0) == 16;
assert size_field(0, 1) == 256;
assert code_words(16) == 4;
assert code_words(13) == 3;
assert code_word_index(0) == 3;
assert code_word_index(4) == 7;
}
test the_memory_check_counts_the_file_offset {
assert code_fits_memory(10, 19673, MEMORY_WORDS) == true;
assert code_fits_memory(10, 19674, MEMORY_WORDS) == false;
assert fits_file(6, 4, 10) == true;
assert fits_file(6, 4, 9) == false;
assert data_fits_memory(19680, 3, MEMORY_WORDS) == true;
assert data_fits_memory(19680, 4, MEMORY_WORDS) == false;
}
test the_other_sections_are_measured_but_not_loaded {
assert constants_payload(3) == 24;
assert constants_skip_after_id() == CONSTANTS_SKIP;
assert data_word_index(16, 2) == 18;
assert code_present(0) == false;
assert code_present(4) == true;
}
All lessons
Module 1 · Three values
Why three, how balanced ternary writes every number without a sign, and what flipping and cutting trits do.
Module 2 · Logic with unknown
Kleene's three-valued gates, two gates binary has no twin for, and addition as a pair of tables.
Module 3 · Adding trits
The half adder, the full adder and a carry that ripples left, one place at a time.
Module 4 · Multiplying without a multiplier
Copy, drop or flip: a product by one trit, long multiplication, and a MAC that only adds.
Module 5 · Trits in binary memory
Two bits per trit, five trits per byte, and the bits a 27-trit word needs.
Module 6 · The TRI-27 instruction word
The 32-bit word the Trinity emulator decodes: its fields, its 47 opcodes and its 15-bit immediate.
Module 7 · A ternary machine
An ALU built from this course's adders, a three-way jump, and a program you can step.
Module 8 · Three answers
Compare at the highest differing trit, find a number in thirds, sort with three-way compares.
Module 9 · Ternary neurons
Weights of -1, 0, +1: one neuron, a detector and a small layer, with no multiplier anywhere.