Home

Arcadia Visuals Programming Language

Specification, version 1

Arcadia VPL is a public, typed, three-address, virtual-register language for visual processing. Authors and tools describe typed operations and explicit control-flow graphs. The canonical in-memory representation is Arcadia.VPL.IR.*. A future higher-level language belongs to a separate module and produces canonical VPL.

The initial scope is a rendering shader/material-editor compiler target: vertex/fragment interfaces, values, shader operations, sampling, and control flow. A material graph tool can produce these operations directly. The comparison with tools such as Unreal's material editor is a scope guide, not a requirement to implement their entire node catalog or a general-purpose GPU, compute, memory, scheduling, or exception language.

The words shall and must express requirements. This specification defines the canonical object model, verifier contract, and implemented version-1 textual format. The grammar and public reading/writing/publication APIs are specified in sections 10 and 11.

1. Scope and status

VPL's canonical types, values, instructions, terminators, and module containers are implemented. The initial IR.Verifier checks directly constructed modules, including operand types, block ownership, root and derived storage bindings, register definite assignment, stage entries/linkage, output definedness, and acyclic helper effects. Text reading/writing, immutable publication, and a GLSL 420 core backend are implemented. The engine material/output path consumes published canonical modules and artifacts.

Version 1 fixes the language scope and text grammar described here. The consistency review covers the reader, canonical verifier, publication boundary, GLSL backend, and executable examples. Target restrictions remain part of conformance; successful canonical verification alone does not promise acceptance by every graphics driver.

The module contains only the canonical representation and compiler services. The former expression AST, symbol model, builders, and AST phases are removed; a future higher-level language belongs to a separate module. This specification describes simple operations and branches, not nested source expressions or structured if/while/for syntax.

2. Lexical translation

Canonical textual VPL shall select reusable productions and translation rules from Common Lexical Translations. The lexical input alphabet is the set of Unicode code points. Shared rules are incorporated by reference; VPL-specific additions or refinements shall be stated explicitly.

name                : Lexical.Name
numberLiteral       : Lexical.Number
stringLiteral       : Lexical.String
booleanLiteral      : Lexical.Boolean
voidLiteral         : Lexical.Void
whitespace          : Lexical.Whitespace
endOfLine           : Lexical.EndOfLine
lineContinuation    : Lexical.LineContinuation
singleLineComment   : Lexical.SingleLineComment
multiLineComment    : Lexical.MultiLineComment
period              : Lexical.Period
leftParenthesis     : Lexical.LeftParenthesis
rightParenthesis    : Lexical.RightParenthesis
leftCurlyBracket    : Lexical.LeftCurlyBracket
rightCurlyBracket   : Lexical.RightCurlyBracket
leftSquareBracket   : Lexical.LeftSquareBracket
rightSquareBracket  : Lexical.RightSquareBracket
colon               : Lexical.Colon
comma               : Lexical.Comma
startOfInput        : Lexical.StartOfInput
endOfInput          : Lexical.EndOfInput

VPL shall define its own lexical start symbol and word alternatives from selected common and VPL-specific productions. The common specification supplies no universal word production. A production's availability does not imply that it is permitted in every operand position, or that VPL has a corresponding value type.

The selected word alternatives, register/label spelling, boundaries, and disambiguation are defined in section 10. The aliases above name available common productions, not a promise that every punctuation category is a version-1 word. VPL and PDL are independent languages: PDL's syntax and lexical refinements are not inherited. Possible grammar convergence is deferred.

3. Modules, stages, functions, and blocks

ObjectContentsRequirements
Moduleuniforms, vertex stage, fragment stageBoth stage objects shall have their designated stage kinds.
Stagekind, ordered interface list, functions, entryFunctionNames are unique within their collections. The selected entry is a same-stage parameterless void function.
Functionname, return type, ordered parameters, ordered blocksThe function shall have at least one block. Its first block is the entry block.
Blockordered instructions, one terminatorA block belongs to exactly one function and appears once in that function's list.

Branches target block objects in the same function. There is no implicit fall-through. Instructions execute in list order, followed by the block's terminator. NULL terminators may be used while building a graph, but are invalid in a verified function.

Function parameters are reference descriptors in ordered parameter slots, supplied by value at invocation. Calls shall refer to functions in the calling stage. Each stage explicitly selects one function in its own list as entry; neither a main name nor list order selects it. Ordinary helpers may have parameters and non-void returns. Function/block objects shall not be shared between stages. Stage_create creates an unfinished stage with NULL entry, and Stage_createWithEntryFunction can supply it at construction; verification requires it.

Record display names, record field names, uniform names, interface names, and function names shall be nonempty strings. They are compared by their exact decoded contents, without case folding or Unicode normalization. Record display names need not be globally unique; their nominal identity follows section 4. Uniform names are unique module-wide, interface names across both directions within a stage, and function names within a stage. Interface and function name collections are separate. Field names are unique within their record.

3.1 Interface linkage

Every ordinary fragment input, whether used or not, requires exactly one ordinary vertex output. An explicit input location matches the same explicit output location, regardless of names. If both locations are -1, match by exact case-sensitive name. There is no mixed-mode inference or name fallback from an explicit location. Types must be equal, including array lengths and nominal record identity. Non-None Color/TexCoord tags must agree when both sides specify one. None leaves that tag unspecified, not the type. Unconsumed vertex outputs are allowed; vertex inputs and fragment outputs are not linkage candidates.

Position is a floating vec4 vertex output or fragment input with location -1. It is not a varying match: fragment Position is supplied by rasterization. FragmentColor is a floating vec4 fragment output. Required declarations and definedness are specified in section 6.3.

3.2 Invocation and execution model

A module describes vertex and fragment processing, not a general-purpose program entry. The graphics execution environment invokes the selected vertex entry for each processed vertex and the fragment entry for each processed fragment, supplying the stage inputs, uniform values, textures, sampler state, and pipeline state. Vertex outputs participate in rasterization and interface interpolation; they are not direct function-call arguments to a particular fragment invocation. The target profile defines those graphics operations.

