Click Ladder CSV Contract (v1)
This document specifies the CSV contract emitted by Click ladder export:
- API entrypoint:
TagMap.to_ladder(program) - File writer:
LadderBundle.write(directory)
It is intended for implementers building a CSV consumer/decoder.
Scope and guarantees
This is a producer contract for what pyrung emits, not a general parser spec for arbitrary CSVs.
- Deterministic output
- Fully expanded rows (no shorthand expansion required)
- Strict prevalidation before emit
- All-or-nothing export (on failure, no bundle is emitted)
Output files
LadderBundle.write(directory) emits:
main.csv- One file per subroutine:
sub_<slug>.csv
Rules:
- Subroutines are emitted in lexical order by subroutine name.
- Output directory is auto-created (
parents=True, exist_ok=True). - Existing files are overwritten.
- Slug generation:
- Lowercase
- Non-alphanumeric sequences become
_ - Leading/trailing
_trimmed - Empty slug becomes
subroutine - Collisions are suffixed (
_2,_3, ...)
CSV shape
- UTF-8 CSV (standard comma-separated, quoted as needed by CSV writer)
- Header is always present and exact:
- Exactly 33 columns per row:
marker+A..AE(31 condition columns) +AF(output token)
Row semantics
marker:R=> first row of a rung""(blank) => continuation row of current rungAF:- exactly one token or blank
- blank means no output token on that row
Rung segmentation rule for consumers:
- A rung starts at each row with
marker == "R"and continues until the nextRor EOF.
Comment rows
Comment rows appear directly above the R row of the rung they annotate:
marker=#- Column
A= comment text for that line - No additional columns
Multi-line comments emit one # row per line. Example:
Comment rows are metadata — consumers may ignore them or display them as rung annotations.
Condition grid cell vocabulary (A..AE)
Cells can contain:
- Contact/operand tokens (for example
X001,DS10,C1) - Negated contact:
~X001 - Edge contacts:
rise(X001),fall(X001) - Comparison terms (for example
DS1!=0,DS1==5,DS1<DS2) - Wiring symbols:
-horizontal-only wireThorizontal + vertical-down wire|vertical-only wire (reserved; currently not emitted because exporter does not output empty vertical-only rows yet)- Blank (
"") empty cell
No shorthand markers (->, ...) are emitted.
No explicit + topology token is emitted in v1.
OR / branch wiring semantics
any_of(...) OR expansion
For OR-expanded condition terms:
- Split/merge marker column uses
Ton non-final stacked rows and-on the final stacked row. - Only the top OR branch row carries trailing downstream condition terms.
- Lower OR continuation rows end at split/merge marker (with wire fill where applicable).
branch(...) rows
Branch rows are continuation rows with normal instruction tokens in AF.
- Branch-local conditions are offset to the right of the parent split column.
- Parent split column is wired with
Ton non-final stacked rows and-on the final stacked row across parent + branch entry rows. - Nested branches are not emitted (export error).
Multi-output rung semantics
If one condition path has multiple output instructions, exporter emits stacked continuation rows:
- First row
marker = R, then blank marker rows - Split column uses
Ton non-final stacked rows and-on the final stacked row - Each row has one
AFtoken
Builder pin continuation rows
Builder side conditions are emitted as continuation rows with dot tokens in AF:
.reset().down().clock().jump(step).jog()
Pin rows are independent left-rail paths (not AND-chained through the parent output row conditions).
For-loop lowering
forloop(count, oneshot=...) lowers to:
for(count,oneshot)row (marker=R)- Body instruction rows (
marker=Rper emitted body instruction row) - Closing
next()row (marker=R)
Subroutine tail guarantee
Each subroutine CSV is guaranteed to end with return():
- If last emitted instruction token is already
return(), unchanged. - Otherwise exporter appends an
Rrow withreturn().
AF token format (canonical)
All tokens are compact canonical function-style strings:
name(arg1,arg2,...)- no extra whitespace
- dot pins as
.name(...)
String rendering:
- Strings are double-quoted.
- Internal
"is escaped as""(doubled quote). No backslash escaping.
Boolean rendering:
1/0
None rendering:
none
Collections:
- List/tuple-like values render as bracket lists, for example
[A,B],[[1,0],[0,1]].
Supported instruction tokens (v1)
Producer may emit:
out(target,oneshot)latch(target)reset(target)copy(source,target,oneshot)blockcopy(source,dest,oneshot)fill(value,dest,oneshot)calc(expression,dest,mode,oneshot)search("cond",value,range,result,found,continuous,oneshot)pack_bits(bit_block,dest,oneshot)pack_words(word_block,dest,oneshot)pack_text(source_range,dest,allow_whitespace,oneshot)unpack_to_bits(source,bit_block,oneshot)unpack_to_words(source,word_block,oneshot)on_delay(done,acc,preset,unit,has_reset)off_delay(done,acc,preset,unit)count_up(done,acc,preset)count_down(done,acc,preset)shift(bit_range)event_drum(outputs,events,pattern,current_step,completion_flag)time_drum(outputs,presets,unit,pattern,current_step,accumulator,completion_flag)send("host",port,"remote_start",source,sending,success,error,exception_response,device_id,count)receive("host",port,"remote_start",dest,receiving,success,error,exception_response,device_id,count)call("subroutine_name")- Subroutine names must not contain
". return()for(count,oneshot)next()
Pin tokens:
.reset().down().clock().jump(step).jog()
Click supports additional instruction placeholders that pyrung does not currently emit:
- Empty instruction placeholder:
,:,... - NOP instruction placeholder:
,:,NOP
Operand normalization notes
- Tags render as mapped Click addresses (for example
X001,DS10). - Block ranges render either:
- contiguous compact form
BANKstart..BANKend(same bank, +1 sequence), or - explicit list form
[A,B,C]. - Indirect refs render as
BANK[pointer]orBANK[pointer+offset]/BANK[pointer-offset]. - Copy modifiers are emitted inline as nested operands:
as_value(source)as_ascii(source)as_binary(source)as_text(source,suppress_zero,pad,exponential,termination_code)
Immediate handling
Immediate operands are supported only in strict, explicit contexts.
Allowed condition-cell forms:
immediate(X001)~immediate(X001)
Allowed AF token forms:
out(immediate(Y001),0)latch(immediate(Y001))reset(immediate(Y001))out(immediate(Y001..Y004),0)(contiguous mapped range only)
Rules:
Tag.immediateandimmediate(...)wrapper style are both supported.- Immediate is allowed only for:
- direct rung contacts (normal and negated), and
out(...),latch(...),reset(...)target operands.- Immediate is not allowed in:
- edge contacts (
rise(...),fall(...)), - non-coil instruction operands (
copy,calc,search, etc.). - Immediate coil targets must resolve to
Ybank addresses. - Immediate-wrapped ranges must resolve to one contiguous address span to emit compact
BANKstart..BANKendform. - Non-contiguous mappings fail strict validation/export with explicit diagnostics.
Strict validation and failure mode
Before rendering, exporter runs strict Click validation and extra export checks.
On any issue:
LadderExportErroris raised- includes structured
issuesentries (path,message,source_file,source_line) - no CSV bundle is returned/written
Consumer recommendations
- Validate exact header and column count (33).
- Parse in row order and preserve ordering semantics.
- Treat
AFas an opaque canonical token string unless your decoder intentionally parses token grammar. - Treat unknown future token names as extension points (fail closed if strict).
- Segment rungs by
marker == "R".