# Setnex ISA — Specification v0.3

Copyright 2026 Eric Tellier (Terias)

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this specification except in compliance with the License.
You may obtain a copy of the License at

    https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, this specification
is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
either express or implied. See the License for the specific language governing
permissions and limitations under the License.

In accordance with Section 3 of the Apache License 2.0, any implementation of
this specification is granted a perpetual, worldwide, non-exclusive, no-charge,
royalty-free, irrevocable patent license to make, use, sell, and distribute
implementations that comply with this specification.

---

> Balanced ternary instruction set architecture, inspired by RISC-V.
> Balanced ternary {−1, 0, +1}, 27-trit word, 27 registers, fixed-length instructions.

---

## Changelog from v0.2

| # | Change | Rationale |
|---|--------|-----------|
| 1 | ADC (funct[13]=N on ADD) and SBC (funct[13]=N on SUB) added | Multi-precision carry chain with ternary carry {N,Z,P}; funct[13] is now a 3-way mode selector: Z=normal, P=saturating, N=with-carry |
| 2 | TSEL added (opcode −2, R format with rp in funct[13..15]) | 3-way conditional select on FLAGS.sign — the defining ternary-native instruction |
| 3 | BF added (opcode −10, J format, rs1 field = condition mask) | Trit-masked branch on FLAGS.sign; 6 comparison branches in 1 opcode |
| 4 | BRT3 added (opcode −21, new format B) | Ternary three-way branch on LST(rX): P=fall-through, Z=off_z, N=off_n |
| 5 | Format B added: 4t opcode + 3t rX + 10t off_z + 10t off_n | New format for BRT3; two 10-trit offsets (±29 524) |
| 6 | Instruction formats: 4 → 5 (R, I, J, U, B) | Accommodates BRT3 |
| 7 | Opcodes used: 43 → 46 (TSEL, BF, BRT3; ADC/SBC via funct) | Was 43 in v0.2 |

---

## 1. Notation and conventions

| Symbol | Meaning |
|--------|---------|
| `t` | trit ∈ {−1, 0, +1}, written `N`, `Z`, `P` |
| `T[n]` | n-trit word (value in [−(3ⁿ−1)/2, +(3ⁿ−1)/2]) |
| `tryte` | 27-trit word (basic memory and register unit) |
| `val(x)` | balanced integer value of a ternary word |
| `enc(n, w)` | balanced ternary encoding of integer n on w trits |
| `sign(x)` | N if x < 0, Z if x = 0, P if x > 0 |
| `t[i]` | trit i of a word (i=0 = least significant trit) |
| `rd` | destination register |
| `rs1`, `rs2` | source registers |
| `imm` | signed immediate (balanced ternary) |
| `LST` | Least Significant Trit (t[0]) |
| `MST` | Most Significant Trit (t[w−1] for a w-trit word) |
| `zero-extend(x)` | Extend a narrow field to 27 trits by filling higher trits with Z (preserves balanced value, since Z = 0) |

Trits are numbered from 0 (LST) to 26 (MST) within a tryte.

### 1.1 Textual representation

Trit glyphs: `-` for N (−1), `0` for Z (0), `+` for P (+1).

Two conventions coexist in this document:

- **MST-first** (human-readable, used in register encoding examples and inline notation): most significant trit is written leftmost, as in ordinary number notation. Example: `++0-+` means t[4]=P, t[3]=P, t[2]=Z, t[1]=N, t[0]=P → val = 81 + 27 + 0 − 3 + 1 = 106.

- **LST-first** (memory layout, used in instruction encoding diagrams): t[0] is written leftmost. This matches the physical trit ordering in memory and in instruction format diagrams.

Each convention is explicitly labeled where it appears. When unmarked, MST-first is assumed.

### 1.2 Integer value

The integer value of a w-trit field starting at t[i] is: Σ trit[i+k] × 3^k for k=0..w−1.

Words are stored in memory **least significant trit first** (little-endian).

---

## 2. Programming model

### 2.1 General-purpose registers

27 T27 registers, named `r0`–`r26`, encoded on 3 trits (address ∈ [−13, +13]).

| Register | ABI name | Conventional role |
|----------|----------|------------------|
| `r0` | `zero` | Always 0 (read-only by convention) |
| `r1` | `ra` | Return address |
| `r2` | `sp` | Stack pointer (grows toward negative addresses) |
| `r3` | `gp` | Global pointer |
| `r4` | `tp` | Thread pointer |
| `r5`–`r7` | `t0`–`t2` | Temporaries (caller-saved) |
| `r8`–`r9` | `s0`–`s1` | Saved registers (callee-saved), `s0` = frame pointer |
| `r10`–`r16` | `a0`–`a6` | Arguments / return values |
| `r17`–`r25` | `s2`–`s10` | Saved registers (callee-saved) |
| `r26` | `t3` | Extra temporary |

Register addresses use balanced ternary encoding naturally (shown MST-first):
r0 = 0 → `000`, r1 = 1 → `00+`, r2 = 2 → `0+-`, r3 = 3 → `0+0`, …, r13 = 13 → `+++`, r14 = −13 → `---`, …, r26 = −1 → `00-`.

> Note: the 27 register indices 0–26 are mapped to the 27 balanced ternary T3 values −13 to +13. The mapping is: register rN has address enc(N, 3) for N ∈ {0..13}, and enc(N−27, 3) for N ∈ {14..26}. This wraps naturally in the balanced ternary range.

### 2.2 Control and status registers (CSR)

CSR addresses are T3 (3 trits), providing **27 addressable slots** (values −13 to +13) — a symmetric match with the 27 GPRs. Each CSR is a full T27 word. Unused slots are reserved and read as zero.

