Skip to content

feat!: move arithmetic from the standard library into the builtins - #1937

Merged
volsa merged 21 commits into
masterfrom
feat/prg-4854
Oct 1, 2026
Merged

volsa merged 21 commits into
masterfrom
feat/prg-4854

Conversation

@ghaith

@ghaith ghaith commented Sep 28, 2026 •

Copy link
Copy Markdown
Collaborator

Problem: The standard library declares a generic ADD<T1, T2> and MUL overload and about sixty ADD__X__Y-style functions whose only purpose is to give the compiler's generic resolution a symbol to link against, because the builtins only accept numbers. Arithmetic with a date or time argument depends on those shims, and ADD(dt, t, t) is rejected.

Solution: The builtins own all arithmetic. A table of the combinations IEC 61131-3 defines types and validates every ADD/SUB/MUL/DIV call; numbers expand to chained operators as before, and a call with a date or time argument expands to the same chain, whose operators the resolver already replaces with the standard library calls (ADD(dt, t, t) becomes ADD_DT_TIME(ADD_DT_TIME(dt, t), t)). Every shim leaves the standard library; the ADD and MUL overloads stay, because they give ADD(IN1 := a, b) its parameter name. Codegen now also converts the result of a replaced expression to the type of its target, so x := MUL(t, 1.5) with an LREAL x no longer stores the raw TIME bits; this affected the numeric builtins and the date and time operators on master too. Moving ADD_TIME and friends into the builtins as well is a follow-up: each row needs its own instructions in codegen (unit conversion for DT and DATE, reduction modulo one day for a time of day, saturation for a real factor, a defined zero divisor).

Refs: PRG-4854

Breaking:

  • ADD on two strings, or on any argument no arithmetic is defined for, is an error now (E156).
  • The ADD__*, SUB__*, MUL__TIME__*, DIV__TIME__* symbols and their long-family counterparts are gone from iec61131std; objects compiled against them must be recompiled.

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Build Artifacts

🐧 Linux

Artifact Link Size
plc-aarch64 Download 43.4 MB
deb-aarch64 Download 30.9 MB
stdlib Download 32.4 MB
plc-x86_64 Download 43.6 MB
schema Download 0.0 MB
deb-x86_64 Download 38.5 MB

From workflow run

🪟 Windows

Artifact Link Size
plc.exe Download 38.4 MB
stdlib.lib Download 4.0 MB
stdlib.dll Download 0.1 MB

From workflow run

@github-actions

Copy link
Copy Markdown

5 findings in 8m 56s for $0.37 between b69ea80 (master) and 44fcc60 (feat/prg-4854):

  • P2 book/technical/internals/08-annotated-ast.md:421: The table says ReplacementAst is produced for numeric builtin chains, but the first resolver pass also attaches it to ADD/SUB/MUL/DIV chains containing date/time operands; the date-time lowerer removes it only after that pass. The page's stated initial annotation shape is incomplete.
  • P2 book/technical/participants/08-date-time-arithmetic.md:3: The chapter says scaling a duration by a real saturates, but the TIME implementations return u32 after the helper's i64 result, so negative factors and results above u32::MAX wrap on the cast; the added tests explicitly cover negative-factor wrapping. This describes the runtime behavior incorrectly for TIME.
  • P2 book/technical/participants/09-generic.md:82: This unconditional claim says every ADD/SUB/MUL/DIV call with a date/time argument becomes a standard-library call, but bare-integer duration cases remain binary operations with E157 and unsupported combinations remain binary for E156. It misstates when the date-time lowerer emits a call.
  • P2 book/user/reference/standard-library.md:19: The self-linking checklist still lists only ** and text comparisons, although date/time operators now lower to iec61131std functions. A user following this reference can omit the library and get missing operator-function diagnostics or link errors.
  • P2 compiler/plc_diagnostics/src/diagnostics/error_codes/E156.md:3: E156 is documented as a date/time-only diagnostic, but the changed builtin validator also emits it for ordinary invalid arithmetic calls such as ADD(INT, STRING) (the updated validation snapshot now expects E156). The explanation therefore misidentifies the operands for a class of diagnostics.

