Analytical Combinator

by bruno77

A programmable circuit combinator controlled via a RISC-V inspired assembly language. Supports reading red and green input signals and writing up to four output signals per tick.

Content
4 days ago
2.0
70
Circuit network
Owner:
bruno77
Source:
https://github.com/bruno7seven/analyt...
Homepage:
N/A
License:
MIT
Created:
21 days ago
Latest Version:
0.8.30 (4 days ago)
Factorio version:
2.0
Downloaded by:
70 users

Analytical Combinator

A Factorio mod adding a programmable circuit combinator controlled via a RISC-V inspired assembly language.

Credits / Attribution

Portions of this project are derived from the Assembly Combinator by Joakim Lönnegren (joalon), and remain subject to the MIT License.

For contributions made by the Analytical Combinator contributors, we do not require attribution and will not enforce the inclusion of our copyright notice in redistributions, although the MIT License text still applies to the project as a whole.

Much credit for my changes goes to Anthropic's Claude AI. In addition to doing all of the coding and documentation, Claude suggested the name "Analytical Combinator" because it has a 19th-century engineering feel that fits the game's aesthetic, and nods to Babbage's Analytical Engine.

Usage

Download it from the mod portal.

The analytical combinator is more like a Decider Combinator in that it supports both input (red and green wires) and output circuit network connections.

Instruction set

The instruction set is "inspired" by the RISC-V assembly language, but I've chosen not to be constrained by a 32-bit processor architecture or instruction length. For example, the immediate operand in the ADDI instruction isn't limited to 12 bits as on a real RISC processor.

I've also added branch immediate instructions where a full 32-bit immediate value along with the jump location are in the instruction. These aren't actual RISC-V instructions since they aren't implementable with the RISC-V instruction format.

Arithmetic

Instruction Syntax Description
ADDI rd, rs, imm rd = rs + imm — add immediate constant
LI rd, imm rd = imm — load immediate (shorthand for ADDI rd, x0, imm)
ADD rd, rs, rt rd = rs + rt — add two registers
SUB rd, rs, rt rd = rs - rt — subtract register
MUL rd, rs, rt rd = rs * rt — multiply two registers
MULI rd, rs, imm rd = rs * imm — multiply by immediate
DIV rd, rs, rt rd = floor(rs / rt) — integer division; error on divide-by-zero
DIVI rd, rs, imm rd = floor(rs / imm) — divide by immediate; error on zero
REM rd, rs, rt rd = rs % rt — remainder; sign follows dividend (C-style)
REMI rd, rs, imm rd = rs % imm — remainder by immediate

Comparison

Instruction Syntax Description
SLT rd, rs, rt rd = (rs < rt) ? 1 : 0
SLTI rd, rs, imm rd = (rs < imm) ? 1 : 0

Bitwise

Instruction Syntax Description
AND rd, rs, rt rd = rs & rt — bitwise AND
ANDI rd, rs, imm rd = rs & imm — bitwise AND with immediate
OR rd, rs, rt rd = rs | rt — bitwise OR
ORI rd, rs, imm rd = rs | imm — bitwise OR with immediate
XOR rd, rs, rt rd = rs ^ rt — bitwise XOR
XORI rd, rs, imm rd = rs ^ imm — bitwise XOR with immediate
NOT rd, rs rd = ~rs — bitwise NOT (unary)

Shifts

Shift instructions follow the RISC-V naming convention. Left shifts are always logical (zero-fill). Right shifts come in two flavours — logical (zero-fill from the left, use when treating the value as unsigned) and arithmetic (sign-extend, use when treating the value as a signed integer).

Instruction Syntax Description
SLL rd, rs, rt rd = rs << rt — shift left logical by register
SLLI rd, rs, imm rd = rs << imm — shift left logical by immediate
SRL rd, rs, rt rd = rs >> rt — shift right logical (zero-fill) by register
SRLI rd, rs, imm rd = rs >> imm — shift right logical (zero-fill) by immediate
SRA rd, rs, rt rd = rs >> rt — shift right arithmetic (sign-extend) by register
SRAI rd, rs, imm rd = rs >> imm — shift right arithmetic (sign-extend) by immediate

Control flow

Instruction Syntax Description
JAL rd, label Jump to label; save address of next instruction in rd (use x0 to discard)
JR rs Jump to address in rs — subroutine return. JR x0 restarts from line 1.
BEQ rs, rt, label Branch if rs == rt
BNE rs, rt, label Branch if rs != rt
BLT rs, rt, label Branch if rs < rt
BLE rs, rt, label Branch if rs <= rt
BGT rs, rt, label Branch if rs > rt
BGE rs, rt, label Branch if rs >= rt
BEQI rs, imm, label Branch if rs == imm (immediate)
BNEI rs, imm, label Branch if rs != imm
BLTI rs, imm, label Branch if rs < imm
BLEI rs, imm, label Branch if rs <= imm
BGTI rs, imm, label Branch if rs > imm
BGEI rs, imm, label Branch if rs >= imm

Circuit network output

Instruction Syntax Description
WSIG od, signal, rs Write signal to output channel od (o0–o3) with count from rs
WSIGI od, signal, imm Write signal to output channel od (o0–o3) with a constant count

Circuit network input

Instruction Syntax Description
RSIG rd, signal Read named signal from both wires and store the sum in rd
RSIGR rd, signal Read named signal from the red input wire into rd (0 if absent)
RSIGG rd, signal Read named signal from the green input wire into rd (0 if absent)
CNTSR rd Set rd to the count of distinct signals on the red input wire
CNTSG rd Set rd to the count of distinct signals on the green input wire

Signal names are validated against Factorio's prototype tables when the program is saved. An unknown signal name is flagged immediately rather than silently returning 0 at runtime.

Control

