Skip to content

Verified: Language reference

Each claim the Language reference page makes, with the tests that prove it in the last green CI run: 152 of 160 verified. The claims live in docs/claims/functions.yaml.

Last green CI run 16 Oct 2026, 14:48 UTC on c7618d3 · all runtime claims

FN-001 verified

Statements and rungs evaluate top to bottom within a scan: a value written by one is visible to the ones below it in the same scan and to everything on the next scan (the seal-in idiom works).

where the docs say so · ld-scan-order runs the same four assignments in ST, LD and FBD: a coil written by one rung is read as a contact by the next rung in the same scan, and a coil written below its reader is seen by it one scan later. The seal-in is run end to end by TestLadderProgram and TestSealInLatch.

FN-002 verified

A diagnostic raised anywhere in the .ld/.fbd to ST chain lands on the user's own source line (the rung, the FBD statement), because every hop preserves a line map.

where the docs say so · The line maps are tested per hop, and the editor's diagnostics (the language server) land on the source line for an error raised at each stage: the ladder hop (a misplaced coil, on its RUNG), the FBD hop (a netlist parse error) and ST (an undeclared name, through .fbd and through .ld).

FN-003 partly verified

Logic that does not type-check is a compile diagnostic on the offending line or rung, never a silent coercion (for example a REAL assigned to a BOOL).

where the docs say so · Sampled cases; there is no exhaustive type-rule matrix.

  • pass TestLowerAssignRealToBool in lang/st
  • pass TestLowerErrorPinsOffendingName in lang/st (13 of 13 passed)
  • pass TestLowerIfNonBool in lang/st

FN-004 verified

FBD execution order is network order: network 1 runs completely, then network 2. A read of a later network's FB output sees the value from the previous scan.

where the docs say so

  • pass TestNetworksRunInOrder in lang/fbd
  • pass TestNetworksParseModelAndOrder in lang/fbd

FN-005 verified

Statements before the first NETWORK line form an untitled first network, and a body with no NETWORK line is a single network, exactly the program it always was.

where the docs say so · The implicit untitled first network is asserted (TestNetworksParseModelAndOrder). A body with no NETWORK line reports no networks, transpiles to the same ST as the same statements under one NETWORK line, and runs as one network (the FB call sorted ahead of its reader, unlike the two-network control).

  • pass TestNetworksParseModelAndOrder in lang/fbd
  • pass TestBodyWithoutNetworkLinesIsOneNetwork in lang/fbd

FN-006 verified

NETWORK stands alone on its line; a tag that happens to be called Network still works (Network := x is a coil), and a statement on the NETWORK line is refused.

where the docs say so

  • pass TestNetworkParseEdges in lang/fbd

FN-007 verified

A read that crosses a network boundary (an FB output or a wire from another network) draws as a variable box in the reading network, never as a line across bands.

where the docs say so · A named wire and an FB instance output read from another network are each a variable box in the reading network, and no edge joins nodes of two networks; the same wire read inside its own network stays a line.

  • pass TestCrossNetworkReadsDrawAsBoxesInTheReadingNetwork in lang/fbd
  • pass TestNetworksParseModelAndOrder in lang/fbd

FN-008 verified

Series ladder elements AND together, parallel branch legs OR (legs are series and nest freely), a plain contact passes power when its tag is TRUE and an NC contact (/Tag) when it is FALSE.

where the docs say so

  • pass TestRungShapes in lang/ld
  • pass TestTranspileRungs in lang/ld
  • pass TestConformance/ld-coils/nested branch, bare leg passes at once in lang/conformance
  • pass TestConformance/ld-coils/nested branch, FB leg is Q false at PT-1ms, true at PT, drop resets in lang/conformance

FN-009 verified

A rising-edge contact (+Tag) passes power for exactly one scan on the tag's 0 to 1 transition and a falling-edge contact (-Tag) for one scan on 1 to 0; holding the input steady fires neither again.

where the docs say so

  • pass TestEdgeContactOneShot in lang/ld
  • pass TestConformance/ld-coils/+ contact passes one scan on the rise, - contact one scan on the fall in lang/conformance

FN-010 verified

A plain coil sets its tag to the rung condition every scan; a set coil latches (Tag := Tag OR condition) and a reset coil unlatches (Tag := Tag AND NOT condition).

where the docs say so

  • pass TestConformance/ld-coils/S coil latches, R coil unlatches, R wins a same-scan tie in lang/conformance
  • pass TestConformance/ld-coils/+ contact passes one scan on the rise, - contact one scan on the fall in lang/conformance

FN-011 verified

A P coil is TRUE for exactly one scan when the rung condition rises and an N coil for one scan when it falls.

where the docs say so

  • pass TestConformance/ld-coils/P coil is TRUE for exactly one scan on the rising edge in lang/conformance
  • pass TestConformance/ld-coils/N coil is TRUE for exactly one scan on the falling edge in lang/conformance

FN-012 verified

Edge instances get a stable derived name (from rung name, reference and position among repeats), so the R_TRIG's retained state survives an unrelated edit; two edge contacts on the same tag in one rung are independent instances.

where the docs say so · Naming and two instances for two contacts on one tag are asserted on the transpiled output; TestEdgeStateSurvivesUnrelatedEdit inserts a rung ahead and edits another, warm-swaps the frame (ir.MigrateFrame) with the tag held TRUE, and the contact does not fire again.

  • pass TestEdgeContactSyntax in lang/ld
  • pass TestEdgeTrigNamesMatchTheCompiler in lang/ld
  • pass TestEdgeStateSurvivesUnrelatedEdit in lang/ld

FN-013 verified

A function contact FN(args) passes power when the call yields TRUE, and /FN(args) negates it; the negation applies to any BOOL-yielding contact term (a plain ref, an accessor chain such as /t1.Q, a call such as /GT(a, b)).

where the docs say so

  • pass TestNegatedFunctionContact in lang/ld
  • pass TestFunctionContactExpressionArgs in lang/ld
  • pass TestTranspileRungs in lang/ld
  • pass TestConformance/ld-coils/negated function contact passes power when the call returns FALSE in lang/conformance

FN-014 verified

A rung with only a coil is driven by the rail (TRUE), and multiple coils on one rung share the same condition.

where the docs say so

FN-015 verified

A rung's only output may be a function block instance, with no trailing coil: its own output (inst.Q, inst.DN) is read elsewhere.

where the docs say so

  • pass TestFBOnlyRung in lang/ld
  • pass TestConformance/ld-coils/FB-only rung (no coil) still runs, its outputs read elsewhere in lang/conformance

FN-016 verified

A rung's trailing output zone (coils or a rung-final function block) must be contiguous at the rung's right end; a coil ahead of later elements is the compile error "coils must sit at the rung's right end".

where the docs say so

  • pass TestRungErrors in lang/ld
  • pass TestCoilBeforeFunctionBlockRefused in lang/ld

FN-017 verified

The BOOL rule: anything that gates ladder power must yield BOOL. Comparisons (GT, GE, LT, LE, EQ, NE) are valid contacts; a numeric function such as ADD(TempC, 0.0) used as a contact is a compile error, not a pass-through.

where the docs say so · ADD(TempC, 0.0) as a contact fails with the page's two messages ("operator AND on BOOL and REAL" in series, "cannot assign REAL to BOOL" alone), and all six comparisons are run as contacts.

  • pass TestNumericFunctionContactIsACompileError in lang/ld
  • pass TestFunctionContactExpressionArgs in lang/ld

FN-018 verified

An assignment element { Tag := expr } is made only when the rung has power at that point and passes power through unchanged; several may be chained with ";".

where the docs say so · ld-assign runs two chained assignments behind a contact: made every powered scan, held while unpowered, and power passes through to the coil after a second contact (A AND B). TestAssignmentElement asserts the SEL lowering.

FN-019 verified

An assignment element's value is evaluated every scan like any block (compiled as a SEL), so an array index in it must stay valid while the rung is false.

where the docs say so · With the rung FALSE, { Out := Arr[I] } faults the scan when I is out of bounds and leaves Out alone when it is in range; TestAssignmentElement asserts the SEL lowering.

  • pass TestLadderAssignmentIndexEvaluatedWhileRungFalse in lang/conformance (4 of 4 passed)
  • pass TestAssignmentElement in lang/ld

FN-020 verified

The standard's output binding (inst:TON(PT := T#5S, ET => Elapsed)) stores an FB output into a variable at the call site, in ladder, FBD and ST.

where the docs say so · Ladder (CV => ld_Cnt in ld-coils; ET => Elapsed in TestLadderOutputBinding), ST (Y => result) and FBD (ET => Elapsed in TestOutputBinding) are run and the bound variable asserted.

  • pass TestConformance/ld-coils/FB-only rung (no coil) still runs, its outputs read elsewhere in lang/conformance
  • pass TestUserFB_OutputBinding in lang/st
  • pass TestOutputBinding in lang/fbd
  • pass TestLadderOutputBinding in lang/ld

FN-021 verified

An instance output is addressable as inst.Pin anywhere in the program that declares the instance, a non-BOOL pin as a contact's argument included. Another task's program cannot name the instance (t1.Q, or through its program's or task's name): that is an undeclared identifier whose error names the owning program and says to go through a tag. Published to a tag in the owner, the output reaches the other task as of the owner's last committed scan — at once for a reader that scans after the owner, on its next scan for one that scans before.