Each function call has private parameter values and local registers. Arguments are read from caller operands and copied by value into ordered parameters, including records/arrays. Registers begin unassigned at every function invocation; register IDs are local to that function. A call begins at its first block. Its return supplies a value, if any, to the caller's destination register and resumes the instruction after the call. Parameter or register writes do not mutate the caller's values. Stage outputs are shared across calls within the same shader invocation, so helper writes are visible to callers; they are not shared registers between unrelated vertex/fragment invocations.

An instruction reads its simple input values and any storage selectors before writing its result or target. Register assignment and aggregate copying use values, not references to another register's mutable contents. A block executes instructions in order and then exactly its terminator; a branch changes the current block without resetting the function frame. Return leaves that frame, while discard abandons the whole fragment invocation and all of its active call frames. Infinite execution is not rejected merely for lacking a return.

Lexical/syntactic errors and violated verifier requirements are invalid programs and require rejection. Explicitly undefined dynamic numeric/index/resource cases are execution conditions for which the language does not require runtime checking or a portable result; they do not relax structural/type validation. Target-dependent precision and resource bounds must be distinguished from invalid program structure. A valid canonical module may still be rejected by a backend capability or hardware-limit check.

4. Types

TypeShape
VoidTypeNo value; permitted as a function return type.
ScalarTypeBoolean, Integer, or Floating element kind.
VectorTypeOne scalar element kind and 2, 3, or 4 components.
MatrixTypeFloating elements, 2 to 4 rows, and 2 to 4 columns.
SamplerTypeThe currently supported kind is Sampler2D.
RecordTypeA name and ordered, uniquely named fields, each with a value type.
ArrayTypeA value element type and a positive fixed element count.

Void shall not be a register, parameter, uniform, interface, record-field, or array-element value type. Recursive value types are invalid. Empty records are representable. Unsized arrays are not supported by the initial verifier, including an ArrayUniform with count zero.

Scalar kinds, vector element kinds and sizes, matrix dimensions, sampler kinds, and array element types and lengths participate in structural type equality. Records have nominal identity defined by their record declaration in a module instance. Every reference to that declaration denotes the same record type; a separate declaration denotes a distinct type, even if its quoted display name and fields match. A builder creates a record declaration by creating one RecordType node and reuses it for that declaration's references. Object identity is the C implementation of the rule, not the language's definition.

Parsing or materializing a module creates a new module instance. Serialization preserves which uses share a record declaration and which declarations are distinct; it does not preserve C pointers or establish cross-module type equality. V1 has no cross-module type linker. Structural wrappers such as arrays are equal only when their element types are equal under these rules.

The current scalar constant representation uses Boolean values, signed 32-bit integer values, and IEEE binary32 floating values. Literal constants must be finite; positive and negative zero are distinct stored literal bit patterns preserved by text round trips. Shader arithmetic is governed by the native execution rules below, not the CPU conversion contract. Additional numeric kinds and target resource limits are extensions.

4.1 GLSL-friendly v1 numeric contract

The v1 execution baseline is deliberately aligned with GLSL 4.20 core, sections 4.1.3, 4.7.1, 5.4, and 5.9. A conforming backend may use its native corresponding operators and constructors; VPL does not require software IEEE emulation, floating exception handling, programmable shader rounding, saturating integer arithmetic, or a stronger fast-math model. A different target must state any capability restriction rather than assume identical precision guarantees.

Integers are signed 32-bit two's-complement values. Add, Subtract, and Multiply, including integer-vector component operations, keep the low 32 bits of the mathematical result and interpret them as signed two's complement. Overflow wraps; it does not saturate or raise an exception. Division truncates toward zero. Integer division by zero and minimum-integer divided by -1 have undefined execution results and must not raise a VPL arithmetic exception; statically explicit constant zero divisors and constant minimum/-1 divisions are rejected. The verifier checks types and those explicit invalid divisions, not general range proofs.

CPU literal reading is a separate guarantee: decimal text rounds nearest-even to finite binary32, preserving signed zero and representable subnormals, and serialization preserves those bits. That does not force a shader to retain a subnormal or a zero sign during execution.

Shader float storage and ordinary finite operations use GLSL 4.20's single-precision range and precision rules. Shader rounding is not programmable; positive/negative zero may be interchanged, subnormal inputs/results may flush to zero, and contraction such as multiply/add may follow GLSL's permitted evaluation. Register boundaries alone are not precise-rounding barriers. Floating arithmetic does not raise VPL exceptions. NaN generation and propagation, signaling NaNs, and portable NaN-dependent branch outcomes are not additional VPL promises. Nonzero float division by zero follows GLSL's infinity rules, including its zero-sign allowance; indeterminate operations retain the GLSL allowances. Compare ordinary finite numbers normally, with equal positive/negative zeros; no stronger cross-driver special-value reproducibility is required. Section 12 specifies the implemented target contract.

Runtime int-to-float construction converts the signed mathematical value, not its bit pattern. Exactly representable integers retain their value. Other integers may lose precision under GLSL's correctly-rounded conversion/unspecified shader-rounding rules. This applies to explicit scalar conversions and integer scalar components in floating vector/matrix construction. It does not introduce implicit arithmetic, call, store, or return conversions.

5. Values and mutable registers

Value formFieldsMeaning
RegisterValueindex, typeA function-local, statically typed, mutable virtual register.
ConstantValuetype, boxed literalA Boolean/Integer32/Real32 scalar literal matching its explicit type.
ReferenceValuestorageKind, slot, typeA root storage descriptor, also the base class of derived reference descriptors.
MemberReferenceValuebase reference, fieldName, inherited typeAn access path to a record field, used by loads and stores.
IndexReferenceValuebase reference, index value, inherited typeAn access path to an array element, used by loads and stores.

Computation operands shall be registers or scalar constants. A storage reference is not implicitly loaded when used in an arithmetic operation, constructor, call, or return; a LoadInstruction is required. Aggregate values are loaded, copied, or constructed rather than boxed as scalar constants.

5.1 Declarations and assignment

All registers shall be declared exactly once in a prefix of DeclareLocalInstruction objects in the entry block, before the first executable operation. These declarations are function-wide metadata and do not initialize values. Register indices need not be dense.