Instruction Syntax Description
WAIT imm or rs Stall for N game ticks (60 ticks = 1 second)
NOP No operation
HLT Halt execution

Registers

  • x0x31: general-purpose integer registers. x0 is always 0 (writes ignored). The alternate ABI register names are not supported - sorry.
  • o0o3: output signal registers, written by WSIG, emitted on the output network each tick.

Instruction case

Mnemonics are case-insensitive — ADDI, addi, and Addi are all accepted. Register names (x0x31, o0o3) and signal names (iron-plate, signal-A, etc.) remain case-sensitive.

Immediate value formats

All instructions that take an immediate (imm) argument accept integers in decimal, hexadecimal (0x prefix), or negative decimal. Octal (0-prefix) is technically accepted by the Lua parser but best avoided.

ADDI x10, x0, 255       # decimal
ADDI x10, x0, 0xFF      # hex — same value, preferred for bitmasks
ADDI x10, x0, -1        # negative decimal
SHLI x11, x10, 0x4      # shift amount as hex (unusual but valid)

Hex is especially useful with bitwise instructions:

ADDI  x10, x0,  0xFF00FF   # load a bitmask
AND   x11, x12, x10        # apply the mask
SRLI  x11, x11, 0x8        # extract middle byte

Example programs

The following examples are simply meant to illustrate what is possible with Analytical Combinators. I realize some of these use-cases may be easy to implement with items such as Decider Combinators.

Simple counter (output only)

main:
    ADDI x10, x0, 0             # Initialize counter to 0
loop:
    ADDI x10, x10, 1            # Increment counter
    WSIG o1, copper-plate, x10  # Output counter value
    WAIT 60                     # Wait 1 second (60 game ticks)
    SLTI x6, x10, 100           # Check if counter < 100
    BNE  x6, x0, loop           # Branch if not equal to zero
    JAL  x1, main               # Jump back to main

Threshold gate (read input, control output)

Reads an iron-ore count from the green wire. Once it exceeds 500, outputs signal-A = 1 on the output connector and halts. The signal appears on whichever wire(s) — red, green, or both — are physically connected to the output side of the combinator.

poll:
    RSIGG x10, iron-ore         # Read iron-ore from green wire
    SLTI  x6, x10, 500          # x6 = 1 if count < 500
    BNE   x6, x0, poll          # Keep polling until threshold met
    ADDI  x11, x0, 1
    WSIG  o0, signal-A, x11     # Emit signal-A = 1
    HLT

Subroutine call with JAL / JR

JAL rd, label saves the address of the next instruction into rd, then jumps to label. JR rd jumps to the address in rd, returning execution to the instruction after the call site. Use a different link register for each call frame. Recursive calls are not supported (no stack), but sequential calls to shared subroutines work cleanly.

JR x0 is a special case: since x0 is always 0 and 0 is not a valid line number, it is defined to restart the program from line 1.

main:
    RSIGG x10, iron-plate
    JAL   x1, clamp_255          # x1 = return address; jump to clamp_255
    WSIG  o0, iron-plate, x10    # resumes here after return
    RSIGG x10, copper-plate
    JAL   x1, clamp_255          # reuse the same subroutine and same link register
    WSIG  o1, copper-plate, x10
    WAIT  60
    JR    x0                     # restart from line 1 (same as JAL x0, main if main: is on line 1)

clamp_255:                       # expects value in x10, returns clamped value in x10
    SLTI  x6,  x10, 256          # x6 = 1 if value already in range
    BNE   x6,  x0,  clamp_ret    # skip clamp if already in range
    ADDI  x10, x0,  255          # clamp to 255
clamp_ret:
    JR    x1                     # return to caller

Bit masking — extract low byte

Factorio signals are 32-bit integers. Use AND to isolate the lower 8 bits, useful if you are packing multiple small values into a single signal channel.

    RSIGR x10, signal-A         # Read packed value from red wire
    ADDI  x11, x0,  255         # Mask = 0xFF
    AND   x12, x10, x11         # x12 = low byte of signal-A
    SRLI  x13, x10, 8           # x13 = next byte up (logical: zero-fills upper bits)
    WSIG  o0, signal-A, x12     # Output low byte
    WSIG  o1, signal-B, x13     # Output second byte
    HLT

Signal presence detection with CNTSG

Fire signal-A when anything appears on the green wire (useful as a "something arrived" trigger without caring what the signal is).

poll:
    CNTSG x10                   # x10 = number of distinct signals on green
    BEQ   x10, x0, poll         # Loop while nothing is present
    ADDI  x11, x0, 1
    WSIG  o0, signal-A, x11     # Trigger output
    WAIT  60                    # Hold for 1 second
    ADDI  x11, x0, 0
    WSIG  o0, signal-A, x11     # Clear output
    JAL   x0, poll

Red/green signal sum

Reads a signal from both wires each tick and outputs their sum. This is one of those cases where Decider Combinators do the job nicely. Note that this example does individual reads from each circuit. The next example uses the RSIG instruction which sums the inputs from both circuits.

loop:
    RSIGR x10, iron-plate       # Read iron-plate from red wire
    RSIGG x11, iron-plate       # Read iron-plate from green wire
    ADD   x12, x10, x11         # x12 = red + green total
    WSIG  o0, iron-plate, x12   # Output the combined count
    WAIT  1
    JAL   x0, loop

Red/green sum with threshold gate

Same idea, but halts and fires a signal once the combined total exceeds 500.

loop:
    RSIG  x12, iron-plate       # Read iron-plate from both circuits
    SLTI  x6,  x12, 500         # x6 = 1 if total < 500
    BNE   x6,  x0,  loop        # Keep polling until threshold met
    LI    x11, 1
    WSIG  o0,  signal-A, x11    # Emit signal-A = 1
    HLT