| Address (T3, MST-first) | Decimal | Name | Description |
|--------------------------|---------|------|-------------|
| `00+` | 1 | `PC` | Program counter (T27, word address) |
| `0+-` | 2 | `LMODE` | Logic mode (see §6) |
| `0+0` | 3 | `FLAGS` | Arithmetic flags (see §5.5) |
| `0++` | 4 | `EPC` | Exception program counter |
| `+--` | 5 | `ECAUSE` | Exception cause (T27) |
| `+-0` | 6 | `EVEC` | Exception vector (handler address) |
| `+-+` | 7 | `STATUS` | Processor status (see §2.4) |
| `+0-` | 8 | `ESAVE` | Saved STATUS on exception entry (new in v0.2) |
| others | — | — | Reserved (read as zero) |

At reset, all CSRs are initialized to zero.

### 2.3 Address space

- Addressing unit: **27-trit word**
- Address space: T27 → val ∈ [−(3²⁷−1)/2, +(3²⁷−1)/2] ≈ ±3.6 × 10¹²
- PC is incremented by 1 (one word) after each instruction
- Negative addresses: stack and kernel space (convention)
- Positive addresses: user code and data

### 2.4 STATUS register structure

| Trit | Name | Values |
|------|------|--------|
| t[0] | `mode` | N = kernel, Z = reserved, P = user |
| t[1] | `ie` | N = interrupts masked, Z = reserved, P = interrupts enabled |
| t[2] | `lx` | Logic extension for LMODE=N: N = Heyting, Z = standard Łukasiewicz, P = RM3 (see §6) |
| t[3..26] | — | Reserved (must be Z) |

At reset, STATUS = 0 → kernel mode, interrupts masked, LMODE=N submode = Łukasiewicz.

---

## 3. Instruction encoding

Every instruction is a fixed-length **27-trit word**.

### 3.1 Opcode field

The **4 least significant trits** (t[0]–t[3]) form the primary opcode.
3⁴ = 81 possible combinations — ample opcode space.

Placing the opcode at t[0]–t[3] allows the decoder to begin working as soon as the first trits of the word arrive, without waiting for the full word.

### 3.2 Instruction formats

Five formats. The format is determined solely by the opcode; the decoder does not inspect other fields to determine the format.

```
R format (register–register)
t:  [0-3]   [4-6]   [7-9]   [10-12]  [13-26]
    opcode   rd      rs1     rs2      funct (14 trits)
    4 trits  3 trits 3 trits 3 trits  14 trits

I format (immediate)
t:  [0-3]   [4-6]   [7-9]   [10-26]
    opcode   rd      rs1     imm17
    4 trits  3 trits 3 trits 17 trits  (imm ∈ [−(3¹⁷−1)/2, +(3¹⁷−1)/2] ≈ ±64 million)

J format (conditional branch)
t:  [0-3]   [4-6]   [7-26]
    opcode   rs1     offset20
    4 trits  3 trits 20 trits  (offset ∈ [−(3²⁰−1)/2, +(3²⁰−1)/2] ≈ ±1.7 billion)

U format (unconditional jump)
t:  [0-3]   [4-26]
    opcode   offset23
    4 trits  23 trits  (offset ∈ [−(3²³−1)/2, +(3²³−1)/2] ≈ ±4.7 × 10¹⁰)

B format (ternary three-way branch) — new in v0.3
t:  [0-3]   [4-6]   [7-16]     [17-26]
    opcode   rX      off_z      off_n
    4 trits  3 trits 10 trits   10 trits  (offsets ∈ [−(3¹⁰−1)/2, +(3¹⁰−1)/2] ≈ ±29 524)
```

Fields are laid out from least significant (t[0]) to most significant (t[26]).

> **Rationale for format U:** In v0.1, JMP and CALL used J format, wasting the rs1 field (3 trits) that they do not need. Format U merges those trits into the offset, multiplying jump range by 27 at no cost. Conditional branches retain J format because they require rs1 for the test register.

> **Rationale for format B (new in v0.3):** A ternary three-way branch needs two offsets (for Z and N outcomes; P falls through). The 23 trits after opcode+rX are split evenly into two 10-trit offset fields, each with a range of ±29 524. This is the most ternary-native branch format: one instruction, three outcomes, zero wasted trits.

The 14-trit `funct` field in R format allows 3¹⁴ ≈ 4.8 million variants per opcode — only a few trits are used in v0.2, leaving the rest for future extensions. Funct sub-fields are specified per-instruction.

---

## 4. Instruction set

### 4.1 Opcode table (4 trits = value from −40 to +40)

#### ALU group — R format (opcode −40 to −27)