A register keeps its declared type but may be assigned repeatedly, including within loops and on different branch paths. Every descriptor using the same index in a function shall agree with that declared type. Copy, load, arithmetic, construction, extraction, non-void calls, and whole-local stores can assign registers. An instruction reads its inputs before assigning its destination, so r0 = add r0, r1 requires both old input values.

5.2 Definite assignment

Every reachable register read shall be preceded by assignment on every incoming control-flow path. The invocation begins with no assigned local registers. At a join, the incoming assigned set is the intersection of predecessor outgoing sets; a write adds its destination to that set. Loops are analyzed to a fixed point. Assignment only on a back-edge cannot justify a read on the first iteration.

The verifier considers both successors of a conditional branch, even for a constant condition. It validates types and graph structure in unreachable blocks too, while checking register availability only in executable prefixes reachable from entry. Non-returning calls cut continuation as specified in section 6.3. There are no phi operations or block parameters in this mutable-register model.

6. Storage references

NamespaceSlot meaningPermissions
StorageKind_LocalA declared register's numeric index.Read/write; a whole-local store assigns that register, and a load requires definite assignment.
StorageKind_ParameterPosition in the current function's parameter list.Read/write invocation-local parameter storage; writes do not alter the caller's argument storage.
StorageKind_UniformPosition in the module's uniform list.Read-only.
StorageKind_StageIOPosition in the current stage's interface list, including inputs and outputs.Inputs are read-only; outputs are writable.

A reference's type shall equal the stored value type of its bound slot. Parameter descriptors shall use the Parameter namespace and their list positions. The legacy two-argument reference constructor defaults to the Local namespace; canonical producers can use ReferenceValue_createWithStorage to specify a namespace explicitly.

Uniform names shall be unique. Scalar uniforms carry a value type, record uniforms carry a nominal record type, and array uniforms carry an element type and positive count. A whole array or record can be loaded into a register. MemberInstruction and IndexInstruction extract values; they do not create references to sub-storage. Loads and stores through sub-storage instead use the derived reference descriptors below.

Interface directions, semantics, and locations are checked within each stage. Location -1 denotes an unspecified location. Names shall be unique, and locations and non-None semantics shall not repeat within one direction. The semantic tags are None, Position, Color, TexCoord, and FragmentColor. Position is a floating vec4 vertex output or fragment input; FragmentColor is a floating vec4 fragment output. Output initialization and interprocedural storage effects use section 6.3.

6.1 Derived storage references

DescriptorBase and selectorStored value type
MemberReferenceValueA reference to record storage and an existing field name.The selected field's declared type.
IndexReferenceValueA reference to array storage and an integer register or scalar constant.The array element type.

These descriptors derive from ReferenceValue, with kind Member or Index and inherited slot zero. Their inherited type shall match the selected stored value type. A base reference may itself be derived, so paths can select an array field, an element of that array, and a field of that element, or index multiple nested arrays. Paths shall be acyclic, and object class, kind, selector, and type shall agree. Parameter declarations still require root Parameter descriptors, not derived references with a forged Parameter kind.

A path inherits read/write permissions from its root. A field or element of a uniform or stage input remains read-only; the derived descriptor cannot grant writable access. A member selector requires record storage, not a vector swizzle or a computed record value. An index selector requires array storage and an integer operand. Literal indices must be nonnegative and smaller than the containing array's count at each selection level.

Reference descriptors describe access paths, not frozen pointer values. A mutable index register is read when a load or store uses the path, rather than when the descriptor is constructed. All index reads and storage-address selection occur before a load assigns its result or a store writes its value. Reusing a path after changing an index register can select a different element. Dynamic index registers must be definitely assigned at each use; out-of-bounds dynamic access has undefined execution behavior, with no implicit clamp or wrap.

A load through a derived reference produces the selected stored value. A store through it requires an exactly matching value and updates that sub-storage, preserving the other fields and elements. The descriptors are storage operands, not computation values or call arguments. The following notation is descriptive pseudo-code; canonical text syntax is defined in section 10:

root  = LocalReference(containerRegister)
cells = MemberReference(root, "cells", arrayType)
cell  = IndexReference(cells, indexRegister, recordType)
field = MemberReference(cell, "value", int)
store field, replacementValue
result : int = load field

6.2 Local aggregate availability

Local availability is tracked for whole registers, not independently for every field or element. A local aggregate must be wholly assigned before any derived load or partial store. Construct, copy, whole-slot load, or whole-local store can establish that assignment.

A partial store shall not establish whole-register assignment: writing one field does not initialize its siblings, even if the producer later writes more fields. This conservative rule avoids implicit partial-initialization tracking. Parameter values are supplied in full at invocation; their mutable sub-storage follows the normal parameter permissions. Stage output initialization follows the finer-grained rules in section 6.3.

6.3 Output declarations and definedness

A vertex stage shall declare its Position output, and a fragment stage its primary FragmentColor output, even if it always discards. Every declared output must be fully assigned on each normal entry-function return. No output is initially assigned. A read requires prior definite assignment on every incoming path. Termination itself is not proved.

Output records and fixed arrays are split recursively into observable leaves; scalar, vector, matrix, and sampler values are each one leaf. Empty records have no leaves. Whole stores establish all selected leaves; member and constant-index stores establish their selected subranges without requiring assigned siblings. Dynamic-index reads require every potentially selected leaf, and dynamic-index stores establish no new definite leaf, even for one-element arrays. These output rules are distinct from the whole-local rule in 6.2.

Each helper has output read-before-write prerequisites, guaranteed assignments on normal returns, and a normal-return possibility. A caller must satisfy read prerequisites and receives those guarantees on normal continuation. Discarded paths do not contribute to return guarantees; reads before discard remain checked. A helper with no normal return cuts the caller's remaining instructions and successors for availability checks. All syntactic instructions still receive structural/type validation. Direct and indirect recursion, even in unused functions or unreachable blocks, are rejected. Summaries use acyclic call-graph order and loop must fixed points. Both conditional successors are considered, even for constant conditions.

