Expressions
PNGine supports compile-time arithmetic expressions for calculating sizes, counts, and other numeric values. Expressions are S-expressions written directly in a numeric slot, using the same prefix syntax as the rest of SJON, the S-expression language PNGine documents are written in.
Expression Syntax
Section titled “Expression Syntax”An expression is a form whose head is an arithmetic function, with numeric
arguments (literals, (define …) symbols, or nested expressions). It evaluates
at compile time and must result in a number.
(define :name NUM_PARTICLES :value 4096)
(buffer :name particles :size (* NUM_PARTICLES 16) :usage [storage])
(compute-pass :name step :pipeline simPipeline (dispatch :workgroups [(ceil (/ NUM_PARTICLES 64))]))Integer slots also accept hex literals, read as the number they spell:
:write-mask 0xF, :stencil-read-mask 0xFF, :mask 0xFFFFFFFF.
Where a bare name works
Section titled “Where a bare name works”Every numeric slot accepts a bare (define …) name as well as a literal or an
expression: byte sizes and offsets, binding and group indices, pool sizes,
device limits, a sampler’s :max-anisotropy, vector elements, and the draw and
dispatch counts.
(define :name SIZE :value 256)(define :name NUM_PARTICLES :value 2048)
(buffer :name uniformBuf :size SIZE :usage [uniform])(draw :vertex-count 6 :instance-count NUM_PARTICLES)The name is resolved against the document’s (define …) forms, so a misspelt
constant is a not_cross_ref diagnostic on that slot, and a literal outside the
slot’s range still reports the range it missed.
Two slots are the exception, because their symbol spelling is already taken
by a member set: :write-mask (member all, a bare name is not_member) and
a (wasm-call :args […]) element (the runtime built-ins, a bare name is
union_no_branch_matched). In both, a constant is written as arithmetic,
(* MASK 1).
Functions
Section titled “Functions”All operations are written in prefix form. The heads are SJON’s core expression table, which PNGine evaluates as is; the ones worth knowing:
| Function | Description | Example | Result |
|---|---|---|---|
+ |
Addition | (+ 1 2) |
3 |
- |
Subtraction | (- 5 3) |
2 |
* |
Multiplication | (* 4 5) |
20 |
/ |
Division | (/ 10 4) |
2.5 |
ceil |
Round up | (ceil 2.3) |
3 |
floor |
Round down | (floor 2.7) |
2 |
round |
Round to nearest | (round 2.5) |
3 |
fract |
Fractional part | (fract 2.7) |
0.7 |
mod |
Remainder | (mod 7 3) |
1 |
pow |
Power | (pow 2 10) |
1024 |
min, max |
Smallest / largest of the arguments | (max 1 2 3) |
3 |
clamp |
(clamp x lo hi) |
(clamp 100 0 16) |
16 |
abs, sign, sqrt |
Magnitude, sign, square root | (sqrt 16) |
4 |
sin, cos, tan, atan2, radians, degrees, (pi), (tau) |
Trigonometry | (* 2 (pi)) |
6.283… |
<, >, <=, >=, =, !=, and, or, not, if |
Comparison and choice | (if (> N 1000) 64 16) |
64 or 16 |
+, -, * and / accept any number of arguments ((* NUM 4 4) multiplies
all three; (* 4) is 4 and (- 5) is -5), except that / needs at least two.
The rounding and trigonometric functions take one; mod, pow and atan2
take two; clamp takes three. A head outside the table is an unknown_form
diagnostic and a wrong argument count an arity_mismatch. What a slot then
does with the result is the slot’s rule: a byte count must come out a whole
non-negative number, so (/ 100 3) is refused there and (floor (/ 100 3))
is 33.
Nesting
Section titled “Nesting”Expressions nest by composition; no operator precedence or grouping parentheses are needed, since the structure is explicit:
(ceil (/ NUM_PARTICLES 64)) ; ceil(NUM_PARTICLES / 64)(* (+ 1 2) 3) ; (1 + 2) * 3 = 9(/ 10 (+ 2 3)) ; 10 / (2 + 3) = 2Using Defines
Section titled “Using Defines”Defines can be used in expressions:
(define :name NUM_PARTICLES :value 2048)(define :name PARTICLE_SIZE :value 32)(define :name WORKGROUP_SIZE :value 64)
(buffer :name particles :size (* NUM_PARTICLES PARTICLE_SIZE) :usage [storage])
(compute-pass :name simulate :pipeline simPipeline (dispatch :workgroups [(ceil (/ NUM_PARTICLES WORKGROUP_SIZE))]))Common Patterns
Section titled “Common Patterns”Buffer Size Calculation
Section titled “Buffer Size Calculation”(define :name VERTEX_COUNT :value 1000)(define :name FLOATS_PER_VERTEX :value 8)(define :name BYTES_PER_FLOAT :value 4)
(buffer :name vertices :size (* VERTEX_COUNT FLOATS_PER_VERTEX BYTES_PER_FLOAT) :usage [vertex])Compute Dispatch
Section titled “Compute Dispatch”(define :name NUM_ITEMS :value 4096)(define :name WORKGROUP_SIZE :value 256)
(compute-pass :name process :pipeline processPipeline (dispatch :workgroups [(ceil (/ NUM_ITEMS WORKGROUP_SIZE))]))Array Stride and Offsets
Section titled “Array Stride and Offsets”(shader-module :name code :code """ @vertex fn vs(@location(0) pos: vec3f, @location(1) nrm: vec3f, @location(2) uv: vec2f) -> @builtin(position) vec4f { return vec4f(pos + nrm * 0.0 + vec3f(uv, 0.0) * 0.0, 1.0); }
@fragment fn fs() -> @location(0) vec4f { return vec4f(1.0, 1.0, 1.0, 1.0); }""")
(render-pipeline :name mesh :layout auto (vertex :module code :entry vs (vertex-buffer :array-stride (* 4 8) ; 8 floats = 32 bytes (attribute :shader-location 0 :offset 0 :format float32x3) (attribute :shader-location 1 :offset (* 4 3) :format float32x3) (attribute :shader-location 2 :offset (* 4 6) :format float32x2))) (fragment :module code :entry fs (target :format preferred-canvas-format)))Evaluation Notes
Section titled “Evaluation Notes”- All expression results are double-precision floats. An integer slot (like
:size) does not truncate: a fractional result is refused on the slot,:sizeevaluates to 33.333333333333336, and this slot takes an integer, and the message says what to wrap it in. A negative result in a non-negative slot is refused the same way. - Division by zero does not evaluate to a number, so the slot rejects it:
:sizedoes not evaluate to a number: it divides by zero. A constant name inside the expression that no(define …)declares is the same refusal with the name in it,:sizedoes not evaluate to a number:NUMMis not a (define …) constant (only a bare name is resolved by the validator; inside an expression it is the evaluator that looks it up). - A wrong argument count is an
arity_mismatchand an unknown function anunknown_form, both on the expression.
:size (floor (/ 100 3)) ; 33 bytes; a bare (/ 100 3) is refused hereComparison with WGSL
Section titled “Comparison with WGSL”PNGine expressions are not WGSL expressions:
| Feature | PNGine | WGSL |
|---|---|---|
| Evaluation | Compile time | Shader compile time |
| Form | S-expression (* a b) |
Infix a * b |
| Functions | SJON’s core table (arithmetic, rounding, min/max/clamp, trigonometry, comparisons) |
Full stdlib |
| Use | Resource creation | Shader logic |
Use PNGine expressions for resource configuration, WGSL for shader logic.
Related
Section titled “Related”(define …)- Define constants- References - Reference syntax
(data …)- Data and shape generators- SJON language docs - Expressions are a SJON feature, not a PNGine one