| Opcode (val) | Mnemonic | funct | Operation |
|-------------|----------|-------|-----------|
| −40 | `ADD` | `funct[13]=Z` | rd ← rs1 + rs2 (balanced arithmetic) |
| −40 | `ADDS` | `funct[13]=P` | rd ← rs1 + rs2 (saturating: clamps to T27 range) |
| −40 | `ADC` | `funct[13]=N` | rd ← rs1 + rs2 + FLAGS.carry (new in v0.3) |
| −39 | `SUB` | `funct[13]=Z` | rd ← rs1 − rs2 |
| −39 | `SUBS` | `funct[13]=P` | rd ← rs1 − rs2 (saturating) |
| −39 | `SBC` | `funct[13]=N` | rd ← rs1 − rs2 − FLAGS.carry (new in v0.3) |
| −38 | `MUL` | `funct[13]=Z` | rd ← low 27 trits of rs1 × rs2 |
| −38 | `MULH` | `funct[13]=P` | rd ← high 27 trits of rs1 × rs2 (54-trit product) |
| −37 | `DIV` | Z | rd ← rs1 ÷ rs2 (symmetric Euclidean, see §5.4) |
| −36 | `MOD` | Z | rd ← rs1 mod rs2 (symmetric remainder, see §5.4) |
| −35 | `NEG` | Z | rd ← −rs1 (trit-by-trit inversion: P↔N, Z→Z) |
| −34 | `TAND` | Z | rd ← rs1 AND rs2 (per LMODE, see §6) |
| −33 | `TOR` | Z | rd ← rs1 OR rs2 (per LMODE) |
| −32 | `TNOT` | Z | rd ← NOT rs1 (per LMODE) |
| −31 | `TIMPL` | Z | rd ← rs1 IMPL rs2 (per LMODE) |
| −30 | `CONS` | Z | rd ← consensus(rs1, rs2) — always Kleene |
| −29 | `ACONS` | Z | rd ← anti-consensus(rs1, rs2) — always Kleene |
| −28 | `TSHIFT` | Z | rd ← rs1 shifted by val(rs2) trits (left if >0, right if <0; vacated trits filled with Z) |
| −27 | `TCMP` | Z | rd ← trit-by-trit comparison: rd[i] = sign(rs1[i] − rs2[i]) |

> **Consensus**: trit-by-trit, `cons(a,b) = a if a==b, else Z`.
> **Anti-consensus**: trit-by-trit, `acons(a,b) = Z if a==b, else the absent trit`:
> `acons(N,Z) = acons(Z,N) = P` — `acons(Z,P) = acons(P,Z) = N` — `acons(N,P) = acons(P,N) = Z`.
> CONS and ACONS are dual operations: CONS extracts agreement, ACONS extracts the absent trit.
> Both ignore LMODE — they are arithmetic primitives, not logic operations.
>
> **TCMP** is the trit-by-trit spaceship operator. For each trit position i:
> `rd[i] = N` if rs1[i] < rs2[i], `Z` if rs1[i] = rs2[i], `P` if rs1[i] > rs2[i].
> TCMP complements CONS/ACONS: CONS extracts shared values, TCMP extracts the ordering relation.
> Together, these three form a complete trit-comparison toolkit.
>
> **Saturating arithmetic** (ADDS, SUBS): the result is clamped to [−(3²⁷−1)/2, +(3²⁷−1)/2] instead of wrapping. FLAGS.overflow is set to Z (no overflow) regardless, since overflow is absorbed. FLAGS.sign reflects the clamped result.
>
> **Carry-chain arithmetic** (ADC, SBC — new in v0.3): the value of FLAGS.carry from the previous ALU operation is added to (ADC) or subtracted from (SBC) the result. This enables multi-precision arithmetic. The balanced ternary carry {N, Z, P} is richer than the binary carry {0, 1} — one ADC propagates 3 values natively. Sequence for 54-trit addition: `ADD lo, a_lo, b_lo` then `ADC hi, a_hi, b_hi`.
>
> **funct[13] on ADD/SUB is a 3-way mode selector:** Z = normal (ADD/SUB), P = saturating (ADDS/SUBS), N = with carry (ADC/SBC). This is itself a ternary exploitation — one trit selects among 3 modes.

#### Memory group — I format (opcode −26 to −18)

| Opcode (val) | Mnemonic | Operation |
|-------------|----------|-----------|
| −26 | `LOAD` | rd ← Mem[rs1 + imm17] |
| −25 | `STORE` | Mem[rs1 + imm17] ← rd (rd field used as source) |
| −24 | `LI` | rd ← zero-extend(imm17) to 27 trits |
| −23 | `LUI` | rd ← imm17 << 10 (load upper immediate, low 10 trits set to Z) |
| −22 | `ADDI` | rd ← rs1 + zero-extend(imm17) |
| −21 | `BRT3` | B | Ternary 3-way branch (new in v0.3, see below) |
| −20 to −19 | — | reserved |
| −18 | `CMPI` | FLAGS ← compare(rs1, zero-extend(imm17)) — see §5.5 |

#### Branch group — J format (opcode −17 to −10) and U format (opcode −9 to −8)

| Opcode (val) | Mnemonic | Format | Condition | Semantics |
|-------------|----------|--------|-----------|-----------|
| −17 | `BEQ` | J | rs1 == 0 | PC ← PC + offset20 |
| −16 | `BNE` | J | rs1 ≠ 0 | PC ← PC + offset20 |
| −15 | `BLT` | J | rs1 < 0 | PC ← PC + offset20 |
| −14 | `BGT` | J | rs1 > 0 | PC ← PC + offset20 |
| −13 | `BLE` | J | rs1 ≤ 0 | PC ← PC + offset20 |
| −12 | `BGE` | J | rs1 ≥ 0 | PC ← PC + offset20 |
| −11 | `JMPA` | J | unconditional | PC ← rs1 + offset20 |
| −10 | `BF` | J | FLAGS.sign matches mask | Trit-masked branch on FLAGS (new in v0.3, see below) |
| −9 | `JMP` | U | unconditional | PC ← PC + offset23 |
| −8 | `CALL` | U | unconditional | ra ← PC + 1 ; PC ← PC + offset23 |

> Branch instructions (BEQ–BGE) use **rs1** (field [4-6]) as the register to test, and the offset is relative to the current PC (before increment).
> BLT/BGT/BLE/BGE compare val(rs1) to 0.
>
> JMPA retains J format because it needs rs1 as the base address register.
> JMP and CALL use U format for maximum jump range (23 trits ≈ ±4.7 × 10¹⁰ words).

#### CSR and system group — I format (opcode −7 to −4)