where the docs say so · #327: the page used to say an instance output is readable from another task's program; it never was (an instance lives in its program's private frame, and tasks share only the tag store). The page now states the rule and the tag recipe. The diagnostic is asserted from the runtime (project load), naut check and the language server, for ST and ladder readers; the recipe's timing in naut test's deterministic task order (main first, then tasks: order).

  • pass TestConformance/ld-coils/FB-only rung (no coil) still runs, its outputs read elsewhere in lang/conformance
  • pass TestCrossTaskPublishedInstanceOutput in lang/conformance
  • pass TestCrossTaskInstanceIsDiagnosed in lang/conformance (6 of 6 passed)
  • pass TestUndeclaredIdentifierWithoutPeerKeepsItsMessage in lang/conformance
  • pass TestCheckGoldenCrossTaskInstance in cmd/naut
  • pass TestCrossTaskInstanceDiagnostic in internal/lsp

FN-022 verified

The untimed action qualifiers behave as specified: N active while the step is active, S / R set / reset once on the activation scan, P1 / P one scan on activation and P0 one scan on deactivation.

where the docs say so

  • pass TestConformance/sfc-qualifiers in lang/conformance (8 of 8 passed)
  • pass TestSetActsOnceThenActionMayClear in lang/sfc
  • pass TestResetActsOnceThenActionMaySet in lang/sfc

FN-023 verified

The timed qualifiers L, D, SD, DS and SL behave as specified: L active from activation for the duration, D once active the duration, SD / DS / SL stored past leaving the step, SL for the duration even after leaving.

where the docs say so

  • pass TestConformance/sfc-timed-qualifiers in lang/conformance (13 of 13 passed)

FN-024 verified

An R acts on its step's activation scan and clears the S, SD, DS and SL latches on its target; a timed SL that has run out stays latched until an R.

where the docs say so · R acting once on its activation scan, R clearing SD, DS and SL, a timed-out SL staying off until an R, and R clearing an S latch (an S-stored BOOL and an S-stored ACTION body that keeps running in the next step until the R) are asserted.

  • pass TestConformance/sfc-timed-qualifiers/R resets the stored qualifiers in lang/conformance
  • pass TestConformance/sfc-timed-qualifiers/R before PT cuts SL short and stops SD from ever coming on in lang/conformance
  • pass TestConformance/sfc-timed-qualifiers/re-entry restarts L and D; a timed-out SL stays off until an R in lang/conformance
  • pass TestConformance/sfc-timed-qualifiers/after an R, SL arms again on the next activation in lang/conformance
  • pass TestConformance/sfc-timed-qualifiers/R acts once on its step's activation, not on every scan the step is active in lang/conformance
  • pass TestConformance/sfc-timed-qualifiers/R clears an S latch - the stored body runs on after the step is left until the R in lang/conformance

FN-025 verified

Both association spellings compile and mean the same: "Q Target;" / "Q Target(T#3S);" and the standard's textual "Target(Q);" / "Target(Q, T#3S);". The duration of a timed qualifier is a TIME literal or a TIME variable.

where the docs say so · sfc-assoc-forms runs twin targets side by side: N in both spellings, L with a literal and with a TIME variable in both spellings, D in each, asserted equal at every edge; a second test changes the variable (40ms) and the variable-duration twins move while the literal ones do not.

  • pass TestIECAssociationForm in lang/sfc
  • pass TestTimedQualifiersParseAndCheckClean in lang/sfc
  • pass TestConformance/sfc-assoc-forms in lang/conformance (3 of 3 passed)

FN-026 verified

Every timed association has its own timer, retained across an online edit.

where the docs say so · One TON per association, with slot names stable across an edit, is asserted on the transpiled chart. TestWarmMigration/timers-latches-pulses edits a running chart (adds a step and a transition), migrates the frame, and asserts the D and SL associations' timers carry 6 s of elapsed time and fire at 10 s / 20 s after activation, not after the swap.

  • pass TestTimedLoweringStableSlots in lang/sfc
  • pass TestWarmMigration/timers-latches-pulses in lang/sfc

FN-027 verified

A step's MAXTIME makes Step.ERR TRUE once the step has been active longer than the limit (Step.ERR is legal in transition conditions and action bodies); it stays TRUE after the step is left and clears on the step's next activation.

where the docs say so

FN-028 verified

"ERROR :=" on a step mirrors Step.ERR into a BOOL variable (a manifest tag an alarm can watch).

where the docs say so

FN-029 verified

Identifiers are case-insensitive in every language: variables, tags, POUs, TYPEs, FB instances and pins, struct members, SFC steps/actions and their .X/.T flags, and FBD wires.

where the docs say so

  • pass TestConformance/st-case-insensitive in lang/conformance (3 of 3 passed)
  • pass TestLowerCaseInsensitiveIdentifiers in lang/st
  • pass TestCaseInsensitiveCallsKeepSpelling in lang/fbd
  • pass TestCaseInsensitiveWiresAndInstances in lang/fbd
  • pass TestTagNamesAreCaseInsensitive in runtime

FN-030 partly verified

The declaration's spelling is the name shown: the tag store, live values and the HTTP API answer with the declared spelling however a program spells it, and no second tag appears.

where the docs say so · Store and HTTP write are covered; Sparkplug metric names, diagnostics, hover and the L5X tag list spelling are not asserted.

  • pass TestTagNamesAreCaseInsensitive in runtime
  • pass TestWriteTagAnyCasing in server

FN-031 verified

Two declarations that differ only in case in the same scope (variables, POUs, TYPEs, manifest tags, block pins) are a duplicate-declaration error naming both; the same name in different scopes is fine.

where the docs say so · Variables, FUNCTIONs, FUNCTION_BLOCKs, TYPEs and FB pins are tested, each error naming both spellings; two manifest tags Level / LEVEL are refused with an error naming both and their sources.

  • pass TestLowerCaseOnlyDuplicates in lang/st (7 of 7 passed)
  • pass TestLowerCaseDifferentScopesCoexist in lang/st
  • pass TestTagFilesRejectDuplicates/differing only in case in internal/project

FN-032 verified

The HTTP API accepts any casing for a tag or member path on writes, and forces and acceptance-test given:/expect: keys accept any casing too.

where the docs say so · HTTP writes of tags and member paths by any casing; a force sent over HTTP as "level" lands on Level (reported under the declared spelling, no new tag) and an unforce as "LEVEL" removes it; given:/expect: keys in other casings (the st-case-insensitive corpus runs through the acceptance runner).

  • pass TestWriteTagAnyCasing in server
  • pass TestForceAPIAnyCasing in server
  • pass TestTagNamesAreCaseInsensitive in runtime
  • pass TestConformance/st-case-insensitive in lang/conformance (3 of 3 passed)

FN-033 verified

Every tag the manifest declares is in scope in every PROGRAM (ST, ladder, FBD, SFC) without a declaration.

where the docs say so · tag-implicit-scope has an ST, a ladder, an FBD and an SFC program, none with a VAR_EXTERNAL, each reading the shared tags (in a rung, an FBD wire, an SFC transition and action body) and writing its own.

FN-034 partly verified

A tag's type is its type: (any elementary type, a project TYPE or an ARRAY of either), else the type its init: implies; a tag with neither cannot be typed and naming it is an error that says to give it a type:.

where the docs say so · Asserted: all 17 elementary type: names load, and the stored kind is checked for BOOL, INT, LWORD, REAL, LREAL, TIME and STRING; a project TYPE, an ARRAY OF REAL and an ARRAY of a project TYPE; the type init: implies (TRUE BOOL, 5 REAL, T#5s TIME, other text STRING), each used in a way only that type compiles; and the untyped-tag error saying "give Valve a type:". Not asserted: that an integer tag keeps its declared width. Every integer width is stored as one 64-bit kind, so a SINT, DINT or UDINT tag typed as INT would pass these tests.

FN-035 partly verified

VAR_EXTERNAL stays legal and must agree with a stated type: (an error at the declaration otherwise); two programs binding one untyped tag with different types is an error naming both.

where the docs say so · Asserted: a VAR_EXTERNAL of BOOL against type: REAL is an error at the declaration's line; two programs binding an untyped tag as DINT and REAL is an error naming both tasks (main, b) and both types. Disagreement: agreement is checked by shape (ir.SameShape), not by type, so integer widths never disagree. `VAR_EXTERNAL D : INT` against `type: DINT` loads cleanly (checked 2026-10-13).

FN-036 verified

A program's own VAR with a tag's name shadows the tag (IEC scoping), and naut check and the editor warn.

where the docs say so · Shadowing at run time; the naut check warning (golden text on the declaration's line) and the language server's warning diagnostic on the declaration.

FN-037 verified

FUNCTION_BLOCK and FUNCTION bodies do not see tags implicitly; a block reaches a tag only through its own VAR_EXTERNAL.

where the docs say so · A FUNCTION_BLOCK body and a FUNCTION body each give an undeclared-name error without a VAR_EXTERNAL. With one, the FUNCTION reads the tag (Out = 10); the FUNCTION_BLOCK case is only shown to load.

FN-038 verified

A GVL file (a library .st holding a file-level VAR_GLOBAL block) declares globals for every program; one the manifest does not declare is a state tag starting at zero.

where the docs say so · Composition into every program; a GVL global the manifest does not declare is a state-role tag holding the zero of its declared type (BOOL, DINT, TIME) before any scan, while one the manifest declares keeps the manifest's role and init.

FN-039 verified

Integer kinds share one 64-bit runtime representation, so arithmetic on INT, DINT, UINT or WORD never wraps at the declared width.

where the docs say so

FN-040 verified

A diagnostic names the declared integer type (DINT, WORD), and partial access knows a DINT has 32 bits.

where the docs say so · Beyond partial access ("bit 32 is out of range for DINT (bits 0..31)"), the same test asserts ordinary diagnostics naming the declared type: "cannot assign STRING to DINT" and "member access on non-struct type WORD".

  • pass TestPartialAccessErrorsNameDeclaredType in lang/st

FN-041 verified

TIME counts milliseconds; literals T#500MS, T#5S, T#2M30S parse, and TIME values compare with the ordinary comparisons.

where the docs say so · Milliseconds (T#2s + T#500ms = 2500) and a TIME comparison are run; TestParseTimeLiterals checks compound literals' values (T#1h30m, T#2d4h) through ParseTimeMs, which lowering uses and which ignores unit case.

  • pass TestLowerTimeArithmetic in lang/st
  • pass TestConformance/fn-conversions/TIME is milliseconds in both directions in lang/conformance
  • pass TestConformance/sfc-step-t/a Step.T condition fires one scan after T reaches PT in lang/conformance
  • pass TestParseTimeLiterals in lang/st

FN-042 verified

Mixed numeric arguments promote to REAL; comparing or combining a STRING with a number is a compile error, not a coercion.

where the docs say so

  • pass TestLowerIntToRealPromotion in lang/st
  • pass TestMixedNumericArgsPromoteStringNumberRefused in lang/st (8 of 8 passed)

FN-043 verified

A user TYPE is first-class: it can be a variable, a tag, a struct field, an array element, a FUNCTION argument or return and a FUNCTION_BLOCK pin, and structs nest.

where the docs say so · TestUserTypeAsFunctionArgLocalReturnAndBlockVar passes a nested UDT to a FUNCTION, uses one as its local and returns one (the argument is a copy).

  • pass TestLowerUDTFieldAccess in lang/st
  • pass TestLowerNestedAggregateAssign in lang/st
  • pass TestFBStructPins in lang/st
  • pass TestEnumInStructsPinsArrays in lang/st
  • pass TestUserTypeAsFunctionArgLocalReturnAndBlockVar in lang/st

FN-044 verified

Assigning a struct or an array copies it (b := a; b.F := 1 leaves a.F alone), and so does passing a struct to a VAR_INPUT pin; only VAR_IN_OUT writes back to the caller.

where the docs say so · TestArrayAssignmentIsACopy: b := a; b[1] := 99 leaves a[1], also for an array inside a struct. A struct passed to a VAR_INPUT that the block changes leaves the caller's alone; a VAR_IN_OUT writes back to the caller's variable, struct field, array element and tag (TestVarInOutAcceptsEveryAssignableForm).

  • pass TestStructAssignmentIsACopy in lang/st
  • pass TestFBStructInputIsCopied in lang/st
  • pass TestArrayAssignmentIsACopy in lang/st
  • pass TestVarInOutAcceptsEveryAssignableForm in lang/st

FN-045 verified

A field or element of a tag assigns directly (P101.Running := TRUE, Levels[2] := 41.0); the whole aggregate round-trips through the tag store.

where the docs say so · Struct-field assignment on a tag; Levels[2] := 41.0 on an ARRAY tag lands in the stored array with the other elements intact, and a second task reads the whole array back from the store.

  • pass TestExternalStructFieldAssignment in lang/st
  • pass TestVarInOutExternalStructField in lang/st
  • pass TestArrayTagElementAssignsDirectly in internal/project

FN-046 verified

An enumerated TYPE member without ":= n" takes the previous value plus one (the first, 0); names and values are unique within the type; the type's initial value is the one after the list, else the first member; a Codesys base type after the list is accepted.

where the docs say so · TestEnumNumberingOrderingInit: an implicit member after an explicit one takes the previous value plus one (Mid = 6, Top = 2), and the initial value after the list (Mid) is not the first member.

FN-047 verified

A member is named qualified (WashState#Fill), or bare where only one enumeration in scope has it or the context says which; otherwise the compiler asks for the qualified form.

where the docs say so

  • pass TestEnumAmbiguousMemberResolvedByContext in lang/st
  • pass TestEnumTypeErrors in lang/st
  • pass TestConformance/st-enum/CASE labels and comparisons on members, qualified and not in lang/conformance

FN-048 verified

Values of one enumeration compare (=, <>, and < / > by member value) and are CASE labels; assignment is between the same enumeration only, and an enumeration does not mix with an integer and has no arithmetic.

where the docs say so · TestEnumNumberingOrderingInit compares members whose values differ from their positions with < and >, so ordering by member value is told apart from ordering by position.

FN-049 verified

Enumeration conversion is explicit: TO_INT(s) (or any TO_<type>) gives the member's integer and TO_WashState(n) gives the member with integer n; a number no member has keeps that number.

where the docs say so

  • pass TestEnumConversions in lang/st

FN-050 verified

A manifest tag may be typed with an enumeration, seeded by member name or number.

where the docs say so · Seeding by name (init: Run) and by number (init: 10, init: 11 give Run and Fault, compared against the members in the program).

FN-051 partly verified

Live values show the member name: /api/state, the live stream, the editors' inline values and acceptance expect: all show and take the member name, while a value underneath is its integer (what Sparkplug, Modbus and the retain store carry).

where the docs say so · Acceptance given/expect names are covered; /api/state, the SSE stream and the integer on Sparkplug/Modbus/retain are not asserted.

  • pass TestGivenEnumMemberByPath in acceptance
  • pass TestGivenEnumNonMemberNamesMembers in acceptance
  • pass TestEnumFromTagStore in lang/st

FN-052 verified

VAR CONSTANT reads fold at compile time; assigning one (or a field, element or bit of one, or using it as a FOR variable) is a compile error.

where the docs say so · TestConstantFoldsAndPartsCannotBeWritten asserts the read lowers to a literal (folded) and that writing a field, an element, a bit (.3, .%X3), a partial (.%B0) or the whole constant, or using it as a FOR variable, is the "is a constant" error.

  • pass TestConstantCannotBeWritten in lang/st
  • pass TestConstantsAndMembersAreNotWritable in lang/conformance
  • pass TestConstantFoldsAndPartsCannotBeWritten in lang/st (8 of 8 passed)

FN-053 verified

A constant's value, an initial value and an array bound may be a constant expression (literals, other constants and operators).

where the docs say so

  • pass TestConstantInBoundsAndInitialValues in lang/st
  • pass TestConformance/st-global-constants/an INT constant from a constant expression sizes an array and bounds a loop in lang/conformance

FN-054 verified

VAR_GLOBAL CONSTANT in a library file declares project constants, visible to every POU in every language (ST, ladder, FBD, SFC) and folded the same way.

where the docs say so

  • pass TestConformance/st-global-constants in lang/conformance (4 of 4 passed)
  • pass TestGlobalConstantsVisibleEverywhere in lang/st
  • pass TestGlobalConstantsLibraryAlone in lang/st

FN-055 verified

A project constant is not a tag (no manifest entry, live value or API path), a POU's own declaration of the same name shadows it, and its type is elementary or an enumeration.

where the docs say so · Shadowing and the type rule are tested; constants are absent from the compiled program's globals, and in a running project from the tag store and from GET /api/state's live values (a GVL global beside it is listed). POST /api/tags with the constant's name is refused with a 400 naming it (an undeclared name never creates a tag, #329) and the program still reads the constant.

  • pass TestLocalShadowsGlobalConstant in lang/st
  • pass TestGlobalConstantRules in lang/st
  • pass TestGlobalConstantsVisibleEverywhere in lang/st
  • pass TestProjectConstantIsNotATag in internal/project

FN-056 verified

VAR_TEMP starts every scan of a PROGRAM, and every call of a FUNCTION_BLOCK or FUNCTION, at its declared initial value (or its type's zero), never at what the previous scan or call left.

where the docs say so

  • pass TestConformance/st-var-temp in lang/conformance (3 of 3 passed)
  • pass TestVarTempResetsEveryScan in lang/st
  • pass TestVarTempResetsEveryFBCall in lang/st
  • pass TestVarTempInFunction in lang/st

FN-057 verified

A temp cannot be RETAIN and cannot be a function-block instance, and an online edit never carries a temp over.

where the docs say so

  • pass TestVarTempRejections in lang/st
  • pass TestVarTempNotMigrated in lang/st

FN-058 verified

REGION groups statements, nests, appears anywhere a statement can, and changes nothing about how they run: it opens no scope and declares nothing, and REGION is not a reserved word (a variable may be called Region).

where the docs say so

  • pass TestConformance/st-region in lang/conformance (3 of 3 passed)
  • pass TestRegionIsANoOpGrouping in lang/st
  • pass TestRegionOpensNoScope in lang/st
  • pass TestRegionInFBAndCase in lang/st

FN-059 verified

A REGION must be closed inside the statement list it opened in (not across an END_IF).

where the docs say so

FN-060 verified

A CASE label is a constant: a literal, a named constant, an enumeration member or a range of them; each label ends the clause before it, named or not.

where the docs say so

  • pass TestCaseNamedConstantLabels in lang/st
  • pass TestCaseConstantLabelForms in lang/st
  • pass TestConformance/st-case-constants in lang/conformance (4 of 4 passed)
  • pass TestGlobalConstantsVisibleEverywhere in lang/st
  • pass TestConformance/st-enum/CASE labels and comparisons on members, qualified and not in lang/conformance

FN-061 verified

A variable is not a CASE label (compile error).

where the docs say so

  • pass TestCaseLabelMustBeConstant in lang/st

FN-062 verified

Two CASE labels with the same value (two constants, a constant and a literal, a value inside a range) are a compile error naming both.

where the docs say so

FN-063 verified

The SCL "#name" local prefix is rejected wherever it appears with "the # prefix is Siemens SCL syntax; write the name without it", including on an assignment target.

where the docs say so

  • pass TestHashPrefixRejectedEverywhere in lang/st
  • pass TestHashPrefixDiagnosed in lang/conformance (3 of 3 passed)

FN-064 verified

AND, OR and XOR are logical on BOOL and bitwise on integers, over all 64 bits (-8 AND 6 = 0).

where the docs say so · Bitwise AND/OR/XOR on integers (ST and FBD, bits above 32, -8 AND 6 = 0) in st-bitwise; the logical BOOL truth tables of AND, OR and XOR, two and three inputs, as FBD blocks and ST operators, in fn-ops.

  • pass TestConformance/st-bitwise in lang/conformance (6 of 6 passed)
  • pass TestConformance/st-xor in lang/conformance (4 of 4 passed)
  • pass TestConformance/fn-ops/AND, OR and XOR are logical on BOOL (the whole truth table), NOT negates a BOOL in lang/conformance
  • pass TestConformance/fn-ops/AND, OR and XOR take a third input - the third one counts in lang/conformance

FN-065 verified

NOT is negation on BOOL and bitwise complement on an integer (NOT 0 = -1, NOT 12 = -13).

where the docs say so · The integer complement (NOT 0 = -1, NOT 12 = -13) in st-bitwise; NOT on a BOOL, as the FBD block and the ST operator, in fn-ops.

  • pass TestConformance/st-bitwise in lang/conformance (6 of 6 passed)
  • pass TestLowerNotOnInt in lang/st
  • pass TestConformance/fn-ops/AND, OR and XOR are logical on BOOL (the whole truth table), NOT negates a BOOL in lang/conformance

FN-066 verified

ADD and MUL are n-ary, SUB is binary, and the result has the common type of the arguments: for integers, the narrowest type both widen to (DINT + DINT is DINT, a literal takes the other argument's type, the wider of two signed, unsigned or bit-string types, signed with unsigned the narrowest signed type holding both, a bit string with a number as its unsigned width); an integer with a REAL is a REAL. The type does not cut the value: TO_INT(d1 + d2) with both at 40000 is 14464.

where the docs say so · Three-input ADD and MUL (the third input counts), SUB as the binary difference (a three-input SUB is refused), INT with REAL is a REAL (1 + 0.5 = 1.5; it may not drive an INT coil). The integer widths (#326): TestArithmeticTakesTheCommonWidth reads the type of a op b out of the checker's diagnostic (assigned to a STRING) for 23 width pairs and every operator (+ - * / MOD), TestArithmeticLiteralTakesTheOperandWidth the literal and chained cases, TestArithmeticBlocksTakeTheCommonWidth the FBD ADD/SUB/MUL/DIV/MOD blocks, and TestCommonIntEveryPair all 144 pairs (order-independent, never narrower than an operand, Equal to INT so assignability is unchanged, converted as LINT). The fn-ops case pins the values: 40000 + 40000 = 80000 as a DINT, TO_INT of it 14464, TO_LINT of 2000000000 + 2000000000 all 4000000000 (ST and FBD).

  • pass TestLowerRealArithmetic in lang/st
  • pass TestConformance/fn-ops/ADD and MUL are n-ary - the third input counts; SUB is the binary difference in lang/conformance
  • pass TestConformance/fn-ops/an INT combined with a REAL gives a REAL - the common type, nothing truncated in lang/conformance
  • pass TestConformance/fn-ops/DINT + DINT keeps its value past INT's range; a conversion of it wraps only to the target in lang/conformance
  • pass TestArithmeticTakesTheCommonWidth in lang/conformance (24 of 24 passed)
  • pass TestArithmeticLiteralTakesTheOperandWidth in lang/conformance (7 of 7 passed)
  • pass TestArithmeticBlocksTakeTheCommonWidth in lang/conformance (9 of 9 passed)
  • pass TestCommonIntEveryPair in lang/ir
  • pass TestB1OperatorArityAndTypes in lang/fbd

FN-067 verified

DIV gives the quotient and MOD the remainder of two integers.

where the docs say so · The DIV and MOD blocks (FBD) and ST's / and MOD on two integers, both signs (7 DIV 2 = 3, -7 DIV 2 = -3, -7 MOD 2 = -1, 7 MOD -2 = 1).

  • pass TestConformance/st-div-zero/ordinary division truncates toward zero, MOD keeps the dividend's sign in lang/conformance
  • pass TestConformance/fn-ops/DIV of two integers is the quotient, MOD the remainder in lang/conformance

FN-068 verified

DIV by zero yields 0 for integer and REAL (never Inf or NaN), MOD by zero yields 0, and the scan keeps running.

where the docs say so

  • pass TestConformance/st-div-zero in lang/conformance (5 of 5 passed)
  • pass TestDivideByZeroCountsAndKeepsScanning in runtime

FN-069 verified

Every divide-by-zero bumps the controller-wide divZero counter in the scan stats (/api/state scan.divZero), counted since start.

where the docs say so

  • pass TestDivideByZeroCountsAndKeepsScanning in runtime
  • pass TestStateCarriesDivZeroCount in server

FN-070 verified

MOVE passes its single input through as an assignment (FBD wiring aid).

where the docs say so · The FBD MOVE block passes a BOOL, an INT (beyond 32 bits), a REAL and a STRING through unchanged, matching the ST assignment; a two-input MOVE is refused and MOVE of a REAL may not drive an INT (it keeps its type).

  • pass TestConformance/fn-ops/MOVE passes its single input through unchanged, whatever its type in lang/conformance
  • pass TestB1OperatorArityAndTypes in lang/fbd

FN-071 verified

GT, GE, LT, LE, EQ, NE take two comparable arguments (numeric or TIME) and return BOOL.

where the docs say so · All six as FBD blocks and ST operators on INT, REAL and TIME, each below, equal and above, driving BOOL tags; a three- or one-input comparison is refused and a comparison may not drive an INT (its result is BOOL). TestFunctionContactExpressionArgs and the sfc-step-t condition were dropped: one compiles contacts, the other is one TIME comparison.

  • pass TestConformance/fn-ops/GT GE LT LE EQ NE on INT return BOOL - below, equal, above in lang/conformance
  • pass TestConformance/fn-ops/GT GE LT LE EQ NE on REAL compare the fractions too in lang/conformance
  • pass TestConformance/fn-ops/GT GE LT LE EQ NE on TIME (milliseconds) in lang/conformance
  • pass TestB1OperatorArityAndTypes in lang/fbd

FN-072 verified

A standard-function call is positional or formal (every input by its standard name: LIMIT(MN := 0.0, IN := x, MX := 100.0)); one call is one or the other, never a mix.

where the docs say so · Formal calls run under every naming scheme the page lists, each matching the positional call: LIMIT (MN/IN/MX) and MAX (IN1/IN2) in the ST EN/ENO tests; SEL (G/IN0/IN1), MUX (K/IN0..IN2), MIN (IN1..IN3) and ABS (IN) in fn-select-numeric, ST and FBD. A mixed call is refused in ST ("formal or positional") and FBD ("by name or none").

  • pass TestENOnFunctionAssign in lang/st
  • pass TestENOOnlyAndPositional in lang/st
  • pass TestENErrors in lang/st
  • pass TestENOErrors in lang/fbd
  • pass TestConformance/fn-select-numeric/SEL gives IN0 when G is FALSE and IN1 when G is TRUE, positional or formal in lang/conformance
  • pass TestConformance/fn-select-numeric/a formal MUX picks by K over IN0..IN2 in lang/conformance
  • pass TestConformance/fn-select-numeric/MIN and MAX of three or four inputs find the extreme wherever it sits in lang/conformance
  • pass TestConformance/fn-select-numeric/ABS is the absolute value of an INT, a DINT and a REAL, each in its own type in lang/conformance

FN-073 verified

SEL(G, IN0, IN1) returns IN0 when G is FALSE and IN1 when G is TRUE.

where the docs say so · G = FALSE gives IN0 and G = TRUE gives IN1, positional and formal, in ST and FBD.

  • pass TestBuiltinSelMux in lang/st
  • pass TestConformance/fn-select-numeric/SEL gives IN0 when G is FALSE and IN1 when G is TRUE, positional or formal in lang/conformance

FN-074 verified

MUX(K, IN0, ..., INn) picks the K-th input, 0-based.

where the docs say so

  • pass TestConformance/st-mux-fault/K picks the K-th input, 0-based in lang/conformance
  • pass TestBuiltinSelMux in lang/st

FN-075 verified

A MUX selector outside 0..n faults the scan; clamping K with LIMIT keeps it in range.

where the docs say so

  • pass TestMuxOutOfRangeFaults in lang/conformance (4 of 4 passed)
  • pass TestConformance/st-mux-fault/LIMIT keeps a wandering selector in range, so the scan never faults in lang/conformance
  • pass TestBuiltinMuxOutOfRangeFaults in lang/st

FN-076 partly verified

MIN and MAX are n-ary numeric and LIMIT(MN, IN, MX) clamps IN into [MN, MX].

where the docs say so · LIMIT clamps at both ends, on INT and on REAL (TestENOnFunctionAssign); MIN and MAX of three INT inputs (and MAX of four, one a DINT) find the extreme in any position, in ST and FBD; MAX of an INT and a REAL is the REAL 2.5. Not asserted: MIN of REAL arguments. No test anywhere runs MIN on a REAL, and disabling the REAL comparison leaves every MIN test green.

  • pass TestBuiltinMinMaxLimit in lang/st
  • pass TestConformance/st-mux-fault/LIMIT keeps a wandering selector in range, so the scan never faults in lang/conformance
  • pass TestConformance/fn-select-numeric/MIN and MAX of three or four inputs find the extreme wherever it sits in lang/conformance
  • pass TestMixedNumericArgsPromoteStringNumberRefused in lang/st (8 of 8 passed)
  • pass TestENOnFunctionAssign in lang/st

FN-077 verified

ABS returns the absolute value in the argument's own type.

where the docs say so · Values for INT, DINT and REAL arguments (ST and FBD); the result type is the argument's for SINT, INT, DINT, LINT, UINT, UDINT and REAL (the compiler names it when the result cannot be assigned).

  • pass TestBuiltinABS in lang/st
  • pass TestConformance/fn-select-numeric/ABS is the absolute value of an INT, a DINT and a REAL, each in its own type in lang/conformance
  • pass TestB1ABSKeepsArgumentType in lang/st

FN-078 verified

SQRT, LN, LOG (base 10, not natural), EXP, EXPT and the trigonometric functions SIN/COS/TAN (radians), ASIN/ACOS/ATAN and the quadrant-correct ATAN2 return REAL with the stated meaning.

where the docs say so

FN-079 verified

TRUNC truncates toward zero for both signs, into the 64-bit integer an undeclared INT is; TRUNC_<int> does the same into the typed integer.

where the docs say so · Toward-zero truncation is asserted for TRUNC and the typed TRUNC_DINT / TRUNC_INT; TRUNC(±5000000000.7) = ±5000000000 shows the result is a 64-bit integer.

  • pass TestConformance/fn-conversions/TRUNC goes toward zero for both signs in lang/conformance
  • pass TestConformance/st-conversions-matrix/REAL to integer rounds half to even; TRUNC goes toward zero in lang/conformance
  • pass TestConversionSemantics in lang/ir
  • pass TestBuiltinTruncStaysTruncation in lang/st
  • pass TestConformance/fn-select-numeric/TRUNC goes toward zero into a 64-bit integer - a value beyond 32 bits survives in lang/conformance

FN-080 verified

SHL and SHR shift left / right (SHR zero-fills) and ROL and ROR rotate; all operate over 64 bits, so a WORD you think of as 16 bits rotates as a 64-bit value.

where the docs say so

  • pass TestConformance/st-int-width/shifts act on 64 bits in lang/conformance
  • pass TestConformance/st-int-width/ROL and ROR rotate over 64 bits, not the declared 16 in lang/conformance
  • pass TestBuiltinBitOps in lang/st

FN-081 verified

Word.3 is bit 3 of an integer as a BOOL, readable anywhere a BOOL is (IF, a ladder contact, a compare argument) and assignable (a ladder coil too), numbered from 0 at the least significant end; the IEC Word.%X3 spelling works, and so do bits of array elements and structure members.

where the docs say so · TestBitAccessInIfCompareArrayAndStruct reads a bit in an IF and as a compare argument, and reads and writes bits of an array element (Words[2].15) and a structure member (P101.Status.12) in both spellings. The st-partial-access corpus runs a ladder contact and coil on a bit in both the .%X3 and the Logix .3 spelling. TestBitAccessInLadder was removed: it checks lowering text only.

  • pass TestBitAccess in lang/st
  • pass TestBitAccessInIfCompareArrayAndStruct in lang/st
  • pass TestConformance/st-partial-access in lang/conformance (5 of 5 passed)

FN-082 verified

Partial access (w.%B1, w.%W0, w.%D1, w.%L0) reads as an unsigned integer of that width and writes back only its own bits; ladder contacts/coils and FBD wires take every form.

where the docs say so · %X, %B, %W, %D and %L read and write back only their own bits, and parts with their top bit set read unsigned (128, 32768, 2147486216), in ST and FBD wires. Ladder contacts and coils take %X and .n, and a function contact's argument takes %B unsigned (GT(Alarms.%B3, 100) passes power at 16#80). A ladder assignment element { x := w.%W1 } does not parse; the page promises partial access in contacts, coils and FBD wires, not in assignment elements.

  • pass TestConformance/st-partial-access in lang/conformance (5 of 5 passed)
  • pass TestPartialAccessReadWrite in lang/st
  • pass TestPartialAccessUnsignedAndL in lang/st

FN-083 verified

A partial access must fit the declared type: d.%X32 on a DINT or w.16 on a WORD is a compile error that names the type.

where the docs say so

  • pass TestPartialAccessErrorsNameDeclaredType in lang/st

FN-084 verified

String functions use 1-based positions, and length and position arguments clamp to the string instead of faulting (LEFT, RIGHT, MID, INSERT, DELETE, REPLACE).

where the docs say so

FN-085 verified

FIND returns the 1-based position of IN2 in IN1, and 0 when absent or when IN2 is empty.

where the docs say so

  • pass TestConformance/st-string-clamp/FIND is 1-based and gives 0 when absent or when the needle is empty in lang/conformance

FN-086 verified

LEN returns the length, and CONCAT is n-ary.

where the docs say so

  • pass TestBuiltinStrings in lang/st
  • pass TestConformance/st-string-clamp/in-range arguments behave as the standard says (1-based) in lang/conformance

FN-087 verified

Every <A>_TO_<B> between the 18 elementary types exists (294 functions) except the marked cells, which are compile errors, as is the overloaded TO_<type> for the same cells; there are no implicit conversions across kinds.

where the docs say so

  • pass TestConversionMatrixShape in lang/ir
  • pass TestConversionCompileErrors in lang/st
  • pass TestConversionMatrixThroughST in lang/st

FN-088 verified

Integer narrowing wraps: the result keeps the target's low bits, read signed or unsigned as the target is (DINT_TO_INT(70000) = 4464, INT_TO_USINT(-1) = 255, BYTE_TO_SINT(255) = -1, SINT_TO_WORD(-1) = 16#FFFF).

where the docs say so

  • pass TestConformance/st-conversions-matrix/integer narrowing wraps at the target width (DINT to INT, UINT, BYTE) in lang/conformance
  • pass TestConversionSemantics in lang/ir

FN-089 verified

The argument of a conversion is read as its own declared type first, so UINT_TO_DINT(u) with u at -1 sees 65535.

where the docs say so

  • pass TestConformance/st-conversions-matrix/sign - a signed source sign-extends, an unsigned one does not in lang/conformance
  • pass TestConversionSemantics in lang/ir

FN-090 verified

REAL to integer and REAL to TIME round to nearest, ties to even (2.5 to 2, 3.5 to 4, -2.5 to -2); TRUNC* go toward zero.

where the docs say so

  • pass TestConformance/fn-conversions/REAL_TO_INT rounds half to even, not half away from zero in lang/conformance
  • pass TestConformance/fn-conversions/REAL_TO_TIME rounds milliseconds half to even in lang/conformance
  • pass TestConformance/st-conversions-matrix/REAL to integer rounds half to even; TRUNC goes toward zero in lang/conformance
  • pass TestBuiltinRealRoundingTiesToEven in lang/st

FN-091 verified

A REAL with no value in the target (beyond its range, NaN or infinite) faults the scan; LREAL_TO_SINT(127.5) faults while LREAL_TO_SINT(-128.5) rounds to the even -128 and does not.

where the docs say so

  • pass TestConversionRangeFaults in lang/conformance (9 of 9 passed)
  • pass TestConversionFaults in lang/ir
  • pass TestConformance/st-conversions-matrix/REAL to integer rounds half to even; TRUNC goes toward zero in lang/conformance

FN-092 verified

REAL and bit-string conversions are binary transfers of the IEEE 754 pattern, only at the same width (REAL_TO_DWORD(1.0) = 16#3F800000); a pattern that is NaN or infinite faults; REAL_TO_WORD is a compile error that says what to write instead.

where the docs say so · TestConversionInfinitePatternFaults: +/-Inf patterns fault for DWORD_TO_REAL and LWORD_TO_LREAL.

  • pass TestConformance/st-conversions-matrix/REAL to DWORD and back is the IEEE 754 bit pattern (binary transfer) in lang/conformance
  • pass TestConversionRangeFaults/NaN bit pattern in lang/conformance
  • pass TestConversionCompileErrors in lang/st
  • pass TestConversionSemantics in lang/ir
  • pass TestConversionInfinitePatternFaults in lang/ir

FN-093 verified

Integers convert to REAL exactly up to 2^53 (REAL and LREAL are both float64 at run time), ULINT and LWORD convert unsigned, and REAL_TO_LREAL / LREAL_TO_REAL change only the type.

where the docs say so · TestConversionExactUnsignedAndRealLreal: 16777217 and 2^53-1 exact as REAL, REAL_TO_LREAL / LREAL_TO_REAL keep 0.1 and 16777217 and are typed LREAL / REAL, ULINT and LWORD all-ones read as 2^64-1 (LWORD_TO_STRING, TO_STRING of an LWORD, ULINT_TO_REAL).

  • pass TestConformance/st-conversions-matrix/integers to REAL - REAL holds LREAL precision here in lang/conformance
  • pass TestConversionSemantics in lang/ir
  • pass TestConversionExactUnsignedAndRealLreal in lang/ir

FN-094 verified

BOOL converts to 0/1 of any numeric type (1 ms for TIME); any number converts to TRUE when it is nonzero in the source width (BYTE_TO_BOOL(256) is FALSE).

where the docs say so

  • pass TestConformance/st-conversions-matrix/BOOL is 0 and 1; a number is TRUE when nonzero in the source width in lang/conformance
  • pass TestConformance/fn-conversions/BOOL to INT and REAL, INT and REAL to BOOL in lang/conformance
  • pass TestConversionSemantics in lang/ir

FN-095 verified

TIME and LTIME count milliseconds (LTIME is not nanoseconds), and TIME_TO_INT wraps like any integer narrowing.

where the docs say so · LTIME appears in neither conformance program; TestConversionSemantics covers it (TIME_TO_LTIME(42) = 42, LTIME_TO_LREAL(250) = 250, LTIME#5000ms both ways), which a nanosecond LTIME would fail.

  • pass TestConformance/st-conversions-matrix/TIME is milliseconds, and wraps like any integer when narrowed in lang/conformance
  • pass TestConformance/fn-conversions/TIME is milliseconds in both directions in lang/conformance
  • pass TestConversionSemantics in lang/ir

FN-096 verified

STRING_TO_x parses after trimming blanks: decimal or base-prefixed integers (16#FF, 2#1010, 8#17) with _ separators, decimal/exponent REALs, durations (T#1m30s, 90s, 500ms), and BOOL as TRUE/FALSE/1/0 in any case.

where the docs say so · TestStringParsesEveryDocumentedForm: 8#17 (with _), 16#FF, 2#1010, exponent REALs (1.5e3, 2E-2), bare durations 90s / 500ms / 1m30s, and BOOL in mixed case (True, fAlSe), each after trimming.

  • pass TestConformance/fn-conversions/parsing from STRING in lang/conformance
  • pass TestConformance/st-conversions-matrix/STRING formatting, parsing and round trips in lang/conformance
  • pass TestConversionSemantics in lang/ir
  • pass TestStringParsesEveryDocumentedForm in lang/ir

FN-097 verified

Text that does not parse, or parses to a value the target cannot hold (STRING_TO_SINT('128'), STRING_TO_UINT('-1')), faults the scan.

where the docs say so

FN-098 verified

Formatting to STRING is decimal for integers (unsigned for U- and bit-string types), the shortest exact form for REAL, T#5000ms / LTIME#5000ms for durations, so STRING_TO_x(x_TO_STRING(v)) gives v back.

where the docs say so

  • pass TestConformance/fn-conversions/formatting to STRING in lang/conformance
  • pass TestConversionSemantics in lang/ir
  • pass TestConversionStringRoundTrip in lang/ir

FN-099 verified

TO_<type>(x) is the overloaded form (an expression's integer converts as LINT, an enumeration as its integer); TRUNC_<int>(x) and <real>_TRUNC_<int>(x) truncate toward zero; TO_<Enum>(n) gives the member.

where the docs say so · TestToTypeOfIntegerExpressionIsLINT: TO_DINT(x + x) with INT x = 30000 is 60000 (an INT variable holding it converts to -5536), TO_LINT of a DINT product keeps all its bits, TO_STRING of an expression is the LINT's text.

  • pass TestOverloadedTo in lang/ir
  • pass TestConversionSemantics in lang/ir
  • pass TestConversionMatrixThroughST in lang/st
  • pass TestEnumConversions in lang/st
  • pass TestToTypeOfIntegerExpressionIsLINT in lang/st

FN-100 verified

DATE, TIME_OF_DAY and DATE_AND_TIME are not run-time types yet and have no conversions.

where the docs say so · Every spelling of the three (DATE, TIME_OF_DAY, TOD, DATE_AND_TIME, DT) is refused as a type, no typed or TO_<type> conversion to or from them exists, and their literals (D#, DATE#, TOD#, TIME_OF_DAY#, DT#, DATE_AND_TIME#) are refused by naut check instead of compiling as integers (#307).

  • pass TestDateTypesAreNotRuntimeTypes in lang/st (20 of 20 passed)
  • pass TestCheckRejects/D literal in cmd/naut
  • pass TestCheckRejects/TOD literal in cmd/naut
  • pass TestCheckRejects/TIME_OF_DAY literal in cmd/naut

FN-101 verified

TON (on-delay): Q rises after IN has been TRUE for PT; ET is the elapsed time.

where the docs say so

FN-102 verified

TOF (off-delay): Q stays TRUE for PT after IN drops.

where the docs say so

FN-103 verified

TP (pulse): rising IN produces a PT-wide TRUE pulse, a rising edge during the pulse is ignored, afterwards ET holds at PT while IN stays TRUE and returns to 0 the scan IN is FALSE.

where the docs say so

FN-104 verified

CTU counts rising CU edges, Q is TRUE when CV reaches PV, and R resets.

where the docs say so · fb-ctu (ST, LD, FBD) pins counting on rising edges only (held CU and its fall do not count), Q on the scan CV reaches PV and staying TRUE past it, R resetting CV and Q, and R dominating a same-scan CU edge.

FN-105 verified

CTD counts down from PV (LD loads PV), and Q is TRUE when CV is at or below 0.

where the docs say so

FN-106 verified

CTUD (inputs CU, CD, R, LD, PV; outputs QU, QD, CV) is an up/down counter.

where the docs say so

FN-107 verified

R_TRIG's Q is TRUE for exactly one scan on CLK's rising edge.

where the docs say so · fb-r-trig (ST, LD, FBD) pins one scan on the rise, nothing while held or on the fall, first scan with CLK TRUE, and a toggling CLK.

FN-108 verified

F_TRIG's Q is a one-scan pulse on CLK's falling edge.

where the docs say so

FN-109 verified

SR (S1, R → Q1) is a set-dominant latch.

where the docs say so

FN-110 verified

RS (S, R1 → Q1) is a reset-dominant latch.

where the docs say so

FN-111 verified

Each function-block instance keeps its own state between scans, and its outputs read as inst.Pin from any language.

where the docs say so · fb-instances runs two CTU instances in ST, LD, FBD and SFC: each count carries across idle scans, edges on one never move the other, and Q (BOOL) and CV (INT) are read as inst.Pin in all four languages.

FN-112 verified

With EN FALSE a function call does not execute: its result is not assigned and the variable it drives keeps its value.

where the docs say so

  • pass TestConformance/fbd-en-eno/EN FALSE leaves the function's result unassigned in lang/conformance
  • pass TestConformance/fbd-en-eno/EN FALSE from power-up never assigns (the tag keeps its init) in lang/conformance
  • pass TestENOnFunctionAssign in lang/st

FN-113 verified

With EN FALSE a function-block body does not run: outputs, every => binding and internal state (a counter's edge memory, a timer's start) keep their values until it runs again.

where the docs say so

  • pass TestConformance/fbd-en-eno/an FB with EN FALSE does not run — edges it misses are not counted in lang/conformance
  • pass TestConformance/fbd-en-eno/the FB's edge memory is stale while disabled — a rise across it counts on return in lang/conformance
  • pass TestENOnFBCall in lang/st

FN-114 verified

ENO = EN AND no error; a call that errors faults the scan, so a call that returns has ENO = EN, except FBD's DIV and MOD whose ENO is FALSE on a zero divisor (the result is still 0, counted in divZero).

where the docs say so · ENO follows EN for LIMIT and CTU (TRUE when run, FALSE when not); DIV's and MOD's ENO is FALSE on a zero divisor with the result 0, and each zero divisor is counted in divZero while the scan keeps running (TestB1FBDDivModZeroENOAndDivZero, FBD on the runtime); a call that errors (MUX out of range) faults the scan.

  • pass TestConformance/fbd-en-eno/EN TRUE runs the call and ENO is TRUE in lang/conformance
  • pass TestConformance/fbd-en-eno/DIV's ENO is FALSE on a zero divisor, TRUE otherwise in lang/conformance
  • pass TestConformance/fbd-en-eno/DIV with EN FALSE holds its result and reports ENO FALSE in lang/conformance
  • pass TestConformance/fbd-en-eno/EN FALSE leaves the function's result unassigned in lang/conformance
  • pass TestENOModByZero in lang/fbd
  • pass TestB1FBDDivModZeroENOAndDivZero in runtime
  • pass TestMuxOutOfRangeFaults in lang/conformance (4 of 4 passed)

FN-115 verified

In ST, EN and ENO are formal arguments (t1(EN := Enable, ..., ENO => t1Ok); LIMIT(EN := ..., ENO => ok)).

where the docs say so

  • pass TestENOnFBCall in lang/st
  • pass TestENOnFunctionAssign in lang/st
  • pass TestENOOnlyAndPositional in lang/st

FN-116 verified

A function's EN/ENO needs its call to be a statement of its own (the whole right-hand side of an assignment, or a user FUNCTION called as a statement); inside a larger expression it is a compile error.

where the docs say so · The compile error inside a larger expression (a standard function and a user FUNCTION) and the whole-right-hand-side form are asserted; a user FUNCTION called as a statement with EN FALSE does not run its body and writes ENO FALSE, and with EN TRUE runs with ENO TRUE.

  • pass TestENErrors in lang/st
  • pass TestENOnFunctionAssign in lang/st
  • pass TestENOnUserFunctionCalledAsStatement in lang/st

FN-117 verified

In FBD, any block takes EN/ENO the same way; a block with EN/ENO must drive a variable (never another block's input), and ENO reads as a wire (t1.ENO, sp.ENO) that feeds a coil or the next block's EN.

where the docs say so · EN/ENO on LIMIT, DIV, TON and CTU, the "must drive a variable" error, and ENO wired to a coil are asserted. TestB1ENOFeedsTheNextBlocksEN chains ENO into the next block's EN through every kind of block (a standard function, an operator block, a user FUNCTION, a standard FB, a user FUNCTION_BLOCK, DIV): EN FALSE at the head stops the whole chain, a zero divisor at the tail drops only the last ENO.

FN-118 verified

A user block that declares its own EN input or ENO output keeps them as ordinary pins: its body decides what they mean.

where the docs say so · Gate's body runs with its own EN FALSE (it counts the call but not the run), so EN is an ordinary pin, not execution control.

  • pass TestENOnUserFunctionStatementAndOwnEN in lang/st

FN-119 verified

In a rung, rung power drives a standard block's power-in pin (TON/TOF/TP IN, CTU CU, CTD CD, R_TRIG/F_TRIG CLK, SR S1, RS S) and continues from its power-out pin (Q, or Q1 for SR/RS).

where the docs say so · The ld column of each feature puts the block in a rung with power on its power-in pin (IN, CU, CD, CLK, S1, S) and its coil after the block, so the power-out pin (Q, Q1) is asserted against ST and FBD.

FN-120 verified

For a user FUNCTION_BLOCK, power drives EN else the first BOOL VAR_INPUT the call does not bind by name, and continues from ENO else the first BOOL VAR_OUTPUT.

where the docs say so · First free BOOL input and first BOOL output (PowerPins); a declared EN taking power over an earlier free BOOL input, ENO over the first BOOL output, and an earlier BOOL input bound by name being skipped (PowerPinPreference, run).

  • pass TestLadderFunctionBlockPowerPins in lang/ld
  • pass TestLadderFunctionBlockPowerOutOfENO in lang/ld
  • pass TestLadderFunctionBlockPowerPinPreference in lang/ld (3 of 3 passed)

FN-121 verified

Passing the power pin explicitly in the argument list (t1:TON(IN := x)) is a compile error: power owns it.

where the docs say so

FN-122 verified

A block with no power-in (every BOOL input bound by name, or none) is called unconditionally and may only sit on a rail-driven rung; with contacts ahead of it is the error "no free BOOL input for the rung's power".

where the docs say so · Both shapes: every BOOL input bound by name (refused behind a contact, run on a rail-driven rung) and a block with no BOOL input at all (refused with the message behind a contact, called on every scan on the rail).

  • pass TestLadderFunctionBlockPowerPins in lang/ld
  • pass TestLadderFunctionBlockWithNoBoolInputs in lang/ld
  • pass TestLadderFunctionBlockInOutStructPin in lang/ld

FN-123 verified

A block with no BOOL output passes power through unchanged, so whatever conditioned the block still conditions the coils to its right.

where the docs say so

  • pass TestLadderFunctionBlockPowerPins in lang/ld

FN-124 verified

A user block's pins are resolved from the whole compile (this file's FUNCTION_BLOCKs plus every project library); a block whose type nothing in the compile declares falls back to IN/Q.

where the docs say so · Pins resolved from this file (PumpSeq's Start in TestLadderFunctionBlockLowers), another file (Debounce's IN/OUT) and project libraries; an undeclared type takes power on IN and passes it on from Q.

  • pass TestLadderFunctionBlockLowers in lang/ld
  • pass TestLadderFunctionBlockNested in lang/ld
  • pass TestLadderLibraryProjectRuns in internal/project
  • pass TestLadderUndeclaredBlockFallsBackToINQ in lang/ld

FN-125 verified

PID output CV is clamped to [CV_MIN, CV_MAX], and leaving both unbound falls back to the IEC 0..100 range instead of pinning CV at zero.

where the docs say so · Both ends are asserted (CV_MIN 10 / CV_MAX 90 with the loop asking for -1000, +1000 and 50 in TestPIDClampsAtCVMin), and the unbound pair gives 0..100 (TestPIDDefaultClampRange).

  • pass TestPIDDefaultClampRange in lang/st
  • pass TestPIDAntiWindupStaysClamped in lang/st
  • pass TestPIDClampsAtCVMin in lang/st

FN-126 verified

DIRECT := FALSE is reverse acting (error = SP - PV, raise CV when PV is low, a heater) and DIRECT := TRUE direct acting (error = PV - SP, a cooling valve).

where the docs say so

  • pass TestPIDDirectVsReverse in lang/st

FN-127 verified

Manual mode (AUTO = FALSE): CV tracks CV_MAN and the integral is back-solved every scan so returning to AUTO is bumpless, with no jump in CV.

where the docs say so · TestPIDManualTracksMovingCVMan moves CV_MAN, PV and SP on every manual scan: CV follows CV_MAN, P_TERM + I_TERM + D_TERM equals CV_MAN on each scan (back-solved every scan, not once on entering MANUAL), and the switch to AUTO continues from the last CV_MAN.

  • pass TestPIDManualToAutoBumpless in lang/st
  • pass TestPIDManualTracksMovingCVMan in lang/st

FN-128 verified

Anti-windup is conditional integration: the integral does not accumulate when that would push the unclamped output further past a rail it has reached, CV comes off the rail as soon as the error reverses, and SAT_HI / SAT_LO report the clamp.

where the docs say so · Each rail is asserted: I_TERM frozen while clamped, SAT_HI / SAT_LO set, and CV off the rail on the scan the error reverses.

  • pass TestPIDAntiWindupStaysClamped in lang/st
  • pass TestPIDAntiWindupLowRail in lang/st

FN-129 partly verified

DT left at 0 (unbound) makes the block measure elapsed time itself from the scan clock; a bound DT is used as seconds since the last call.

where the docs say so · A bound DT is asserted: 0.5 s per scan integrates 0.5 s while the clock advances 1 s (and once 3 s). The unbound case is not: TestPIDDTZeroUsesScanDt advances the clock a constant 1 s per scan, so a fallback hard-coded to 1 s passes it too (every TestPID* stays green with dt = 1.0 in place of the measured interval). An irregular clock with DT unbound is needed to show the elapsed time is measured.

  • pass TestPIDDTZeroUsesScanDt in lang/st
  • pass TestPIDBoundDTOverridesClock in lang/st

FN-130 verified

PID is closed-loop control: around a process it drives PV to the setpoint and holds it there.

where the docs say so

  • pass TestPIDStepResponseConverges in lang/st

FN-131 verified

KP = 0 disables the whole controller, not just the P term; KI = 0 disables integral action and KD = 0 derivative action.

where the docs say so · KP = 0 with KI and KD nonzero and PV moving keeps CV, P_TERM, I_TERM and D_TERM at 0; KI = 0 keeps I_TERM at 0 under a held error (a KI > 0 control integrates); KD = 0 keeps D_TERM at 0 under a PV ramp.

  • pass TestPIDZeroGainsDisable in lang/st (4 of 4 passed)

FN-132 verified

The derivative acts on PV, not on error (a setpoint step never kicks D_TERM), through a fixed first-order filter with time constant KD/10.

where the docs say so · Setpoint steps with PV still leave D_TERM at 0; a PV step's D_TERM decays with the filter weight DT/(KD/10 + DT), asserted at KD = 10 and KD = 30.

  • pass TestPIDDerivativeOnPVFiltered in lang/st (2 of 2 passed)

FN-133 verified

DB is a deadband on error: within +/-DB the P and I terms see zero error, while the D term still sees every PV move.

where the docs say so · PV wandering inside ±DB leaves P_TERM and I_TERM at 0 while D_TERM follows each move; outside the band the full error reaches P and I.

FN-134 verified

RESET = TRUE zeroes the integral this scan.

where the docs say so

  • pass TestPIDResetZeroesIntegral in lang/st

FN-135 verified

The diagnostic outputs hold their stated values: ERR is the error before the deadband, and P_TERM, I_TERM, D_TERM are the three contributions to CV.

where the docs say so · ERR, P_TERM, I_TERM, D_TERM and CV = P + I + D are asserted against hand-computed values over four scans, reverse and direct acting, the last scan inside the deadband (ERR raw, P_TERM 0). The block's first AUTO scan does not integrate (the bumpless-transfer rule), so the integral counts from the second scan.

  • pass TestPIDDiagnosticTerms in lang/st

FN-136 verified

A function-block instance can be declared in an array (Timers : ARRAY[0..3] OF TON); each element is its own instance with its own retained state, callable by index in ST and in a rung, and the index may be a variable.

where the docs say so · In ST and in a rung, a TON element called by a variable index keeps its own elapsed time across scans in which the index points elsewhere, and elements never called stay idle.

  • pass TestFBInstanceArray in lang/st
  • pass TestFBInstanceArrayVariableIndexRuns in lang/st
  • pass TestFBInstanceArrayInLadder in lang/ld
  • pass TestFBInstanceArrayVariableIndexInLadderRuns in lang/ld

FN-137 verified

On a Logix target an array of instances is one TIMER[4] tag; an element called with a literal index carries its preset in the tag's data, one called with a computed index takes a MOVE to its .PRE ahead of the rung. Once an array is called with a computed index anywhere, its literal-index calls also re-assert their presets with a MOVE ahead of their rungs.

where the docs say so · Asserted: ARRAY[0..3] OF TON is one TIMER tag with Dimensions 4; with only literal indices each element's preset is in the tag's data and no MOVE is emitted; a computed index takes MOVE(1000,Timers[i].PRE) ahead of its rung. On an array also called with a computed index, the literal-index elements still carry their presets in the tag's data (TestArraysOfBlocks: PRE 2000 / 0 / 500; the arrays golden: [[0,2000,0],[0,0,0],[0,0,0],[0,500,0]]) and their calls take MOVE(2000,Timers[0].PRE) ahead of the rung (#331). The re-assert is the Echo finding in docs/design/logix-authoring.md: a MOVE to .PRE is sticky on the controller. Written, imported and written again, every call keeps its PT := and the rungs are identical.

FN-138 verified

dialect: names are nautilus (the default, adds nothing), logix, and the reserved siemens and codesys.

where the docs say so · nautilus adding nothing, logix having a library, and siemens reserved and empty (Sources); a project with no dialect: or dialect: nautilus has no TONR while dialect: logix does, and dialect: siemens and dialect: codesys are accepted by the manifest.

FN-139 verified

The logix dialect's TONR is a TON whose Reset clears the accumulated time and Q while TRUE; the free-running pulse is t:TONR(PT := T#1S, Reset := t.Q).

where the docs say so · dialect-logix-tonr composes the logix dialect through dialect: logix and runs TONR in ST, ladder and FBD: Reset clears ET and Q and holds them cleared while TRUE, then the timer times from scratch; without Reset it is a TON; and the free-running pulse (Reset := Q, PT 1s) gives one TRUE scan per period, twice in a row.

FN-140 verified

On a Logix controller a TONR in ladder is written as a TON with a RES(t) rung ahead of the timer's rung, and in ST as TONR(t) on an FBD_TIMER whose .Reset is TRUE while Reset is TRUE or IN is FALSE.

where the docs say so

FN-141 verified

A manifest tag's alias: is emitted by the Logix writer as an alias tag, and the L5X importer turns an export's alias tags, and rack points the logic named directly, into tags with alias:, and a rung that names an alias tag imports with the tag typed through its target.

where the docs say so · The writer's alias tags, the importer turning an export's alias tag (TagType="Alias" AliasFor=...) into a tag with alias:, and rack points named directly in the logic, are asserted. Rungs naming alias tags of every target kind (a base tag, a UDT member, a UDT BIT member, an Add-On Instruction parameter, a bit of a DINT, an array element, a rack point's bit) import with each tag declared as its target's type, keep alias: in the tag file, and write back to the export's rung text with each tag an alias tag of the same target (#330). An alias the export cannot type is refused by name, each case asserted: a whole rack word with no module definition, a target not in the export, a whole TIMER, a member the target's type lacks, and a program-scoped alias tag.

FN-142 verified

The nautilus runtime ignores a tag's alias: binding: the tag is a tag, so a simulation runs the same source.

where the docs say so · tag-alias gives every tag an alias: naming a rack point and runs a start/stop seal-in on them in ST, ladder and FBD: inputs reach the programs and outputs hold their writes exactly as plain tags.

FN-143 partly verified

A FUNCTION_BLOCK pin (VAR_INPUT / VAR_OUTPUT / VAR_IN_OUT / VAR) and a FUNCTION's inputs, locals and return type may be any type, including a user TYPE with nested structs, resolved against the whole compile; a pin naming an undeclared type is a compile error naming the type.

where the docs say so · Asserted: VAR_INPUT, VAR_OUTPUT, VAR_IN_OUT and VAR of a nested user TYPE, a TYPE from a library joined ahead, a FUNCTION's input, local and return of a nested user TYPE, and the unknown-type error naming the type; elementary pins are used throughout. Not asserted: "any type" for ARRAY types. No test declares a VAR_INPUT, VAR_OUTPUT or VAR_IN_OUT of an ARRAY type, or a FUNCTION input of one.

  • pass TestFBStructPins in lang/st
  • pass TestFBStructPinFromLibraryFile in lang/st
  • pass TestFBUnknownPinTypeStillErrors in lang/st
  • pass TestVarInOutStructLocal in lang/st
  • pass TestUserTypeAsFunctionArgLocalReturnAndBlockVar in lang/st

FN-144 verified

A call may leave any VAR_INPUT out: it keeps its value, the declared initial value until something writes it (as an unconnected FB input in TIA or CODESYS); EN, unbound, is TRUE.

where the docs say so · TestUnboundInputKeepsTheWrittenValue: an omitted input reads its initial 7, then 100 after one call writes it, and keeps 100 on later calls that omit it; a user block called with EN unbound runs and reports ENO TRUE.

  • pass TestENOOnlyAndPositional in lang/st
  • pass TestUserFB_UnboundInputKeepsInitial in lang/st
  • pass TestUnboundInputKeepsTheWrittenValue in lang/st

FN-145 verified

A VAR_IN_OUT is bound to a variable, and what the block writes into it is visible to the caller when the call returns (scalar, struct local, VAR_EXTERNAL struct tag as one whole-struct write).

where the docs say so

  • pass TestVarInOutScalar in lang/st
  • pass TestVarInOutStructLocal in lang/st
  • pass TestVarInOutExternalStructTag in lang/st
  • pass TestUserFB_VarConstant_InOutWriteBack in lang/st

FN-146 verified

A VAR_IN_OUT argument must be assignable (a variable, a struct field, an array element or a VAR_EXTERNAL tag); an expression is a compile error.

where the docs say so · TestVarInOutAcceptsEveryAssignableForm binds a variable, a struct field, array elements (literal and variable index), a VAR_EXTERNAL tag and a tag's field, and each receives the block's write.

  • pass TestVarInOutRejectsNonLValue in lang/st (4 of 4 passed)
  • pass TestVarInOutAcceptsEveryAssignableForm in lang/st

FN-147 verified

The argument's type must match a VAR_IN_OUT pin exactly (an INT does not stand in for a REAL pin).

where the docs say so

  • pass TestVarInOutTypeMustMatch in lang/st

FN-148 verified

Every VAR_IN_OUT must be bound at every call site (a compile error otherwise).

where the docs say so

  • pass TestVarInOutMustBeBound in lang/st

FN-149 verified

A VAR_IN_OUT pin's own type may not be a function block.

where the docs say so

  • pass TestVarInOutRejectsFBPinType in lang/st

FN-150 verified

Two calls in one scan bound to different variables each see and update their own.

where the docs say so · One instance called twice in a scan with a and c, and two instances with x and y: each call sees its own variable's value on entry and writes back to it, over two scans.

  • pass TestVarInOutTwoCallsSameScanEachOwnVariable in lang/st

FN-151 verified

FUNCTIONs have no VAR_IN_OUT or VAR_OUTPUT: an IEC function is a single return value, and the compiler says so.

where the docs say so · Both VAR_IN_OUT and VAR_OUTPUT in a FUNCTION are compile errors naming the section and "single return value".

  • pass TestFunctionRefusesInOutAndOutput in lang/st (3 of 3 passed)

FN-152 verified

A VAR_IN_OUT is bound with :=, like an input; the => form binds outputs, so it is not the way to bind an IN_OUT (a compile-time rule).

where the docs say so · Binding an IN_OUT with := runs (TestVarInOutScalar); => on a VAR_IN_OUT is refused at compile time, alone (unbound, "N := <variable>") and beside a := binding ("is a VAR_IN_OUT - bind it with N := <variable>").

  • pass TestVarInOutScalar in lang/st
  • pass TestVarInOutRefusesOutputBinding in lang/st

FN-153 verified

A .ld file may define FUNCTION_BLOCKs whose body is rungs, each instance with its own retained state (two pumps are two instances, each with its own seal-in and its own t1).

where the docs say so · Two PumpSeq instances in one program each hold their own seal-in and their own t1, started three seconds apart and finishing three seconds apart.

  • pass TestLadderFunctionBlockRuns in lang/ld
  • pass TestLadderFunctionBlockLowers in lang/ld
  • pass TestLadderFunctionBlockTwoInstancesIndependent in lang/ld

FN-154 verified

A ladder block's VAR_* sections are ordinary POU declarations, VAR_IN_OUT included, so a ladder block can take a UDT by reference.

where the docs say so

  • pass TestLadderFunctionBlockInOutStructPin in lang/ld

FN-155 verified

A ladder block is called from ladder with power on its power-in pin and => capturing outputs, and from ST or FBD like any other block.

where the docs say so · Ladder (power on Start, Run => PumpRun), ST and FBD programs each call the ladder block and run its seal-in.

  • pass TestLadderFunctionBlockRuns in lang/ld
  • pass TestLadderFunctionBlockFromST in lang/ld
  • pass TestLadderAndFBDLibrariesCalledFromEveryLanguage in internal/project

FN-156 verified

An FB instance a rung declares inline needs no VAR entry, and a declaration in the POU's own header wins (the rung is a call on it, not a second declaration).

where the docs say so · TestLadderFunctionBlockNested runs an inline d1:Debounce() with no VAR entry; TestLadderFunctionBlockLowers asserts a header-declared t1 is declared once and compiles. Only the POU's own header counts: an inline t1:TON still runs with an ST block's VAR t1 earlier in the file (TestLadderInlineInstanceNotHiddenByLibraryBlockVar, #332). Removed: TestInlineRungBody (a one-line rung, not an inline instance), TestInsertReusesHeaderDeclaredInstance (an editor insert) and the FBD TestInstanceDeclaredInHeaderIsNotRedeclared (not ladder).

  • pass TestLadderFunctionBlockNested in lang/ld
  • pass TestLadderFunctionBlockLowers in lang/ld
  • pass TestLadderInlineInstanceNotHiddenByLibraryBlockVar in lang/ld

FN-157 verified

A .ld file with no PROGRAM is a project library whose blocks join the prelude ahead of every task, usable from any language; .fbd libraries work the same way.

where the docs say so · A PROGRAM-less .ld and a PROGRAM-less .fbd library are both composed, and ST, LD, FBD and SFC tasks of one project each call both libraries' blocks.

FN-158 verified

A library may sit in the project root or under lib/; lib/ holds libraries only (a PROGRAM there is an error) and no other subdirectory composes.

where the docs say so · The libdir fixture has lib/ libraries only; the project-root case is TestLadderLibraryProjectRuns, whose blocks.ld and scale.st sit in the root and run.

FN-159 verified

Composition order is every .st library first, then every transpiled .ld / .fbd library, each group sorted by project-relative path; order never decides whether a call resolves (blocks may reference each other in either direction, across files), only which duplicate is reported.

where the docs say so · Tier order is asserted where path order disagrees with it (TestLadderLibraryComposes: scale.st composes ahead of blocks.ld). TestLibraryPathsRootAndLib sorts each tier by project-relative path, root and lib/ interleaved, with .fbd in the graphical tier. A block in both the root and lib/ reports the same duplicate error as two root files. a.st (composed first) instantiates a block from b.st and one from a .ld library, b.st calls a FUNCTION from a.st, and the project runs. TestLibDirComposes and the resolve test cannot tell tier order from path order on their own: their .st paths already sort first.

FN-160 verified

Declaring the same FUNCTION_BLOCK twice in one .ld file is refused with the second declaration's line, before anything is transpiled.

where the docs say so

  • pass TestLadderFunctionBlockErrors in lang/ld