@github-actions

Copy link
Copy Markdown

2 findings in 10m 33s for $0.38 between b69ea80 (master) and 2a79679 (feat/prg-4854):

  • P2 book/technical/pipeline/04-validation.md:102: The new sentence says the arithmetic builtins report E156 for any argument that is not a number, but valid date/time arguments are intentionally non-numeric and are accepted (for example ADD(dt, t)) and lowered to standard-library calls. Qualify this as arguments that are neither numeric nor part of a defined date/time combination.
  • P2 book/user/language/time.md:65: The table is presented as the complete set of date/time combinations but omits a number * TIME -> TIME (and the corresponding long-family form), even though the compiler explicitly accepts 2 * cycle and swaps the operands for MUL_TIME. Add the reverse-multiplication row or make the table explicitly cover both operand orders.

ghaith and others added 11 commits September 28, 2026 10:25
Problem: `dt + t`, `d1 - d2` and `tod + 5` compile to raw integer
operations on values with different units, and nothing tells the user
which combinations exist.

Solution: A table of the IEC 61131-3 date and time arithmetic in the
typesystem, checked by the validator for every binary expression with a
date or time operand: an undefined combination reports E156, a missing
standard function reports E073, and a duration combined with a bare
integer is accepted with warning E157.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Problem: `dt + t` adds seconds to milliseconds, `tod + t` never wraps at
midnight, and `t * 1.5` truncates the factor, because the operators on
date and time types were plain integer instructions.

Solution: The annotator types such an expression by the result the
standard defines and replaces it with the call of the standard library
function that carries it out (`ADD_DT_TIME`, `SUB_DATE_DATE`, ...), the
way a string comparison becomes `STRING_EQUAL`. A scaled duration calls
the implementation for the kind of number (`MUL_TIME__LINT`, `__ULINT`,
`__REAL`, `__LREAL`), which the standard library now declares, with the
number widened to it. A duration combined with a bare integer stays a
plain operation of the duration type.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ME width

Problem: `MUL_TIME__DINT` and the other short-family monomorphs take
and return `i64`, while the compiler passes and reads a 32-bit `TIME`.
The mismatch only works because 32-bit register writes zero-extend.

Solution: The short-family monomorphs take and return `u32` and
compute in 64 bits as before, so every observable result stays: a
product wraps modulo 2^32, a saturated float result is `u32::MAX`, a
NaN factor yields zero, and a zero integer divisor panics.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Problem: `ADD_LTOD_LTIME` and `SUB_LTOD_LTIME` are plain additions, so
a moment of a day can leave the day, while their TIME_OF_DAY
counterparts wrap at midnight.

Solution: Both reduce their result modulo one day of nanoseconds, the
way `ADD_TOD_TIME` and `SUB_TOD_TIME` do in milliseconds.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Problem: The builtins only accept numbers, so `ADD(dt, t)` needs the
standard library's generic overload and `ADD(dt, t, t)` is rejected
with a wrong argument count.

Solution: The builtins accept any type and validate every left-folded
pair by the date and time table; a call with a date or time argument
expands to the chain of operators, which the resolver replaces with
the standard library calls, e.g. `ADD(dt, t1, t2)` becomes
`ADD_DT_TIME(ADD_DT_TIME(dt, t1), t2)`. An argument no arithmetic is
defined for reports E156.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Problem: The standard library carries a generic overload of ADD and
MUL and about sixty `ADD__X__Y`-style functions whose only purpose is
to give the compiler's generic resolution a symbol to link against.

Solution: The builtins carry out date and time arithmetic through the
IEC-named functions now, so the overload, the nine ST shims, and the
Rust alias exports go. Object files compiled against those symbols
must be recompiled.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@ghaith
ghaith changed the base branch from master to feat/prg-4854-operators September 29, 2026 05:12
@github-actions