7. Instructions

Each result-producing operation has an explicit destination register. Three-address does not mean every operation has exactly three operands: calls and constructors have ordered operand lists, while loads, stores, and control transfers have their own shapes. Each operation consumes simple operands; nested source-language expression trees are outside canonical VPL.

InstructionFieldsSemantics
DeclareLocalInstructionregisterValue, typeDeclare a register of that type; no initialization or runtime value computation.
CopyInstructionresult, sourceAssign a value of exactly the result's type. Copying a reference is not an implicit load.
LoadInstructionresult, source referenceRead root or derived storage into a register of the same type.
StoreInstructiontarget reference, valueWrite an exactly matching value to writable root or derived storage. No result register.
BinaryInstructionresult, operatorKind, lhs, rhsCompute typed arithmetic or a scalar comparison.
CallInstructionresult?, calleeFunction?, builtin, argumentsCall a same-stage function or a supported built-in with signature-matching values.
ConstructInstructionresult, type, argumentsExplicitly construct a value or perform a supported scalar conversion.
SwizzleInstructionresult, source, patternExtract 1 to 4 xyzw components from a vector, in pattern order.
MemberInstructionresult, source, fieldNameExtract an existing record field with its declared type.
IndexInstructionresult, source, indexExtract an array element using an integer scalar index. Literal indices must be in bounds.

7.1 Arithmetic and comparisons

Add, Subtract, Multiply, and Divide require numeric operands. Matching scalars/vectors produce the same type; vector operations are component-wise. Matching matrices support addition, subtraction, and component-wise division. Matrix multiplication requires matching inner dimensions and produces the outer dimensions; matrix-times-vector produces a vector with the matrix's row count.

A floating vector or matrix may be multiplied or divided by a floating scalar, retaining its type. Floating scalar multiplication in the opposite order is also supported. Vector-times-matrix and other mixed arithmetic types are currently rejected. Numeric-to-Boolean conversion is not implicit. Integer-to-float conversion shall be an explicit construction before arithmetic, assignment, calls, or returns requiring float.

Equal and NotEqual accept matching Boolean, integer, or floating scalars and produce bool. Less, LessEqual, Greater, and GreaterEqual require matching numeric scalars and produce bool. Composite comparisons are not defined. Integer/Boolean comparisons are exact; floating precision and special-value allowances follow section 4.1.

The complete v1 operand table below is normative. A and B denote floating matrices, V a floating vector, and S a floating scalar. Other operands have exactly matching types where stated. All combinations not listed are rejected; there is no implicit promotion.

OperandsOperationsResult
Matching int or float scalarsAdd, Subtract, Multiply, DivideThe matching scalar type
Matching integer or floating vectorsAdd, Subtract, Multiply, DivideThe matching vector, component-wise
Matching floating matrix shapesAdd, Subtract, DivideThe matching matrix, component-wise
A with c columns, B with c rowsMultiplyMatrix with A's rows and B's columns
A with c columns, floating V with c componentsMultiplyFloating vector with A's row count
Floating V or matrix, SMultiply, DivideThe aggregate type, component-wise scalar operation
S, floating V or matrixMultiplyThe aggregate type
Matching bool, int, or float scalarsEqual, NotEqualbool
Matching int or float scalarsLess, LessEqual, Greater, GreaterEqualbool

7.2 Construction and conversion

A scalar construction takes one operand of the same scalar type, or an integer operand for a floating result. This explicitly represents integer widening. Floating-to-integer and numeric-to-Boolean conversions are not currently supported.

A vector construction supplies matching scalar/vector components totaling exactly its dimension, or one matching scalar to fill every component. Vector operands contribute all their components in order; excess components are rejected rather than truncated. Floating vector/matrix construction also accepts integer scalar components as an explicit part of that construction, but does not convert integer-vector operands. Matrix construction takes floating scalar/vector components, with that same integer-scalar allowance, in column-major order, totaling exactly rows times columns; one scalar instead forms a diagonal matrix with zero off-diagonal entries. Matrix operands are not constructor components.

An array construction supplies exactly its element count, with each operand matching the element type. Record construction supplies one exactly matching operand per field, in field order. Empty record construction is permitted. Samplers cannot be constructed.

7.3 Calls

A function call shall select a non-NULL calleeFunction and BuiltinFunction_None. Its operand count and types shall match the ordered parameters. A non-void callee requires a matching destination register; a void callee requires NULL result. For a built-in call, the function reference shall be NULL and the selector shall be supported. The current built-in is Texture(sampler2D, vec2) -> vec4, with floating vectors.

7.3.1 Texture sampling

Texture performs the ordinary non-shadow, non-projective, two-dimensional floating sampling operation corresponding to texture(sampler2D, vec2) without explicit bias, LOD, gradients, or offset. The two coordinates are normalized texture coordinates, not integer texel indices. Coordinates outside [0,1] are handled by sampler addressing state; VPL does not implicitly clamp them or promise one wrapping mode.

The host binds a compatible complete 2D texture and supplies filtering, addressing, mipmap, base/max-level, LOD-clamp, border, swizzle, and applicable format/decode state. Sampling returns the floating vec4 defined by that texture and sampler state. Normalized formats convert their texels to floating values; floating texture values are not universally clamped to [0,1]. Integer and shadow-sampler operations are not part of v1. Incompatible resource/sampler combinations and invalid texture state have the underlying graphics API's undefined-result behavior, not a portable VPL fallback color.

Vertex-stage implicit base LOD is zero; actual level selection still obeys the bound texture/sampler state. Fragment-stage implicit LOD uses the target's ordinary derivative-based selection when needed. Implicit derivatives are undefined in non-uniform control flow; non-mipmapped sampling need not require them. VPL neither supplies invented gradients nor promises reconvergence across arbitrary branch/back-edge graphs. The GLSL dispatch lowering preserves explicit per-invocation control flow but is not a derivative-reconvergence guarantee.