| Opcode (val) | Mnemonic | Operation |
|-------------|----------|-----------|
| −7 | `CSRR` | rd ← CSR[imm17] |
| −6 | `CSRW` | CSR[imm17] ← rs1 |
| −5 | `CSRX` | rd ← CSR[imm17] ; CSR[imm17] ← rs1 (atomic read-then-write) |
| −4 | `ECALL` | System call (cause = imm17); triggers exception sequence |

#### Special group — R format (opcode −3 to +4)

| Opcode (val) | Mnemonic | Operation |
|-------------|----------|-----------|
| −3 | `IRET` | Atomic exception return: PC ← EPC ; STATUS ← ESAVE (new in v0.2) |
| −2 | `TSEL` | R | rd ← rs1 if FLAGS.sign=N, rs2 if Z, funct[13..15] if P (new in v0.3, see below) |
| −1 | `NOP` | No operation |
| 0 | `HALT` | Halt processor |
| +1 | `TGET` | rd ← trit t[val(rs2)] of rs1 (result is N, Z, or P in a T27 word) |
| +2 | `TSET` | rd ← rs1 with trit t[val(rs2)] set to value encoded in funct[13] (see below) |
| +3 | `TSIGN` | rd ← sign(rs1) : N, Z, or P (as T27: −1, 0, or +1) |
| +4 | `CMP` | FLAGS ← compare(rs1, rs2) — see §5.5 |

**TSET encoding** (opcode +2): the value to insert is determined by `funct[13]`:

| funct[13] | Assembler mnemonic | Inserted trit value |
|-----------|--------------------|---------------------|
| N | `TSETN rd, rs1, rs2` | N (−1) |
| Z | `TSETZ rd, rs1, rs2` | Z (0) |
| P | `TSETP rd, rs1, rs2` | P (+1) |

> `rs2` provides the trit index (0–26); the value to write comes from funct, not from rs2.
> The bare mnemonic `TSET` is accepted by the assembler as an alias for `TSETZ` (clear a trit).

#### Absolute value and trit-reduce (opcode +5 to +7)

| Opcode (val) | Mnemonic | Operation |
|-------------|----------|-----------|
| +5 | `TABS` | rd ← \|rs1\| (absolute value) |
| +6 | `TMIN` | rd ← minimum trit of rs1 (fold-min across all 27 trits; result is N, Z, or P as T27) |
| +7 | `TMAX` | rd ← maximum trit of rs1 (fold-max across all 27 trits; result is N, Z, or P as T27) |

> **TMIN / TMAX** are trit-reduce operations: they fold across all 27 trit positions and return the extremum as a single-trit value in a T27 register.
> - `TMIN(x) = P` if and only if all trits of x are P (all-P test).
> - `TMAX(x) = N` if and only if all trits of x are N (all-N test).
> - After a subsumption check (`TIMPL result, req, caps`), the pattern `TMIN result` followed by `BGT t0, granted` branches if all 27 capabilities are satisfied — no constant needed.

#### New in v0.3: TSEL, BF, BRT3

**TSEL — 3-way conditional select (opcode −2, R format)**

`TSEL rd, rn, rz, rp` dispatches based on FLAGS.sign:
- FLAGS.sign = N → rd ← rn (rs1 field)
- FLAGS.sign = Z → rd ← rz (rs2 field)
- FLAGS.sign = P → rd ← rp (funct[13..15] field, register address)

This is the defining ternary-native data instruction. After a CMP, one instruction selects among three registers — binary requires 2 CMOVs or a branch. The R format accommodates 4 register fields: rd (destination), rs1=rn, rs2=rz, funct[13..15]=rp.

Example — clamp to range [lo, hi]:
```asm
CMP    val, lo          # FLAGS.sign: N if val < lo, Z if =, P if >
TSEL   t0, lo, val, val # t0 = lo if below, val otherwise
CMP    t0, hi           # FLAGS.sign: N if t0 < hi, Z if =, P if >
TSEL   result, t0, t0, hi  # result = hi if above, t0 otherwise
```

**BF — trit-masked branch on FLAGS (opcode −10, J format)**

`BF cond, offset20` branches if FLAGS.sign matches the condition mask encoded in the rs1 field [4-6]:
- t[4] = P → match if FLAGS.sign = N
- t[5] = P → match if FLAGS.sign = Z
- t[6] = P → match if FLAGS.sign = P

The branch is taken if any matching trit is set. This encodes all 6 standard comparison branches plus "always" in a single opcode:

| Assembler | rs1 mask | Condition |
|-----------|----------|-----------|
| `BFLT` | `P00` | FLAGS.sign = N (less than) |
| `BFEQ` | `0P0` | FLAGS.sign = Z (equal) |
| `BFGT` | `00P` | FLAGS.sign = P (greater than) |
| `BFLE` | `PP0` | FLAGS.sign = N or Z (less or equal) |
| `BFGE` | `0PP` | FLAGS.sign = Z or P (greater or equal) |
| `BFNE` | `P0P` | FLAGS.sign = N or P (not equal) |

The pattern `CMP a, b ; BFLT label` replaces `SUB t0, a, b ; BLT t0, label`, saving a register and an instruction. FLAGS are not modified by BF.

**BRT3 — ternary three-way branch (opcode −21, B format)**

`BRT3 rX, off_z, off_n` reads the least significant trit (LST) of register rX and dispatches:
- LST(rX) = P → fall through to PC + 1 (no branch penalty)
- LST(rX) = Z → PC ← PC + off_z
- LST(rX) = N → PC ← PC + off_n

Format B provides two 10-trit signed offsets (±29 524 each). The P-falls-through convention optimizes for the common case: loop bodies execute directly without a branch.

