Ladder logic has no FOR loop by design. Why, the four workarounds plants use (AOIs, array instructions, one-per-scan, jumps), and our compiler's approach.

Every few weeks someone asks our drafter for a loop. "For each of the eight feeders, trip the breaker if its current exceeds the setting." It is a perfectly reasonable thing to want, and in structured text it is a five-line FOR. In ladder logic there is no such thing, and our compiler currently says so with a line number. This post is about why the language is built that way, what people do about it, and what we are building.

The scan is the loop

A relay ladder never looped because relays do not execute; they are all energised at once. When PLCs replaced relay panels, the ladder metaphor kept a property that made the migration safe: every rung is evaluated once per scan, top to bottom, in a fixed time. The loop is the scan itself. The controller reads inputs, sweeps the rungs, writes outputs, and does it again, typically every few milliseconds.

That determinism is the whole value proposition. A rung that takes the same time on every scan is a rung you can reason about, and a watchdog can catch a controller that misses its cycle. A loop inside a rung breaks that. If the bound depends on data, the scan time depends on data, and a bad value on one input can stall the outputs for everything else. The IEC 61131-3 ladder language therefore has no iteration construct, and the standard's structured text does.

Workaround one: put the loop in a block

The most common answer is to move the loop into a function block written in structured text, then call that block from a rung. Rockwell calls these Add-On Instructions; Siemens has FBs and FCs in SCL; CODESYS has function blocks in any of its languages. The ladder stays flat, the loop lives inside a box with a name.

This is a good pattern with one cost: the technician reading the ladder can no longer see what the box does. Troubleshooting stops at the block boundary and continues in another editor, in another language. Plants that do this well document the block interface carefully and keep the ladder around it dumb.

Workaround two: vendor array instructions

Most platforms provide instructions that operate on a whole array in one rung: file arithmetic (FAL), copy (COP), fill (FLL), search and compare instructions, and their equivalents elsewhere. They are loops in disguise, executed by the firmware in a bounded time, and they are the right tool for "reset all forty alarm bits" or "copy this recipe into the working area".

They are not portable. An FAL has no meaning on a Siemens controller, and even the semantics of an indexed copy differ between vendors. They also do not compose: you cannot put an IF inside one.

Workaround three: one element per scan

The oldest trick is to let the scan do the iterating. Keep an index in an integer, process element [index] this scan, increment, wrap at the end. Eight feeders get checked every eight scans. Nobody has to write a loop, the scan time stays flat, and the ladder is honest about what happens each cycle.

The catch is latency. If the scan is 10 ms and there are 64 elements, an element is looked at every 640 ms. For a protection function that is far too slow; for a housekeeping task it is fine. This pattern is everywhere in older plants, and it is worth recognising when you meet it, because the index register is the first thing that goes wrong.

Workaround four: jumping backwards

Some platforms allow a JMP to a label above the current rung, which turns a section of ladder into a loop. It works, it is compact, and it is the fastest way we know to trip a watchdog. Most style guides ban it outright, and several controllers refuse to compile a backward jump at all. We mention it for completeness and because you will find it in the field.

What we do today

Our compiler accepts a published subset of structured text and turns it into ladder and function block diagrams from the same model. FOR, WHILE and REPEAT parse, but they are rejected with a message that names the line, not silently converted into something that does not mean the same thing. Arrays are rejected for the same reason. We would rather say no clearly than produce a diagram that looks right and is not.

The approach we are building toward

The direction we favour is compile-time unrolling for loops whose bounds are literal constants. FOR i := 1 TO 8 over eight declared feeder signals becomes eight networks, each with the index substituted, each a plain rung a technician can read and force in the simulator. Nothing about scan time changes because nothing iterates at run time; the loop existed only in the source.

Two rules make this safe. The bound has to be a constant, so the unrolled size is known before the program runs, and there is a cap on the total number of networks an unroll may produce, so a typo in a bound cannot generate ten thousand rungs. Data-dependent bounds stay rejected; that is exactly the case where the ladder metaphor breaks.

This is design work, not a shipped feature. When it lands you will see it in the supported-subset list, and the drafter will start using it. Until then, if you ask the AI for a loop over eight feeders it will write eight assignments, which is, after all, what the unroller would have produced.

Try it: describe your logic in plain English at PLC-Ladder, and see how it reads as rungs before it runs.