IC10 Data Stack, sp/ra Registers, and push/pop Semantics
The IC10 ProgrammableChip exposes a 512-entry data stack plus a stack-pointer register (sp) and a return-address register (ra). push/pop/peek/poke/get/put and the jal family all read and mutate these. This page records the exact array size, register indices, mutation order, and the out-of-range behavior, because a script that uses the stack for argument passing and for protecting ra across nested calls depends on every one of these details.
Stack array, sp, and ra register layout¶
The chip holds 18 registers and a 512-entry stack (ProgrammableChip fields):
private readonly double[] _Registers = new double[18]; // line 393250
private readonly double[] _Stack = new double[512]; // line 393256
private readonly int _StackPointerIndex = 16; // line 393252 -> sp == r16
private readonly int _ReturnAddressIndex = 17; // line 393254 -> ra == r17
- The data stack is
double[512]: valid indices0through511. spis registerr16. It is aliased to the namespat compile time (new _ALIAS_Operation(this, 0, "sp", $"r{_StackPointerIndex}"), line 393704).spholds the index where the NEXTpushwill write; it starts at 0.rais registerr17, aliased tora(line 393705).rais an ordinary register:jaloverwrites it, and nothing auto-saves it across a nestedjal. Protectingraacross a nested call is the script author's responsibility (push ra/pop ra).GetStackSize()returns_Stack.Length, i.e. 512 (line 394313-394316). This is also what theStackSizeLogicType reads.- On reset/recompile,
spis set to 0 (_Registers[_StackPointerIndex] = 0.0;at lines 393694 and 394214).
push: write-then-increment¶
_PUSH_Operation.Execute (lines 392473-392492) reads sp, range-checks, writes _Stack[sp], then increments sp:
public override int Execute(int index)
{
double variableValue = _Argument1.GetVariableValue(_AliasTarget.Register);
int num = (int)Math.Round(_Chip._Registers[_Chip._StackPointerIndex]);
if (num < 0)
{
throw new ProgrammableChipException(ProgrammableChipException.ICExceptionType.StackUnderFlow, _LineNumber);
}
if (num >= _Chip._Stack.Length)
{
throw new ProgrammableChipException(ProgrammableChipException.ICExceptionType.StackOverFlow, _LineNumber);
}
_Chip._Stack[num] = variableValue;
_Chip._Registers[_Chip._StackPointerIndex] += 1.0; // sp incremented AFTER the write
...
return index + 1;
}
- The range check is on the PRE-increment
sp.pushwithsp == 512throwsStackOverFlow(index 512 is out of range). The last valid push is atsp == 511, leavingsp == 512afterward. pushwithsp < 0throwsStackUnderFlow(only reachable ifspwas already corrupted negative).spis rounded (Math.Round) before use, so fractionalspvalues snap to the nearest integer index.
pop: decrement-then-read¶
_POP_Operation derives from _PEEK_Operation but overrides Execute to decrement first (lines 392413-392431):
public override int Execute(int index)
{
_Chip._Registers[_Chip._StackPointerIndex] -= 1.0; // sp decremented FIRST
int variableIndex = _Store.GetVariableIndex(_AliasTarget.Register);
int num = (int)Math.Round(_Chip._Registers[_Chip._StackPointerIndex]);
if (num < 0)
{
throw new ProgrammableChipException(ProgrammableChipException.ICExceptionType.StackUnderFlow, _LineNumber);
}
if (num >= _Chip._Stack.Length)
{
throw new ProgrammableChipException(ProgrammableChipException.ICExceptionType.StackOverFlow, _LineNumber);
}
_Chip._Registers[variableIndex] = _Chip._Stack[num];
...
return index + 1;
}
popdecrementsspFIRST, then range-checks and reads_Stack[sp].popon an empty stack (sp == 0) throwsStackUnderFlow:spbecomes-1, thenum < 0check fires. The chip does NOT return 0 and does NOT wrap. Critically,sphas ALREADY been decremented to-1when the exception is thrown, so the corrupted value is visible if the chip is later resumed without a reset.peek(_PEEK_Operation, lines 392442-392460) reads_Stack[sp - 1]WITHOUT modifyingsp; it throwsStackUnderFlowwhensp == 0(computed index-1 < 0).
Out-of-range behavior: error-halt, not wrap or zero¶
A stack range violation throws ProgrammableChipException with type StackUnderFlow or StackOverFlow. The chip's run loop (ProgrammableChip.Execute(int runCount), lines 393771-393815) catches it:
catch (ProgrammableChipException ex)
{
CircuitHousing?.RaiseError(1);
_ErrorLineNumber = ex.LineNumber;
_ErrorType = ex.ExceptionType;
_NextAddr = nextAddr; // rewind to the faulting line; do NOT advance
break;
}
Consequences:
- The housing's error flag is raised (
RaiseError(1)),_ErrorLineNumber/_ErrorTyperecord the fault,_NextAddris rewound to the faulting instruction, and the run loopbreaks. - The chip is now in an error state and stops executing.
CircuitHousing.Execute()runs the chip only when!ProgrammableChip.CompilationErrorand the housing is powered/on; a raised runtime error halts useful progress until the chip is reset or re-flashed. It does NOT silently continue, does NOT return 0 for the bad pop, and does NOT wrap the index. - The same three exception types are surfaced for the device-memory variants too:
put/putdwrapIMemoryWritable.WriteMemory(lines 392581-392601, 392655-392675) and translateStackUnderflowException/StackOverflowExceptioninto the sameICExceptionType.StackUnderFlow/StackOverFlow.ReadMemory/WriteMemorythemselves throw onaddress < 0oraddress >= _Stack.Length(lines 394279-394303).
Note there are two distinct exception families in the source: the chip-internal ProgrammableChipException (thrown directly by push/pop/peek) and the StackUnderflowException/StackOverflowException SystemExceptions (thrown by ReadMemory/WriteMemory and re-mapped by put/putd). Both end the run-loop iteration with an error.
jal / j / branch-to-ra and how ra is clobbered¶
jal target(_JAL_Operation.Execute, lines 391415-391419) setsra = index + 1(the line after thejal) and then jumps:
public override int Execute(int index)
{
_Chip._Registers[_Chip._ReturnAddressIndex] = index + 1;
return base.Execute(index); // _J_Operation: jump to target
}
j target(_J_Operation, lines 391201-391211) jumps without touchingra.- A branch whose jump target is the register
ra(e.g.j ra,beq r0 1 ra) jumps to whatever line number is currently inra. This is the return mechanism. - Because
jalunconditionally overwritesra, calling a second subroutine withjalfrom inside the first destroys the first's return address. A subroutine that calls another viajaland still needs toj raafterward MUST bracket the inner call withpush ra/pop ra. There is no hardware call stack;rais a single shared register. - The
*albranch variants (beqzal,bdseal, etc.) setra = index + 1only when the branch is taken (if (hasJumped), e.g._BDSEAL_Operation, lines 391429-391437).
Verification History¶
- 2026-05-31: Page created. Decompiled Assembly-CSharp.dll v0.2.6228.27061. Confirmed stack is
double[512](indices 0-511),sp == r16,ra == r17,_Registersisdouble[18]. Confirmed push is write-then-increment with pre-increment range check (overflow atsp == 512), pop is decrement-then-read (underflow atsp == 0, withspleft at-1). Confirmed range violations throwProgrammableChipException(StackUnderFlow / StackOverFlow), which the run loop catches, rewinds_NextAddr, raises the housing error, and breaks: error-halt, not return-0 or wrap. Confirmedjalunconditionally writesra = index+1andj/branch-to-ra do not, so nestedjalrequires manualpush ra/pop ra. Sourced while adversarially reviewing an airlock+forcefield IC10 script for stack-discipline defects.
Open Questions¶
None at present.