A `while` loop compiles to:
```asm
loop_start:
    ; evaluate condition → rX (P=true, Z=unknown, N=false)
    BRT3  rX, loop_start, loop_exit
    ; fall-through (P) → loop body
    ...
    JMP   loop_start
loop_exit:
```

Two instructions of overhead; the body executes without any branch. Variants:
- `while!` (optimistic): set off_z = +1 → Z falls through with P.
- `while?` (pessimistic): set off_z = off_n → Z treated as N, exits loop.

FLAGS are not affected by BRT3.

#### Reserved and extensions (opcode +8 to +40)

Opcodes +8 to +40 (33 values) are **reserved** for future extensions:
- +8 to +14 : ternary floating-point extension (TFP)
- +15 to +24 : ternary vector instructions
- +25 to +40 : implementer-defined / custom

---

## 5. Balanced ternary arithmetic

### 5.1 Addition

Addition follows balanced base-3 tables. The carry is also balanced ternary:

```
a + b  →  sum | carry
N + N  →   P  |  N     (−1 + −1 = −2 = +1 − 3 → sum=P, carry=N)
N + Z  →   N  |  Z
N + P  →   Z  |  Z     (−1 + +1 = 0)
Z + Z  →   Z  |  Z
Z + P  →   P  |  Z
P + P  →   N  |  P     (+1 + +1 = +2 = −1 + 3 → sum=N, carry=P)
```

When a carry-in is present (multi-trit addition), the full 3-input sum produces sum and carry-out in the same balanced ternary system.

### 5.2 Negation (NEG)

NEG inverts every trit: P→N, Z→Z, N→P. This is the "free" operation of balanced ternary — it requires no carry chain, just a trit-wise inversion.

### 5.3 Multiplication

Trit-by-trit extended product. `MUL` returns the low 27 trits (truncated). `MULH` returns the high 27 trits of the 54-trit product.

For a full 54-trit result: `MUL rd_lo, rs1, rs2` then `MULH rd_hi, rs1, rs2`.

The trit-by-trit product table is:

```
a × b → product
N × N →  P    (−1 × −1 = +1)
N × Z →  Z    (−1 ×  0 =  0)
N × P →  N    (−1 × +1 = −1)
Z × Z →  Z
Z × P →  Z
P × P →  P    (+1 × +1 = +1)
```

### 5.4 Integer division (symmetric Euclidean)

Setnex uses **symmetric Euclidean division**, the natural convention for balanced ternary:

- Quotient: `q = round_to_nearest(a / b)`, with ties rounded toward zero.
- Remainder: `r = a − q × b`, satisfying `|r| ≤ |b| / 2`.

This minimizes the magnitude of the remainder, which aligns with the balanced ternary philosophy of keeping values centered on zero.

When `|b|` is odd (including all powers of 3), the tie-break case does not occur and the result is unique.

Division by zero triggers the `EXC_DIV0` exception.

> **Comparison with C convention**: C truncates toward zero, which can produce larger remainders. The symmetric convention is more natural for balanced ternary and simplifies subsequent computations on the remainder.

### 5.5 FLAGS register

The FLAGS CSR (address 3) is updated after every ALU instruction (ADD through TCMP) and after CMP/CMPI. All three flag trits are fully ternary (N/Z/P), not binary.

| Trit | Position | Name | Values |
|------|----------|------|--------|
| t[0] | `sign` | Result sign / comparison trichotomy | N = negative, Z = zero, P = positive |
| t[1] | `overflow` | Overflow direction | N = underflow (wrapped past negative limit), Z = no overflow, P = overflow (wrapped past positive limit) |
| t[2] | `carry` | Carry-out from the most significant trit | N / Z / P = outgoing carry from the balanced ternary adder |
| t[3..26] | — | Reserved (always Z) |

**After ALU instructions** (ADD, SUB, MUL, etc.):
- `sign` = sign(result) : N if result < 0, Z if result = 0, P if result > 0.
- `overflow` = N if the true result was below −(3²⁷−1)/2 (underflow), P if above +(3²⁷−1)/2 (overflow), Z otherwise.
- `carry` = carry-out from trit position 26 of the adder (meaningful for ADD/SUB; Z for other ALU ops).

**After CMP rs1, rs2**:
- `sign` = sign(val(rs1) − val(rs2)) — this is the **trichotomy trit**: N if rs1 < rs2, Z if rs1 = rs2, P if rs1 > rs2.
- `overflow` and `carry` reflect the subtraction rs1 − rs2 internally.

**After CMPI rs1, imm17**: same behavior with the zero-extended immediate in place of rs2.

> The trichotomy trit in FLAGS.sign is the native ternary comparison result — it encodes three outcomes (less, equal, greater) in a single trit, which would require two bits in binary.

---

## 6. Configurable ternary logic (LMODE)

The `LMODE` CSR (address 2) selects the truth tables for logic instructions TAND, TOR, TNOT, and TIMPL. LMODE holds a T27 value; only trit t[0] is significant.

When LMODE=N, the `STATUS.lx` trit (t[2]) selects among three sub-modes. This provides 5 logic modes total using only two trits.

### 6.1 Logic mode map

| LMODE t[0] | STATUS.lx | Logic | Z means… | Notes |
|------------|-----------|-------|----------|-------|
| N | N | Heyting (HT) | not provable | Intuitionistic; NOT and IMPL differ from Łukasiewicz |
| N | Z | Łukasiewicz (Ł) | not yet known | Most tolerant; IMPL = order test |
| N | P | RM3 | both true and false | Paraconsistent (Routley–Meyer); IMPL differs |
| Z | (ignored) | Kleene (K) | undecidable | Default at reset; neutral; SQL 3VL |
| P | (ignored) | B3 (Bochvar) | meaningless | Z infectious: any input Z → output Z |