Copy link
Copy Markdown

7 findings in 13m 53s for $0.69 between ce96349 (master) and 9148d08 (feat/prg-4854):

  • P1 book/user/reference/built-in-functions.md:42: This reference still says ADD/MUL accept only ANY_NUM, that SUB/DIV return the biggest type, and the page introduction says built-ins need no library. Date/time calls now are valid according to the date/time table but lower to standard-library functions and use the table's result type, so this page can make users reject valid calls or link a build that fails; update the arithmetic rows and library caveat.
  • P2 book/technical/internals/08-annotated-ast.md:352: This says every arithmetic operator on a date/time operand becomes a standard-library call, but TIME/LTIME plus or minus a bare integer is deliberately left as a plain binary operation (with E157), and undefined combinations are also left written. Qualify the sentence to the defined combinations that have a declared operator function.
  • P2 book/technical/participants/08-generic.md:82: The claim that built-in ADD is generated inline by codegen is no longer true for date/time arguments: the resolver expands the call to date/time operators and those operators call standard-library functions. Qualify this exception here so the technical generic-lowering description agrees with the new behavior.
  • P2 book/technical/pipeline/04-validation.md:102: The new validation summary says the arithmetic builtins report E156 for any argument that is not a number, which would include valid TIME, DATE, and DATE_AND_TIME arguments. They accept those when the left-folded date/time table has a defined row; restrict the sentence to unsupported nonnumeric arguments/combinations.
  • P2 book/user/reference/standard-library.md:25: The new bullet overstates that arithmetic on date/time types calls date_time_numeric_functions.st: TIME/LTIME plus or minus a bare integer remains a plain integer operation and only warns E157, so it does not require one of those functions. Narrow this to the table-defined operations (and retain the bare-integer exception described in time.md).
  • P2 book/user/reference/standard-library.md:32: The changed Arithmetic-family row now omits ADD and MUL, although libs/stdlib/iec61131-st/arithmetic_functions.st still declares those library overloads (and the page says the library declares them again). Readers using the family table will be told that the file does not contain these functions.
  • P2 compiler/plc_diagnostics/src/diagnostics/error_codes/E156.md:20: The E156 page says any duration 'combined with a bare integer' gets E157, but duration multiplication/division by an integer is a defined operation and does not emit E157. Limit this sentence to addition and subtraction, as the implementation and E157 page do.

Base automatically changed from feat/prg-4854-operators to master September 30, 2026 05:08
volsa
volsa previously approved these changes Sep 30, 2026
Problem: Codegen stores the result of an expression the resolver
replaced (the builtins ADD, SUB, MUL, DIV, date and time operators,
string comparisons) without converting it to the type of the target.
`x := MUL(t, 1.5)` with an LREAL `x` writes the TIME bits into the
double and reads 0.0, and `(tod + T#20s)` in parentheses skips the
replacement and does not wrap at midnight.

Solution: Generate the replacement where every expression value is
generated, so the replaced value takes the same conversion to its hint
as any other value, also inside parentheses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
volsa and others added 2 commits September 30, 2026 13:15
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: The built-in reference still limits ADD and MUL to ANY_NUM
and says no built-in needs a library, the standard library family
table drops the ADD and MUL overloads that still ship, and several
technical pages overstate which date and time calls become library
calls or report E156 and E157.

Solution: Describe the date and time forms of the arithmetic builtins
and their library dependency, restore the overloads in the family
table, and narrow the affected statements to what the compiler does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@volsa
volsa marked this pull request as ready for review September 30, 2026 11:24
@github-actions

Copy link
Copy Markdown