The verifier checks the signature and storage permissions. It does not prove dynamic uniformity, derivative availability, texture completeness, or external resource compatibility. Those are execution preconditions, as described by GLSL 4.20 section 8.9 and the OpenGL 4.2 texture/sampler rules. This bounded shader/material-editor target scope does not require a general-purpose GPU execution or resource language.

7.4 Extraction

A swizzle's pattern shall contain only xyzw selectors valid for the source dimension. One selector produces a scalar of the source element kind; multiple selectors produce a vector of that kind and pattern length. Repeated components are allowed for extraction.

A member operation requires a record value and a declared field name. An index operation requires an array value and integer index. Both produce values of the selected element type, not storage addresses; use MemberReferenceValue/IndexReferenceValue for storage access. Dynamic out-of-bounds access has undefined execution behavior as in section 6.1.

8. Control flow

TerminatorFieldsEffect
BranchTerminatortargetTransfer to a block in the same function.
ConditionalBranchTerminatorcondition, trueTarget, falseTargetTest a bool value and transfer to one of the two same-function blocks.
ReturnTerminatorvalue?Finish the function. Void returns require NULL value; non-void returns require an exactly matching value.
DiscardTerminatorNoneAbandon the fragment invocation, with no successor and no returned value, regardless of function return type.

Discard is fragment-only and terminates a block. It is represented only by DiscardTerminator; the former instruction-kind value 9 is reserved and invalid.

Loops are explicit back-edges. Higher-level if/while/for/break/continue statements are expressed by graph construction in the producing tool or future external language frontend. The following version-1 function fragment shows mutable registers and a loop:

(registers (register r0 IntType) (register r1 BoolType))
(block entry
  (copy r0 (int 0))
  (branch test))
(block test
  (less r1 r0 (int 10))
  (branch_if r1 body exit))
(block body
  (add r0 r0 (int 1))
  (branch test))
(block exit (return r0))

The enclosing function in this sketch returns int. Removing the initial copy makes its first comparison invalid: assignment on the loop back-edge does not define r0 on first entry.

9. Verification

Arcadia_VPL_IR_Verifier_run(thread, verifier, module) checks a current module graph without the legacy AST or symbol phases. It reports the first invalid object through a VerificationDiagnostic containing a message and an optional subject object, and raises Arcadia_Status_SemanticalError. It releases scratch memory and restores the value stack on success or failure. The same verifier can be used for subsequent runs.

Object constructors do not establish all canonical invariants. Direct builders and parsed text shall use the same verifier. A successful check applies to that graph at that moment; mutations require re-verification. Immutable publication through VerifiedModule separates saved compiler input from mutable builders and fresh materializations (section 11).

Arcadia.VPL.Tests.Verifier covers branch joins, repeated writes, first-iteration loop availability, register/type consistency, storage permissions, calls including void calls, cyclic types, fragment discard, arrays of records, constructors, swizzles, texture sampling, nested storage paths, inherited permissions, cyclic references, partial-write initialization, selector availability, constructor rejection, reruns, and failure cleanup. Construction tests remain in Arcadia.VPL.Tests.IR. Canonical GLSL golden and native-driver tests check the four engine variants. The GLSL tests also compare each embedded built-in with its independently parsed checked-in source file. Diagnostic tests use canonical verification and the shared diagnostic context.

10. Canonical text format, version 1

A .vpl file is UTF-8 without a byte-order mark. It contains one module form followed by end of input. Parenthesized forms make declarations, scopes, instruction operands, and boundaries explicit. Line endings are trivia, not instruction terminators. This format is independent of PDL and contains no nested arithmetic expressions.

10.1 Lexical selection and refinements

VPL selects Common Lexical Translations names, decimal numbers, quoted strings, Boolean and void words, parentheses, spaces/tabs, end-of-line sequences, comments, line continuations, and start/end conditions. Names are ASCII alphabetic names with the common underscore and digit suffix rules; matching is case-sensitive. Names true, false, and void are reserved. Other form-head words are contextual. Register identifiers refine names to r followed by decimal digits; their numeric IDs are function-local.

Outside strings, spaces, tabs, common line endings, non-nesting solidus/asterisk comments, double-solidus line comments, and reverse-solidus immediately followed by a line ending are ignored. Comments and line continuations separate words; a continuation does not join name fragments. Parentheses and either quote delimiter terminate a preceding name or number, so id"display"Type contains three tokens without intervening spaces. Other punctuation is not a version-1 word alternative. Integer and real spelling overlap is resolved by operand/declaration context; a maximal number word must match the common decimal grammar in its entirety.

Both quote styles are accepted. Raw C0 controls and line endings are forbidden inside strings. VPL explicitly refines the common escape spelling: reverse-solidus, either quote, \b, \f, \n, \r, and \t decode to reverse-solidus, that quote, U+0008, U+000C, U+000A, U+000D, and U+0009 respectively. \uHHHH uses exactly four hexadecimal digits (0-9, a-f, A-F). Surrogates are rejected rather than paired; non-BMP scalar characters can be written directly in UTF-8. These explicit refinements select VPL's supported escape set and retain its four-digit Unicode spelling independently of other languages' profiles.

Signed integer operands and locations accept an optional sign and decimal digits, within signed 32-bit range. Sizes and counts use unsigned decimal digits within Arcadia.Size range; register IDs use the digits following r. Decimal float operands accept a sign, decimal point, and/or exponent under the common number spelling, then round directly to binary32 using nearest-even conversion in a private C numeric locale. Underflow to signed zero is allowed; overflow to infinity is rejected. Invalid UTF-8, overlong encodings, encoded surrogates, unclosed strings/comments, and malformed numbers are lexical errors.

Size/count and numeric register-ID storage limits are implementation resource bounds, separate from shader arithmetic int32. The current Arcadia reader uses unsigned Arcadia.Size: at most 2^32-1 on a 32-bit build or 2^64-1 on a 64-bit build, with zero forbidden for array lengths. Values beyond that bound are rejected with a range diagnostic. They are not rounded or truncated. This explicitly retains the implementation-defined bound rather than silently narrowing existing v1 inputs; implementations must disclose their bound. GLSL target and hardware resource limits are additional checks. For source portability between these builds, keep register IDs within unsigned 32 bits and GLSL array counts within positive signed 32 bits; this is not a guarantee that a GPU can allocate arrays of that size.

10.2 Grammar

In the following grammar, braces denote repetition and brackets optional syntax. Parentheses and words in quotes are literal. id, reg, string, size, signedInteger, and number denote the lexical categories/refinements above. Every count is exact unless repetition is shown.

module       : '(' 'module' version types uniforms vertex fragment ')'
version      : size /* its numeric value shall equal 1 */
types        : '(' 'types' {type} ')'
type         : '(' 'type' id scalarShape ')'
             | '(' 'type' id 'vector' elementKind size ')'
             | '(' 'type' id 'matrix' size size ')'
             | '(' 'type' id 'array' id size ')'
             | '(' 'type' id 'record' string {field} ')'
scalarShape  : 'void' | 'bool' | 'int' | 'float' | 'sampler2D'
elementKind  : 'bool' | 'int' | 'float'
field        : '(' 'field' string id ')'
uniforms     : '(' 'uniforms' {uniform} ')'
uniform      : '(' 'scalar' id string id ')'
             | '(' 'record' id string id ')'
             | '(' 'array' id string id size ')'
vertex       : '(' 'vertex' id interface functions ')'
fragment     : '(' 'fragment' id interface functions ')'
interface    : '(' 'interface' {io} ')'
io           : '(' direction id string id signedInteger semantic ')'
direction    : 'in' | 'out'
semantic     : 'none' | 'position' | 'color' | 'texcoord' | 'fragment_color'
functions    : '(' 'functions' {function} ')'
function     : '(' 'function' id string id parameters registers block {block} ')'
parameters   : '(' 'parameters' {parameter} ')'
parameter    : '(' 'parameter' id id ')'
registers    : '(' 'registers' {register} ')'
register     : '(' 'register' reg id ')'
block        : '(' 'block' id {instruction} terminator ')'

operand      : reg | '(' 'bool' boolean ')' | '(' 'int' signedInteger ')'
             | '(' 'float' number ')'
boolean      : 'true' | 'false'
reference    : '(' 'local' reg ')' | '(' 'parameter' id ')'
             | '(' 'uniform' id ')' | '(' 'stage' id ')'
             | '(' 'member_ref' reference string ')'
             | '(' 'index_ref' reference operand ')'

instruction  : '(' 'copy' reg operand ')'
             | '(' 'load' reg reference ')' | '(' 'store' reference operand ')'
             | '(' binary reg operand operand ')'
             | '(' 'construct' reg {operand} ')'
             | '(' 'call' result id {operand} ')'
             | '(' 'texture' reg operand operand ')'
             | '(' 'swizzle' reg operand string ')'
             | '(' 'member' reg operand string ')'
             | '(' 'index' reg operand operand ')'
binary       : 'add' | 'subtract' | 'multiply' | 'divide' | 'equal' | 'not_equal'
             | 'less' | 'less_equal' | 'greater' | 'greater_equal'
result       : reg | 'void'
terminator   : '(' 'branch' id ')' | '(' 'branch_if' operand id id ')'
             | '(' 'return' [operand] ')' | '(' 'discard' ')'

The module's second item is an unsigned integer value equal to 1; other versions are rejected. Matrix dimensions are rows then columns. Type IDs are module-wide; uniform IDs are module-wide; interface/function IDs are stage-local; parameter/register/block IDs are function-local. Display names and field names are quoted strings, distinct from binding IDs.

Type, same-stage function, and same-function block forward references are permitted. Types must be acyclic, including unused declarations. Duplicate identifiers in each scope are rejected. Register spellings with the same numeric ID (for example r0 and r00) duplicate one register. The registers form creates the declaration prefix in the first block.

References bind to explicitly declared slot types; field/element descriptors inherit the selected type. A construct uses its destination's declared type. This is binding of declared types, not source-expression type inference. Calls cannot name a function in another stage. A block has exactly one final terminator. Text reading always invokes canonical verification.

Version 1 limits syntactic form/atom nesting and named-type dependency depth to 128. The type bound counts dependency edges on the longest path, including unused types, regardless of declaration order or whether a referenced type has already been resolved. Canonical writing limits derived reference depth to 120 so emitted forms fit that reader bound. All counts, ranges, entry signatures, interfaces, operands, and output flows must also satisfy the semantic sections above.

10.3 Complete examples

This exact source is also read and verified by Arcadia.VPL.Tests.Text:

// Arcadia
// Copyright (C) 2024-2026 Michael Heilmann
//
// This program is free software: you can redistribute it and/or modify it under
// the terms of the GNU Affero General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option) any
// later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
// details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see https://www.gnu.org/licenses/.
//
// Canonical VPL text, version 1. This exact example is parsed by Arcadia.VPL.Tests.Text.
(module 1
  (types
    (type Void void)
    (type Color vector float 4))
  (uniforms)
  (vertex vertex_entry
    (interface
      (out position "position" Color -1 position))
    (functions
      (function vertex_entry "vertex_main" Void
        (parameters)
        (registers (register r0 Color))
        (block entry
          (construct r0 (float 1.0))
          (store (stage position) r0)
          (return)))))
  (fragment fragment_entry
    (interface
      (out color "color" Color 0 fragment_color))
    (functions
      (function fragment_entry "fragment_main" Void
        (parameters)
        (registers (register r0 Color))
        (block entry
          (construct r0 (float 1.0) (float 0.0) (float 0.0) (float 1.0))
          (store (stage color) r0)
          (return))))))

The following two hand-authored modules are also read, published, materialized, and round-tripped by that test. Arcadia.VPL.Tests.GLSL.Driver compiles and links those same files and checks their opaque-green fragment results on a native OpenGL driver.

Loop and helper call

This module uses an explicit loop back-edge, mutable registers, a forward-referenced helper, a parameter load, and explicit integer-to-floating conversion.

// Arcadia
// Copyright (C) 2024-2026 Michael Heilmann
//
// This program is free software: you can redistribute it and/or modify it under
// the terms of the GNU Affero General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option) any
// later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
// details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see https://www.gnu.org/licenses/.

// A loop with an ordinary helper call. The fragment result is opaque green.
// The quoted strings are display names; the unquoted names are binding IDs.
(module 1
  (types
    (type Void void)
    (type Int int)
    (type Bool bool)
    (type Float float)
    (type Position vector float 3)
    (type Color vector float 4))
  (uniforms)
  (vertex vertex_entry
    (interface
      (in attribute "input_position" Position 0 none)
      (out clip "clip_position" Color -1 position))
    (functions
      (function vertex_entry "vertex_entry" Void
        (parameters)
        (registers (register r0 Position) (register r1 Color))
        (block entry
          (load r0 (stage attribute))
          (construct r1 r0 (float 1.0))
          (store (stage clip) r1)
          (return)))))
  (fragment fragment_entry
    (interface (out pixel "pixel_color" Color 0 fragment_color))
    (functions
      (function fragment_entry "fragment_entry" Void
        (parameters)
        (registers
          (register r0 Int)
          (register r1 Bool)
          (register r2 Float)
          (register r3 Color))
        (block entry
          (copy r0 (int 0))
          (branch test))
        (block test
          (less r1 r0 (int 2))
          (branch_if r1 body finish))
        (block body
          (call r0 increment r0)
          (branch test))
        (block finish
          (construct r2 r0)
          (divide r2 r2 (float 2.0))
          (construct r3 (float 0.0) r2 (float 0.0) (float 1.0))
          (store (stage pixel) r3)
          (return)))
      // Function/block/register IDs are scoped; r0 here is a different register.
      (function increment "increment" Int
        (parameters (parameter value Int))
        (registers (register r0 Int))
        (block entry
          (load r0 (parameter value))
          (add r0 r0 (int 1))
          (return r0))))))

Nested aggregate storage

This module initializes a complete local array of records before a dynamic indexed field store and load. The derived reference aliases storage in the local register.

// Arcadia
// Copyright (C) 2024-2026 Michael Heilmann
//
// This program is free software: you can redistribute it and/or modify it under
// the terms of the GNU Affero General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option) any
// later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
// details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see https://www.gnu.org/licenses/.

// Construct complete local values, then update one record field in an array.
// The fragment result is opaque green; the other element remains red.
(module 1
  (types
    (type Void void)
    (type Int int)
    (type Position vector float 3)
    (type Color vector float 4)
    (type Cell record "Cell" (field "color" Color) (field "weight" Int))
    (type Cells array Cell 2))
  (uniforms)
  (vertex vertex_entry
    (interface
      (in attribute "input_position" Position 0 none)
      (out clip "clip_position" Color -1 position))
    (functions
      (function vertex_entry "vertex_entry" Void
        (parameters)
        (registers (register r0 Position) (register r1 Color))
        (block entry
          (load r0 (stage attribute))
          (construct r1 r0 (float 1.0))
          (store (stage clip) r1)
          (return)))))
  (fragment fragment_entry
    (interface (out pixel "pixel_color" Color 0 fragment_color))
    (functions
      (function fragment_entry "fragment_entry" Void
        (parameters)
        (registers
          (register r0 Color)
          (register r1 Cell)
          (register r2 Cells)
          (register r3 Int))
        (block entry
          (construct r0 (float 1.0) (float 0.0) (float 0.0) (float 1.0))
          (construct r1 r0 (int 1))
          (construct r2 r1 r1)
          (copy r3 (int 1))
          (construct r0 (float 0.0) (float 1.0) (float 0.0) (float 1.0))
          (store (member_ref (index_ref (local r2) r3) "color") r0)
          (load r0 (member_ref (index_ref (local r2) r3) "color"))
          (store (stage pixel) r0)
          (return))))))

11. Text APIs and verified publication

Arcadia_VPL_Text_read accepts bytes, byte count, diagnostics, and an optional source label. It returns a verified mutable module. Lexical, syntactic, and binding/verification failures raise LexicalError, SyntacticalError, or SemanticalError respectively. Structured diagnostics carry a source label, byte offset in inputPosition, and one-based Unicode-code-point line/column. Common CR/LF groups count as one line ending. Missing-object verifier diagnostics fall back to the module opening when no precise subject is available.

Malformed form shapes or token categories are syntactic failures; for example, texture requires a register destination and cannot use void. Unknown or duplicate bindings, invalid type dimensions or uniform kinds, invalid member/index base types, and constructor type/count mismatches are semantic failures. Trailing input is a syntactic failure located at the first trailing token.

Arcadia_VPL_Text_write verifies a current module, emits deterministic text, and reads that result back through the same verifier before returning it. Type IDs are t0, t1, and so on in deterministic first-discovery order; uniforms, interface slots, functions, parameters, and blocks use u/s/f/p/b plus their list positions. Numeric register IDs are preserved. Display names, declaration/instruction/block order, nominal record relationships, and floating bits (including signed zero) are preserved. Unused type declarations, comments, trivia, and original binding-ID spellings are not preserved. The writer uses double quotes and Unicode escapes for controls, LF endings, and round-trippable finite decimal floats.

Arcadia_VPL_VerifiedModule_create publishes an opaque immutable canonical-text snapshot of a verified module. No mutable graph or cached-validity flag is exposed. getText returns immutable text; materialize returns a fresh, separately verified module. Mutating the original builder or one materialization cannot alter the saved snapshot or later materializations. The GLSL backend consumes this publication boundary.

These APIs restore the value stack on success/failure and hold transient objects through a private text context. The scanner's generic forms are private parsing data, not a public expression AST or a higher-level convenience language.

12. GLSL 420 core backend

Arcadia_VPL_Backends_GLSL_compile(thread, publishedModule, diagnostics, version) materializes and verifies an independent snapshot and returns a ProgramArtifact. The implemented configuration is version 420 core; another version is a target error. The backend receives no legacy expression AST or symbol graph and has no caller-owned mapping or fragment-name out-parameters.

12.1 Artifact and bindings

The artifact owns immutable vertexCode and fragmentCode strings, uniform bindings, vertexInterface and fragmentInterface bindings, version, and the resolved primary fragmentColorOutput and fragmentColorLocation. Code strings contain no extra terminator; GL consumers pass their explicit byte lengths. Binding glName strings include one trailing zero byte for native GL name queries.

Each binding records its logical name, exact GLSL name, type, source-list slot, and resolved location. Uniform bindings additionally record the canonical uniform kind and a blockBinding point (-1 for non-blocks). Interface bindings record direction and semantics; their uniform kind/block fields are not meaningful.

Record uniforms become std140 interface blocks. Their fields keep canonical declaration order and are named vpl_m0, vpl_m1, and so on; nested records use deterministic vpl_R names. Block names are vpl_Block followed by source uniform slot, and instance names vpl_u followed by slot. Block binding points are assigned in record-uniform order, independently of driver optimization. Other uniforms use vpl_u names and preserve declared array lengths. Reflection describes root declarations, not a table of every active leaf location; array/record uploads may query selectors such as [0].vpl_m0 using the reflected type.

12.2 Names and interface locations

Explicit interface locations are preserved. Unspecified locations are allocated in source order, avoiding explicit and previously assigned ranges within each direction. Linked fragment inputs inherit the resolved vertex-output location. Matrices consume one location per column; arrays and records consume their recursive spans. Target overlap/range errors are rejected.

Vertex inputs use vpl_attribute names, linked varyings vpl_varying names, and fragment outputs vpl_output names, suffixed by location. Thus linked declarations use consistent names even when their logical names differ. Position maps to gl_Position in vertex output and gl_FragCoord in fragment input, with no user declaration. Integer-containing varyings use flat interpolation. Canonical names cannot inject GLSL identifiers: emitted functions, registers, parameters, records, fields, blocks, and bindings use deterministic generated names.

12.3 Operations and control flow

Result-producing instructions assign statically typed function-scope register variables. Loads, stores, constructors, extraction, calls, comparisons, and arithmetic follow the canonical operations without reassociating matrix products. Parameters are invocation-local by-value function parameters. Whole block-uniform loads explicitly construct a record value from block fields; an interface-block instance is not treated as an ordinary struct value.

One-block return/discard functions emit directly. General graphs use a function-local integer block selector, a while loop, and a switch with one case per block. Branches set the selector and leave the switch iteration; returns leave the function, and discard abandons the fragment. A default return value in non-void dispatch fallback code is unreachable in the verified graph. A main wrapper invokes the explicitly selected canonical entry function. Forward declarations permit source-independent helper ordering.

Finite float literals are emitted with a private C numeric locale and nine significant digits, preserving binary32 values and signed zero. Minimum integer spelling avoids an out-of-range positive signed literal. Arithmetic and comparisons use GLSL 420 operators. Signed add/subtract/multiply therefore wrap to low 32 bits. Runtime conversion, rounding, zero signs, subnormal flushing, NaN behavior, and contraction follow GLSL 4.20 core rather than an emulated CPU IEEE contract. Cross-driver bit-identical floating arithmetic is not promised, and VPL registers do not imply the GLSL precise qualifier.

Texture emits native texture(sampler, coordinates). Vertex implicit base LOD is zero; fragment implicit LOD and addressing/filtering use the driver and external sampler state. Dispatch loops do not provide a portable implicit-derivative reconvergence promise. No uniformity analysis or explicit gradient/LOD emulation is inserted.

12.4 Initial target restrictions

These restrictions are checked after target-neutral verification and produce structured target diagnostics. Hardware-specific limits and active-resource optimization remain driver compile/link checks. The target does not silently emit placeholder code for unsupported types.

12.5 Engine integration and verification

Programs_create selects one complete checked-in canonical description from VPL/Library/Assets/Programs/: mesh-ambient-color.vpl, vertex-ambient-color.vpl, texture-ambient-color.vpl, or texture-vertex-ambient-color.vpl. CMake embeds the exact source bytes into a private BuiltinPrograms.h in the external build tree. Changes to a program, embedding script, or header template regenerate that header and rebuild the library.

The runtime selects the embedded bytes, reads/binds/verifies them through Text_read with the source filename as its diagnostic label, and publishes VerifiedModule. No runtime file lookup, working-directory convention, or source-tree availability is required for built-ins. Arbitrary external source files still use the ordinary text-reading interface. The embedded data is a build product; the .vpl files are the editable source of truth.

Material nodes and backend resource creation pass VerifiedModule. The OpenGL resource compiles one artifact, uploads both code strings with explicit lengths, binds the resolved fragment output before linking, and uses artifact reflection for uniform blocks and scalar locations. Deterministic block binding points remain available to model resources even when a driver optimizes an unused block away.

Canonical goldens cover all four variants and reflection. The Windows GLSL.Driver test creates a hidden 4.2 core context, compiles/links, checks std140 block sizes and output/attribute locations, and renders each variant into an RGBA8 framebuffer. Additional loop variants check dispatch execution with expected pixels. A general fixture compiles/links record/array uniforms, integer-containing varyings, calls, storage selectors, and terminating discard. Driver tests report skipped status if no suitable core context is available.

Numeric/sampling conformance renders a pass color only when native signed wrapping, negative integer division, exact runtime conversion, finite arithmetic, and zero comparison behave as specified. Sampler cases distinguish repeat from clamp addressing and verify fragment implicit mip selection versus vertex base LOD zero using different mip colors. Tests do not assert nonportable NaN payloads, zero-sign arithmetic, subnormal retention, correctly-rounded shader division, or implicit derivatives in divergent flow.

13. Conformance and extensions

Version-1 conformance evidence includes positive and negative text/binding cases, constructor and binary-operation tables, verifier data-flow/output checks, deterministic publication and GLSL goldens, and native driver compile/link/pixel checks. The complete examples in section 10.3 are read from the same files included in this specification. Driver tests may be skipped when no suitable context exists; such a skip is not native-driver execution evidence.

The following work extends the established version-1 scope:

Target-neutral validity alone does not imply support by every backend. The migration checklist and register-based control-flow rationale are recorded in VPL/Library/Sources/Arcadia/VPL/DESIGN.md.