At reset, LMODE = 0 and STATUS = 0 → Kleene active without explicit configuration.

STATUS.lx is only significant when LMODE=N. When LMODE=Z or LMODE=P, the lx trit is ignored.

### 6.2 Mode descriptions

**Kleene (LMODE=Z)** — the default. AND = min, OR = max, NOT = negation, IMPL(a,b) = OR(NOT(a), b). This is the standard three-valued logic used by SQL for NULL handling. Z propagates through some operations but not all.

**Łukasiewicz (LMODE=N, STATUS.lx=Z)** — shares AND/OR/NOT with Kleene. Differs only on IMPL: IMPL(a,b) = min(P, −a + b + 1). The result is P if and only if a ≤ b, making TIMPL a trit-parallel subsumption test. Most tolerant of indetermination.

**Heyting (LMODE=N, STATUS.lx=N)** — intuitionistic logic. Shares AND/OR with Kleene/Łukasiewicz but **differs on NOT**: NOT_HT(Z) = N, NOT_HT(P) = N (only N maps to P). IMPL uses the Heyting algebra residual: IMPL_HT(a,b) = greatest c such that AND(a, c) ≤ b. Most conservative about the unknown state: "not provable" is treated as false.

**RM3 (LMODE=N, STATUS.lx=P)** — paraconsistent logic (Routley & Meyer). Shares AND/OR/NOT with Kleene/Łukasiewicz. Differs only on IMPL. Z represents "both true and false" — a contradiction that does not explode into arbitrary conclusions. Suitable for reasoning with inconsistent data.

**B3 / Bochvar (LMODE=P)** — Bochvar's internal logic (1937). Z is "meaningless" and **infectious**: any operation with a Z input produces Z. AND, OR, NOT, and IMPL are all affected. On classical inputs (N and P only), B3 reduces to Boolean logic. Most strict: incomplete data produces no conclusion.

> **Hardware cost of STATUS.lx**: When LMODE=Z or LMODE=P, the lx trit is ignored and the datapath is unchanged. When LMODE=N, the TNOT instruction must check STATUS.lx to select between standard NOT (−a) and Heyting NOT. This is a single AND + MUX-2→1 on the NOT output, gated by LMODE=N ∧ STATUS.lx=N. The TIMPL MUX adds one input (RM3) to the existing Łukasiewicz/Heyting selector. Total added cost: negligible.

### 6.3 LMODE-insensitive operations

CONS and ACONS are **always** evaluated using Kleene semantics, regardless of LMODE. They are arithmetic primitives, not logic operations.

CONS extracts the common trit (agreement → value, disagreement → Z).
ACONS extracts the absent trit (agreement → Z, disagreement → the missing trit from {N, Z, P}).
Together they form a complete dual pair for balanced ternary arithmetic circuits.

TCMP is also LMODE-insensitive — it is a pure arithmetic comparison.

### 6.4 Complete truth tables

#### AND

**Kleene / Łukasiewicz / Heyting / RM3 AND** (identical — min):

|AND| N | Z | P |
|---|---|---|---|
| N | N | N | N |
| Z | N | Z | Z |
| P | N | Z | P |

**B3 (Bochvar) AND** — Z infectious:

|AND| N | Z | P |
|---|---|---|---|
| N | N | Z | N |
| Z | Z | Z | Z |
| P | N | Z | P |

#### OR

**Kleene / Łukasiewicz / Heyting / RM3 OR** (identical — max):

|OR| N | Z | P |
|---|---|---|---|
| N | N | Z | P |
| Z | Z | Z | P |
| P | P | P | P |

**B3 (Bochvar) OR** — Z infectious:

|OR| N | Z | P |
|---|---|---|---|
| N | N | Z | P |
| Z | Z | Z | Z |
| P | P | Z | P |

#### NOT

**Kleene / Łukasiewicz / RM3 NOT** (identical — negation):

| a | NOT(a) |
|---|--------|
| N | P |
| Z | Z |
| P | N |

**Heyting NOT**:

| a | NOT(a) |
|---|--------|
| N | P |
| Z | N |
| P | N |

**B3 (Bochvar) NOT** — Z infectious:

| a | NOT(a) |
|---|--------|
| N | P |
| Z | Z |
| P | N |

> Note: B3 NOT has the same table as Kleene NOT. The infectious property of B3 manifests in AND, OR, and IMPL, not in NOT (since NOT is unary and NOT(Z) = Z is already the "contaminated" result).

#### IMPL — all 5 modes are distinct

**Kleene IMPL**: IMPL(a,b) = OR(NOT(a), b) = max(−a, b)

|IMPL| N | Z | P |
|----|---|---|---|
| N  | P | P | P |
| Z  | Z | Z | P |
| P  | N | Z | P |

**Łukasiewicz IMPL**: IMPL(a,b) = min(P, −a + b + 1)

|IMPL| N | Z | P |
|----|---|---|---|
| N  | P | P | P |
| Z  | Z | P | P |
| P  | N | Z | P |

**Heyting IMPL**: IMPL(a,b) = greatest c such that AND(a, c) ≤ b

|IMPL| N | Z | P |
|----|---|---|---|
| N  | P | P | P |
| Z  | N | P | P |
| P  | N | Z | P |

**RM3 IMPL** (paraconsistent):

|IMPL| N | Z | P |
|----|---|---|---|
| N  | P | P | P |
| Z  | N | Z | P |
| P  | N | N | P |

**B3 (Bochvar) IMPL**: IMPL(a,b) = OR_B3(NOT_B3(a), b) — Z infectious:

|IMPL| N | Z | P |
|----|---|---|---|
| N  | P | Z | P |
| Z  | Z | Z | Z |
| P  | N | Z | P |

> All five IMPL tables are distinct. The discriminating cells:
>
> | (a, b) | Kleene | Łukasiewicz | Heyting | RM3 | B3 |
> |--------|--------|-------------|---------|-----|----|
> | (Z, N) | Z | Z | **N** | **N** | **Z** |
> | (Z, Z) | Z | **P** | **P** | Z | **Z** |
> | (N, Z) | P | P | P | P | **Z** |
> | (Z, P) | P | P | P | P | **Z** |
> | (P, N) | N | N | N | N | N |
> | (P, Z) | Z | Z | Z | **N** | **Z** |

---

## 7. Exception handling

### 7.1 Exception causes

| ECAUSE code | Name | Trigger |
|------------|------|---------|
| −13 | `EXC_DIV0` | Division by zero |
| −12 | `EXC_ALIGN` | Misaligned memory access (future) |
| −11 | `EXC_FAULT` | Invalid address |
| −10 | `EXC_ILLEGAL` | Undefined opcode or reserved instruction |
| 0 | `EXC_ECALL` | System call (ECALL instruction) |
| +10 | `EXC_OVERFLOW` | Arithmetic overflow (if enabled in STATUS) |

### 7.2 Exception entry sequence

1. `ESAVE ← STATUS` (save current processor status)
2. `EPC ← PC` (address of the faulting instruction)
3. `ECAUSE ← code`
4. `STATUS.mode ← N` (switch to kernel mode)
5. `STATUS.ie ← N` (disable interrupts)
6. `PC ← EVEC`

The handler reads EPC and ECAUSE via CSRR, processes the exception, then returns via IRET.

### 7.3 Exception return (IRET)

The `IRET` instruction performs an atomic return from exception:

1. `PC ← EPC`
2. `STATUS ← ESAVE`

Both writes are performed atomically — no interrupt can be taken between them. This prevents the STATUS/PC corruption that was possible in v0.1's two-instruction sequence.

> IRET uses opcode −3 (R format). The rd, rs1, rs2, and funct fields are ignored and should be set to zero.

---

## 8. Calling convention (ABI)

### 8.1 Argument passing

- Arguments 1–7: `a0`–`a6` (r10–r16)
- Additional arguments: pushed on the stack (decreasing addresses from `sp`)
- Return values: `a0` (single), `a0`+`a1` (pair)

### 8.2 Callee-saved registers

`s0`–`s10` (r8–r9, r17–r25), `sp` (r2), `ra` (r1).

Caller-saved (may be clobbered across a call): `t0`–`t3` (r5–r7, r26), `a0`–`a6`.

### 8.3 Stack

The stack grows toward negative addresses. `sp` points to the top (last valid word).
Standard prologue:
```asm
  ADDI   t0, sp, -1      # address for saving ra
  STORE  ra, t0, 0        # save ra
  ADDI   s0, sp, 0        # frame pointer = old sp
  ADDI   sp, sp, -N       # allocate frame (N = frame size)
```

### 8.4 Pseudo-instructions (assembler)

| Pseudo | Expansion | Notes |
|--------|-----------|-------|
| `RET` | `JMPA ra, 0` | Return from subroutine |
| `MOV rd, rs` | `ADD rd, rs, zero` | Register copy |
| `NEG rd, rs` | native `NEG rd, rs` (R format, rs2/funct ignored) | Balanced ternary negation |
| `CALL label` | `CALL offset23(label)` | Compute offset from PC |
| `LI rd, imm` | `LI` if fits imm17; else `LUI` + `ADDI` | Load arbitrary immediate |
| `TSET rd, rs1, rs2` | `TSETZ rd, rs1, rs2` | Alias: clear trit to Z |
| `TNIMPL rd, a, b` | `TNOT t0, b` then `TAND rd, a, t0` | Non-implication: a AND NOT b |
| `TREIMPL rd, a, b` | `TIMPL rd, b, a` | Reverse implication: b ⇒ a |
| `NOT rd, rs` | `TNOT rd, rs` | Mnemonic alias for clarity |
| `BFLT label` | `BF P00, label` | Branch if FLAGS.sign = N (less than) |
| `BFEQ label` | `BF 0P0, label` | Branch if FLAGS.sign = Z (equal) |
| `BFGT label` | `BF 00P, label` | Branch if FLAGS.sign = P (greater than) |
| `BFLE label` | `BF PP0, label` | Branch if FLAGS.sign ≤ Z (less or equal) |
| `BFGE label` | `BF 0PP, label` | Branch if FLAGS.sign ≥ Z (greater or equal) |
| `BFNE label` | `BF P0P, label` | Branch if FLAGS.sign ≠ Z (not equal) |

---

## 9. Reference encoding (textual representation)

Trits are written using the `-`/`0`/`+` convention.

In **memory layout** (LST-first): t[0] is stored and written first. Instruction encoding diagrams use this convention.

In **human-readable display** (MST-first): the most significant trit is written leftmost, as in ordinary number notation. Register addresses and integer literals use this convention.

Each convention is explicitly labeled.

**Example — `ADD r3, r1, r2`** (R format)

Field values:
- opcode ADD = −40 = enc(−40, 4):
  −40 ÷ 3 → q = −13, r = −1 → t[0] = N;
  −13 ÷ 3 → q = −4, r = −1 → t[1] = N;
  −4 ÷ 3 → q = −1, r = −1 → t[2] = N;
  −1 ÷ 3 → q = 0, r = −1 → t[3] = N.
  Result: `----` (LST-first). Verify: −1 −3 −9 −27 = −40 ✓

- rd = r3 = enc(3, 3): 3 ÷ 3 → q=1, r=0 → t[0]=Z; 1 ÷ 3 → q=0, r=1 → t[1]=P; t[2]=Z → LST-first: `0+0`
- rs1 = r1 = enc(1, 3): t[0]=P, t[1]=Z, t[2]=Z → LST-first: `+00`
- rs2 = r2 = enc(2, 3): 2 ÷ 3 → q=1, r=−1 → t[0]=N; 1 ÷ 3 → q=0, r=1 → t[1]=P; t[2]=Z → LST-first: `-+0`
- funct = 0 (14 trits of Z)

```
LST-first memory layout:

t[0-3]  t[4-6]  t[7-9]  t[10-12]  t[13-26]
----    0+0     +00     -+0       00000000000000
```

Full 27-trit word (LST-first): `----0+0+00-+000000000000000`

The opcode sits at t[0]–t[3] — the decoder starts working as soon as the first trits arrive.

---

## 10. Architectural summary

| Parameter | Value |
|-----------|-------|
| Word width | 27 trits |
| General-purpose registers | 27 (r0–r26), T3 address |
| CSR registers | 27 addressable (T3), 8 defined |
| Instruction width | 27 trits (fixed) |
| Instruction formats | 5 (R, I, J, U, B) |
| Opcode | 4 trits (81 values, 46 used in v0.3) |
| Address space | ±(3²⁷−1)/2 words ≈ ±3.6 × 10¹² |
| Max immediate (I format) | 17 trits ≈ ±64 million |
| Max branch offset (J format) | 20 trits ≈ ±1.7 billion |
| Max jump offset (U format) | 23 trits ≈ ±4.7 × 10¹⁰ |
| Logic modes | 5: Kleene (default), Łukasiewicz, Heyting, RM3, B3 (Bochvar) |
| Arithmetic flags | 3 ternary: sign, overflow, carry |
| Division convention | Symmetric Euclidean |
| Memory unit | 1 word = 27 trits |
| Endianness | Least significant trit first (little-endian) |

---

## 11. Complete opcode map

Quick-reference table, sorted by opcode value.

| Opcode | Mnemonic | Format | Group |
|--------|----------|--------|-------|
| −40 | ADD / ADDS / ADC | R | ALU |
| −39 | SUB / SUBS / SBC | R | ALU |
| −38 | MUL / MULH | R | ALU |
| −37 | DIV | R | ALU |
| −36 | MOD | R | ALU |
| −35 | NEG | R | ALU |
| −34 | TAND | R | ALU / Logic |
| −33 | TOR | R | ALU / Logic |
| −32 | TNOT | R | ALU / Logic |
| −31 | TIMPL | R | ALU / Logic |
| −30 | CONS | R | ALU / Trit |
| −29 | ACONS | R | ALU / Trit |
| −28 | TSHIFT | R | ALU / Trit |
| −27 | TCMP | R | ALU / Trit |
| −26 | LOAD | I | Memory |
| −25 | STORE | I | Memory |
| −24 | LI | I | Memory |
| −23 | LUI | I | Memory |
| −22 | ADDI | I | Memory |
| −21 | BRT3 | B | Branch |
| −20..−19 | — | — | reserved |
| −18 | CMPI | I | Memory |
| −17 | BEQ | J | Branch |
| −16 | BNE | J | Branch |
| −15 | BLT | J | Branch |
| −14 | BGT | J | Branch |
| −13 | BLE | J | Branch |
| −12 | BGE | J | Branch |
| −11 | JMPA | J | Branch |
| −10 | BF | J | Branch |
| −9 | JMP | U | Jump |
| −8 | CALL | U | Jump |
| −7 | CSRR | I | System |
| −6 | CSRW | I | System |
| −5 | CSRX | I | System |
| −4 | ECALL | I | System |
| −3 | IRET | R | System |
| −2 | TSEL | R | Special |
| −1 | NOP | — | Special |
| 0 | HALT | — | Special |
| +1 | TGET | R | Trit ops |
| +2 | TSET(N/Z/P) | R | Trit ops |
| +3 | TSIGN | R | Trit ops |
| +4 | CMP | R | Trit ops |
| +5 | TABS | R | Trit ops |
| +6 | TMIN | R | Trit ops |
| +7 | TMAX | R | Trit ops |
| +8..+14 | — | — | reserved (TFP) |
| +15..+24 | — | — | reserved (Vector) |
| +25..+40 | — | — | reserved (Custom) |

---

## 12. Extension roadmap

### v0.4 — Floating-point, vector, and system extensions

| Extension | Opcode range | Rationale |
|-----------|-------------|-----------|
| Ternary floating-point (TFP) | +8 to +14 | Native ternary float; Tekum balanced ternary tapered precision (arXiv:2512.10964) as reference format. |
| Vector instructions | +15 to +24 | Trit-parallel operations on 27-trit lanes. |
| Memory protection (MPU) | extended CSR | User/kernel address space enforcement. |
| Interrupt controller | extended CSR | Priority-based interrupt dispatch, nested exceptions. |

### v0.5 — Hardware target

| Milestone | Notes |
|-----------|-------|
| FPGA soft core | VHDL or Verilog description of Setnex; each trit encoded as 2 bits on FPGA fabric, ternary signals on external bus (same approach as 5500FP). Target: iCE40 (open toolchain via nextpnr/yosys) or Xilinx/Intel. |
| Assembler tooling | Setnex assembler in Python, building on tritlib. |
| ASIC exploration | Contingent on CNT or memristor ternary gate availability; long-term. |

---

*Setnex ISA v0.3 — Reference specification*
*Setnex project / Terias — Eric Tellier*