3 findings in 3m 20s for $0.56 between 9413775 (master) and 3a9ca89 (feat/prg-4854):

  • P1 src/builtins.rs:1064: Date/time variadic folding causes exponential resolver work: each generated binary node revisits its full left prefix initially, then twice more through visit_date_time_arithmetic and the generated call's argument annotation, with no memoization in visit_statement. With ADD_TIME declared, a valid ADD containing 20 TIME arguments therefore requires roughly 3^19 prefix visits and retains fresh generated-call annotations, making a short call consume extreme CPU and memory (static trace, untested).
  • P2 book/user/language/time.md:80: The new builtin-call rules omit the bare-integer exception: ADD(cycle, 5) and SUB(cycle, 5) are accepted with E157 just like the corresponding operators, but this paragraph says builtins take only the table combinations, whose rows exclude a bare integer. Clarify that the duration/bare-integer exception and warning also apply to these calls.
  • P2 src/builtins.rs:822: The validation fold does not use the numeric promotions applied by the resolver, so it can check for the wrong standard function. For MUL(u, s, t) with u: UINT, s: SINT, and t: TIME, validation keeps the numeric prefix as unsigned UINT and requires MUL_TIME__ULINT, whereas the replacement promotes that prefix to signed DINT and calls MUL_TIME__LINT. Declaring only the actually required MUL_TIME__LINT therefore rejects this valid expression with E073 (static trace, untested).

volsa and others added 2 commits October 1, 2026 09:43
Problem: An expression passed to a by-reference input, such as
`read(ADD(n, f()))` or `read(n + f())` for a `VAR_INPUT {ref}`, was
generated twice: once to find out that it is not an lvalue, and again to
store it in a temporary. A side effect in the argument ran twice.

Solution: Store the value that was already generated, converted to the
type hint of the argument like any other generated expression.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fallback for an argument no arithmetic is defined for visited the
call arguments a second time, although the resolver visits them before
it calls the builtin annotation. The biggest argument type is now only
computed for a chain of numbers, the only case that uses it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
volsa and others added 3 commits October 1, 2026 10:00
Problem: `t / 0` is rejected with E123, but `DIV(t, 0)` compiled without
a diagnostic. For a TIME the call then reaches the standard library,
which panics on a zero divisor at runtime.

Solution: The DIV builtin applies the zero divisor check of the `/`
operator to its argument IN2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: For reordered named arguments, such as
`SUB(IN2 := dt, IN1 := t)`, the E156 diagnostic underlined only `t`,
because the span was built in parameter order and its end came before
its start in the source.

Solution: Span the arguments of each folded step from the first to the
last one as they are written in the source.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown

3 findings in 3m 46s for $0.60 between f7d42e8 (master) and efacd33 (feat/prg-4854):

  • P2 book/user/language/functions.md:64: The compiler now guarantees that an expression passed to a by-reference input is evaluated once, including when it is lowered through a replacement; the new tests cover side-effecting arguments. Document this call behavior here so readers know expressions are not reevaluated when materialized for the parameter.
  • P2 book/user/reference/built-in-functions.md:47: The standard-library requirement is too broad: calls such as ADD(cycle, 5) are lowered to plain integer arithmetic and need no iec61131std, even though they have a date/time argument. Qualify this to combinations implemented by library functions, and likewise narrow the claims in user/language/generics.md and user/language/time.md.
  • P2 src/validation/statement.rs:2944: Typed zero divisors bypass E123: DIV(x, DINT#0) reaches this helper as ReferenceAccess::Cast, but get_node_peeled() removes only parentheses and the cast's Value annotation has no constant-variable path. The helper therefore returns false, unlike for DIV(x, 0), allowing integer division by a known zero to reach LLVM (or panic in the duration library). This is a static code trace, not an executed test.

volsa and others added 2 commits October 1, 2026 10:26
The divisor check wrote the parameter names of DIV once more. The DIV
validation now names them once and hands them to both of its checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown

0 findings in 3m 12s for $0.56 between f7d42e8 (master) and 587631f (feat/prg-4854)

@volsa
volsa enabled auto-merge October 1, 2026 09:20
@volsa
volsa added this pull request to the merge queue Oct 1, 2026
Merged via the queue into master with commit f8e02a7 Oct 1, 2026
23 checks passed
@volsa
volsa deleted the feat/prg-4854 branch October 1, 2026 09:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants