Program
pyrung.core.program
Program and Rung context managers for the immutable PLC engine.
Provides DSL syntax for building PLC programs:
with Program() as logic:
with Rung(Button):
out(Light)
runner = PLCRunner(logic)
CountDownBuilder
Bases: _BuilderBase
Builder for count_down instruction with chaining API (Click-style).
Supports required .reset() chaining: count_down(done, acc, preset=25).reset(reset_tag)
reset
Add reset condition (required).
When reset condition is true, loads preset into accumulator and clears done bit.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
*conditions
|
Condition | Tag | tuple[Condition | Tag, ...] | list[Condition | Tag]
|
Condition(s) for resetting the counter. |
()
|
Returns:
| Type | Description |
|---|---|
Tag
|
The done bit tag. |
CountUpBuilder
Bases: _BuilderBase
Builder for count_up instruction with chaining API (Click-style).
Supports optional .down() and required .reset() chaining: count_up(done, acc, preset=100).reset(reset_tag) count_up(done, acc, preset=50).down(down_cond).reset(reset_tag)
down
down(
*conditions: Condition
| Tag
| tuple[Condition | Tag, ...]
| list[Condition | Tag],
) -> CountUpBuilder
Add down trigger (optional).
Creates a bidirectional counter that increments on rung true and decrements on down condition true.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
*conditions
|
Condition | Tag | tuple[Condition | Tag, ...] | list[Condition | Tag]
|
Condition(s) for decrementing the counter. |
()
|
Returns:
| Type | Description |
|---|---|
CountUpBuilder
|
Self for chaining. |
reset
Add reset condition (required).
When reset condition is true, clears both done bit and accumulator.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
*conditions
|
Condition | Tag | tuple[Condition | Tag, ...] | list[Condition | Tag]
|
Condition(s) for resetting the counter. |
()
|
Returns:
| Type | Description |
|---|---|
Tag
|
The done bit tag. |
OffDelayBuilder
Bases: _AutoFinalizeBuilderBase
Builder for off_delay instruction (TOF behavior, Click-style).
Auto-resets when re-enabled.
OnDelayBuilder
Bases: _AutoFinalizeBuilderBase
Builder for on_delay instruction with optional .reset() chaining (Click-style).
Without .reset(): TON behavior (auto-reset on rung false, non-terminal) With .reset(): RTON behavior (manual reset required, terminal)
reset
Add reset condition (makes timer retentive - RTON).
When reset condition is true, clears both done bit and accumulator.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
*conditions
|
Condition | Tag | tuple[Condition | Tag, ...] | list[Condition | Tag]
|
Condition(s) for resetting the timer. |
()
|
Returns:
| Type | Description |
|---|---|
Tag
|
The done bit tag. |
ShiftBuilder
Branch
Context manager for a parallel branch within a rung.
A branch executes when both the parent rung conditions AND the branch's own conditions are true.
Example
with Rung(Step == 0): out(Light1) with branch(AutoMode): # Only executes if Step==0 AND AutoMode out(Light2) copy(1, Step, oneshot=True)
ForLoop
Context manager for a repeated instruction block within a rung.
Program
Container for PLC logic (rungs and subroutines).
Used as a context manager to capture rungs
with Program() as logic: with Rung(Button): out(Light)
Also works with PLCRunner
runner = PLCRunner(logic)
call_subroutine
Execute a subroutine by name (legacy state-based API).
call_subroutine_ctx
Execute a subroutine by name within a ScanContext.
register_dialect
classmethod
Register a portability validator callback for a dialect name.
registered_dialects
classmethod
Return registered dialect names in deterministic order.
validate
Run dialect-specific portability validation for this Program.
Rung
Subroutine
Context manager for defining a subroutine.
Subroutines are named blocks of rungs that are only executed when called.
Example
with subroutine("my_sub"): with Rung(): out(Light)
SubroutineFunc
A decorated function that represents a subroutine.
Created by using @subroutine("name") as a decorator. When passed to call(), auto-registers with the current Program on first use.
Example
@subroutine("init") def init_sequence(): with Rung(): out(Light)
with Program() as logic: with Rung(Button): call(init_sequence)
ForbiddenControlFlowError
Bases: RuntimeError
Raised when Python control flow is used inside strict DSL scope.
count_down
Count Down instruction (CTD) - Click-style.
Creates a counter that decrements every scan while the rung condition is True.
Use rise() on the condition for edge-triggered counting.
Example
with Rung(rise(Dispense)): count_down(done_bit, acc, preset=25).reset(Reload)
This is a terminal instruction. Requires .reset() chaining.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
done_bit
|
Tag
|
Tag to set when accumulator <= -preset. |
required |
accumulator
|
Tag
|
Tag to decrement while rung condition is True. |
required |
preset
|
Tag | int
|
Target value (Tag or int). |
required |
Returns:
| Type | Description |
|---|---|
CountDownBuilder
|
Builder for chaining .reset(). |
count_up
Count Up instruction (CTU) - Click-style.
Creates a counter that increments every scan while the rung condition is True.
Use rise() on the condition for edge-triggered counting.
Example
with Rung(rise(PartSensor)): count_up(done_bit, acc, preset=100).reset(ResetBtn)
This is a terminal instruction. Requires .reset() chaining.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
done_bit
|
Tag
|
Tag to set when accumulator >= preset. |
required |
accumulator
|
Tag
|
Tag to increment while rung condition is True. |
required |
preset
|
Tag | int
|
Target value (Tag or int). |
required |
Returns:
| Type | Description |
|---|---|
CountUpBuilder
|
Builder for chaining .down() and .reset(). |
off_delay
off_delay(
done_bit: Tag,
accumulator: Tag,
*,
preset: Tag | int,
unit: TimeUnit = TimeUnit.Tms,
) -> OffDelayBuilder
Off-Delay Timer instruction (TOF) - Click-style.
Done bit is True while enabled. After disable, counts until preset, then done bit goes False. Auto-resets when re-enabled.
Example
with Rung(MotorCommand): off_delay(done_bit, acc, preset=10000)
Off-delay timers are composable in-rung (not terminal).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
done_bit
|
Tag
|
Tag that stays True for preset time after rung goes false. |
required |
accumulator
|
Tag
|
Tag to increment while disabled. |
required |
preset
|
Tag | int
|
Delay time in time units (Tag or int). |
required |
unit
|
TimeUnit
|
Time unit for accumulator (default: Tms). |
Tms
|
Returns:
| Type | Description |
|---|---|
OffDelayBuilder
|
Builder for the off_delay instruction. |
on_delay
on_delay(
done_bit: Tag,
accumulator: Tag,
*,
preset: Tag | int,
unit: TimeUnit = TimeUnit.Tms,
) -> OnDelayBuilder
On-Delay Timer instruction (TON/RTON) - Click-style.
Accumulates time while rung is true.
Example
with Rung(MotorRunning): on_delay(done_bit, acc, preset=5000) # TON on_delay(done_bit, acc, preset=5000).reset(ResetBtn) # RTON
Without .reset(), this is TON and remains composable in-rung. With .reset(), this is RTON and becomes terminal in the current flow.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
done_bit
|
Tag
|
Tag to set when accumulator >= preset. |
required |
accumulator
|
Tag
|
Tag to increment while enabled. |
required |
preset
|
Tag | int
|
Target value in time units (Tag or int). |
required |
unit
|
TimeUnit
|
Time unit for accumulator (default: Tms). |
Tms
|
Returns:
| Type | Description |
|---|---|
OnDelayBuilder
|
Builder for optional .reset() chaining. |
shift
Shift register instruction builder.
Data input comes from current rung power. Use .clock(...) then .reset(...) to finalize and add the instruction.
Example
with Rung(DataBit): shift(C.select(2, 7)).clock(ClockBit).reset(ResetBit)
all_of
all_of(
*conditions: Condition
| Tag
| ImmediateRef
| tuple[Condition | Tag | ImmediateRef, ...]
| list[Condition | Tag | ImmediateRef],
) -> AllCondition
AND condition - true when all sub-conditions are true.
This is equivalent to comma-separated rung conditions, but useful when building
grouped condition trees with any_of() or &.
Example
with Rung(all_of(Ready, AutoMode)): out(StartPermissive)
Equivalent operator form:
with Rung((Ready & AutoMode) | RemoteStart): out(StartPermissive)
any_of
OR condition - true when any sub-condition is true.
Use this to combine multiple conditions with OR logic within a rung. Multiple conditions passed directly to Rung() are ANDed together.
Example
with Rung(Step == 1, any_of(Start, CmdStart)): out(Light) # True if Step==1 AND (Start OR CmdStart)
Also works with | operator:
with Rung(Step == 1, Start | CmdStart): out(Light)
Grouped AND inside OR (explicit):
with Rung(any_of(Start, all_of(AutoMode, Ready), RemoteStart)): out(Light)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
conditions
|
Condition | Tag | ImmediateRef
|
Conditions to OR together. |
()
|
Returns:
| Type | Description |
|---|---|
AnyCondition
|
AnyCondition that evaluates True if any sub-condition is True. |
fall
Falling edge contact (FE).
True only on 1->0 transition. Requires PLCRunner to track previous values.
Example
with Rung(fall(Button)): reset(MotorRunning) # Resets when button is released
rise
Rising edge contact (RE).
True only on 0->1 transition. Requires PLCRunner to track previous values.
Example
with Rung(rise(Button)): latch(MotorRunning) # Latches on button press, not while held
branch
Create a parallel branch within a rung.
A branch executes when both the parent rung conditions AND the branch's own conditions are true.
Example
with Rung(Step == 0): out(Light1) with branch(AutoMode): # Only executes if Step==0 AND AutoMode out(Light2) copy(1, Step, oneshot=True)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
conditions
|
ConditionTerm
|
Conditions that must be true (in addition to parent rung) for this branch's instructions to execute. |
()
|
Returns:
| Type | Description |
|---|---|
Branch
|
Branch context manager. |
forloop
Create a repeated instruction block context.
Example
with Rung(Enable): with forloop(10) as loop: copy(Source[loop.idx + 1], Dest[loop.idx + 1])
subroutine
Define a named subroutine.
Subroutines are only executed when called via call(). They are NOT executed during normal program scan.
Example
with Program() as logic: with Rung(Button): call("my_sub")
with subroutine("my_sub"):
with Rung():
out(Light)
program
program(
fn: Callable[[], None] | None = None,
/,
*,
strict: bool = True,
) -> Program | Callable[[Callable[[], None]], Program]
Decorator to create a Program from a function.
Example
@program def my_logic(): with Rung(Button): out(Light)
@program(strict=False) def permissive_logic(): with Rung(Button): out(Light)
runner = PLCRunner(my_logic)
blockcopy
Block copy instruction.
Copies values from source BlockRange to dest BlockRange. Both ranges must have the same length.
Example
with Rung(CopyEnable): blockcopy(DS.select(1, 10), DD.select(1, 10))
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source
|
Any
|
Source BlockRange or IndirectBlockRange from .select(). |
required |
dest
|
Any
|
Dest BlockRange or IndirectBlockRange from .select(). |
required |
oneshot
|
bool
|
If True, execute only once per rung activation. |
False
|
calc
Calc instruction.
Evaluates an expression and stores the result in dest, with truncation to the destination tag's bit width (modular wrapping).
Key differences from copy(): - Truncates result to destination tag's type width - Division by zero produces 0 (not infinity) - Infers decimal/hex behavior from referenced tag types
Example
with Rung(Enable): calc(DS1 * DS2 + DS3, Result) calc(MaskA & MaskB, MaskResult)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
expression
|
Any
|
Expression, Tag, or literal to evaluate. |
required |
dest
|
Tag
|
Destination tag (type determines truncation width). |
required |
oneshot
|
bool
|
If True, execute only once per rung activation. |
False
|
Returns:
| Type | Description |
|---|---|
Tag
|
The dest tag. |
call
Call a subroutine instruction.
Executes the named subroutine when the rung is true. Accepts either a string name or a @subroutine-decorated function.
Example
with Rung(Button): call("init_sequence")
with subroutine("init_sequence"): with Rung(): out(Light)
Or with decorator:
@subroutine("init") def init_sequence(): with Rung(): out(Light)
with Program() as logic: with Rung(Button): call(init_sequence)
copy
copy(
source: Any,
target: Tag | IndirectRef | IndirectExprRef,
oneshot: bool = False,
) -> Tag | IndirectRef | IndirectExprRef
Copy instruction (CPY/MOV).
Copies source value to target.
Example
with Rung(Button): copy(5, StepNumber)
fill
Fill instruction.
Writes a constant value to every element in a BlockRange.
Example
with Rung(ClearEnable): fill(0, DS.select(1, 100))
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
value
|
Any
|
Value to write (literal, Tag, or Expression). |
required |
dest
|
Any
|
Dest BlockRange or IndirectBlockRange from .select(). |
required |
oneshot
|
bool
|
If True, execute only once per rung activation. |
False
|
latch
Latch/Set instruction (SET).
Sets target to True. Unlike OUT, does NOT reset when rung goes false. Use reset() to turn off.
Example
with Rung(StartButton): latch(MotorRunning) latch(C.select(1, 8))
out
out(
target: Tag | BlockRange | ImmediateRef,
oneshot: bool = False,
) -> Tag | BlockRange | ImmediateRef
Output coil instruction (OUT).
Sets target to True when rung is true. Resets to False when rung goes false.
Example
with Rung(Button): out(Light) out(Y.select(1, 4))
pack_bits
Pack BOOL tags from a BlockRange into a register destination.
pack_text
pack_text(
source_range: Any,
dest: Any,
*,
allow_whitespace: bool = False,
oneshot: bool = False,
) -> None
Pack Copy text mode: parse a TXT/CHAR range into a numeric destination.
pack_words
Pack two 16-bit tags from a BlockRange into a 32-bit destination.
reset
Reset/Unlatch instruction (RST).
Sets target to its default value (False for bits, 0 for ints).
Example
with Rung(StopButton): reset(MotorRunning) reset(C.select(1, 8))
return_early
Return from the current subroutine.
Example
with subroutine("my_sub"): with Rung(Abort): return_early()
run_enabled_function
run_enabled_function(
fn: Callable[..., dict[str, Any]],
ins: dict[
str, Tag | IndirectRef | IndirectExprRef | Any
]
| None = None,
outs: dict[str, Tag | IndirectRef | IndirectExprRef]
| None = None,
) -> None
Execute a synchronous function every scan with rung enabled state.
run_function
run_function(
fn: Callable[..., dict[str, Any]],
ins: dict[
str, Tag | IndirectRef | IndirectExprRef | Any
]
| None = None,
outs: dict[str, Tag | IndirectRef | IndirectExprRef]
| None = None,
*,
oneshot: bool = False,
) -> None
Execute a synchronous function when rung power is true.
search
search(
condition: str,
value: Any,
search_range: BlockRange | IndirectBlockRange,
result: Tag,
found: Tag,
continuous: bool = False,
oneshot: bool = False,
) -> Tag
Search instruction.
Scans a selected range and writes the first matching address into result.
Writes found True on hit; on miss writes result=-1 and found=False.
unpack_to_bits
Unpack a register source into BOOL tags in a BlockRange.