Rules: actions and assignment
Action rules
-
The assistant MUST avoid
FORwhen the same result can be expressed with a set-based construct.FORiterates row by row and SHOULD be the last resort when no declarative alternative exists. The one measured exception runs the other way: when the assigned value is aGROUPaggregate whose bounds correlate with the row being updated, and both sets are large, the set-based form can compile to a query that materializes the whole correlation, and a row-by-rowFOR ... NOINLINEcan be the faster one — where an index on the aggregated class lets each row's aggregate be answered by an index lookup.Prefer set-based alternatives, for example:
- aggregation or set materialization
->
GROUP SUM,GROUP CONCAT,GROUP MAX,GROUP LAST,GROUP AGGR - assigning a property over a set
-> direct property assignment with parameters
instead of a
FOR ... DOloop - exporting tabular or hierarchical data
->
EXPORT FROM,EXPORT JSON FROM,EXPORT XML FROM,EXPORT CSV FROM - building structured payloads
->
JSON FROM,XML FROM - bulk integration writes
->
NEW,DELETE, or set-based property change instead of a per-rowFOR
FORis acceptable when the body has genuine per-row control flow such as conditionalAPPLY,MESSAGE,throwException, or external calls that cannot be expressed as a set operation. - aggregation or set materialization
->
-
Parameters introduced in
NEW alias = ClassandFOR expr(p) [NEW alias = Class] DO { ... }do NOT follow the usual lexical scoping rules of mainstream programming languages.Such parameters are visible ONLY inside the body of the
NEWblock or theFORloop that introduces them.The assistant MUST NOT reference these parameters outside their introducing block.
When dependent computation must reuse these parameters, the assistant SHOULD nest further
NEWorFORblocks inside the introducing block, where the parameters are still in scope, rather than lifting values out into auxiliary storage.Conversely, a parameter declared inside a
GROUPaggregate belongs to that aggregate and is NOT visible outside of it; in particular it cannot serve as the loop variable of the enclosingFOR. Declare the variable as theFOR's own parameter and use the aggregate only as a boolean condition over it. -
The assistant SHOULD avoid introducing
LOCALproperties without a concrete need.A
LOCALmaterializes a temporary table in PostgreSQL only once it holds more than one row, so the runtime cost well above a stack variable in a conventional language applies toLOCALs with parameters (buffers keyed by row number, per-object values). A parameterlessLOCALholds at most one row and always stays in memory, so parameterless flags and single values are cheap; avoid them to keep the number of entities down, not because of cost. -
A
LOCALis normally justified when BOTH conditions hold:- its value is non-trivial to compute (aggregation, joins, multi-step logic, external calls, or other work worth materializing), AND
- the same value is consumed more than once, so materializing it avoids recomputation.
-
When possible, the assistant SHOULD prefer alternatives to a fresh
LOCAL:- inline the expression at each use site if it is cheap
- nest
NEW/FORblocks so intermediate values stay in parameter scope - use a regular (non-
LOCAL) calculated property when the value is reusable across actions
-
These are recommendations, not hard prohibitions. If the assistant cannot find a working syntax for a
LOCAL-free construction, or some other approach keeps failing and a clean action cannot be built, falling back to aLOCALis acceptable as a last resort.Established
LOCALpatterns mandated by other rules (e.g. import staging, nested-session carry-over) remain valid; the assistant SHOULD still keep suchLOCALs minimal in count and scope. -
The parameters of the top-level statements of an action body share one parameter context: identical names denote the same parameter, and a parameter's class is declared only at its first use.
In generated scripts (
eval, data seeding) the assistant SHOULD give the parameters of top-level statements unique names, so as not to depend on the statement order. -
Many system utility actions return their result through a same-named parameterless
LOCALproperty (for example, inUtils: the actionfileExists[ISTRING[500]]writes into the propertyfileExists[]). Such an element is an ACTION, not a boolean property: the assistant MUST call the action first and then read the parameterless property (fileExists(path); IF fileExists() THEN ...), and MUST NOT use the parameterized form inside an expression (IF fileExists(path)is wrong).
Assignment rules (<-)
-
The arguments of the changed property on the left side of
<-may be expressions over the statement's parameters (sentFolder(account(f)) <- f), but new local parameters can be introduced only as typed parameters, not inside expressions. Writing "into a computed key" by analogy with imperativemap[key] = valueeasily breaks this.So when remapping self-referential links while deep-copying an object graph, the assistant SHOULD keep an inverse map and iterate with the TARGET object as the parameter —
link(Copy n) <- newOf(link(srcOf(n))) WHERE spec(n);— rather than writelink(newOf(x)) <- newOf(link(x)); -
<- expr IF condassigns the whole expression to ALL objects: wherecondfails, the property is overwritten withNULL. It is effectively reset-plus-set.When ADDING an assignment to a property already populated earlier in the same action, the assistant MUST use the
WHEREform (prop(x) <- TRUE WHERE cond(x)), which changes only the rows matching the condition. A second IF-form assignment to the same property MUST be treated as a review red flag. -
Inline in an action or event body,
PREV(<expr>)takes the WHOLE wrapped expression to the session-start state, including its argument sub-expressions: an argument computed in the current session (aLOCAL, a property of an object created in the session) reads asNULLinsidePREV, silently nulling the result.To read previous data with current arguments, the assistant MUST wrap
PREVin a separate property —prevF(x) = PREV(f(x));— and call it instead of writingPREV(f(<session-computed arg>))inline. -
A parameter through which a property whose name is declared on several classes is read or changed MUST be annotated with its class at first use (
date(Interaction i) <- ...): an overloaded name is resolved by the parameter classes, and an untyped parameter yields an "ambiguous name" error. This especially concerns events: their statement is a separate parameter context in which the class is not inferred from anywhere else, and ani IS Interactioncondition does not set the parameter's class.
Loop rules (FOR, WHILE)
-
FORfixes its set before the first iteration: the condition is evaluated once, the matching rows are read, and the body then runs once per row of that set. What the body changes — the data under the condition included — does not add or remove iterations.WHILEis the operator that re-reads, but it does so per STEP, not per row: one step re-evaluates the condition, reads the whole matching set and runs the body for every row of it, and only then is the set read again; iteration stops when it comes back empty. So a row already in the current step still gets its turn even if an earlier row of that same step has made the condition false for it. -
Without
ORDERaFORwalks its set in arbitrary order. The assistant MUST give an explicitORDERwhenever the result depends on the sequence — numbering, running totals, anything reading what an earlier iteration wrote — or wheneverTOPlimits how many rows are taken, and MUST end thatORDERwith a key that separates any two rows.
Thread rules (NEWTHREAD, NEWEXECUTOR)
-
A server-side thread action shares the change session of the calling code, and change sessions are not thread-safe.
The assistant SHOULD therefore wrap the body of a server-side
NEWTHREADinNEWSESSION, and inNEWSESSION NEWSQLwhen it needs a database transaction of its own. It is a trade: a plainNEWSESSIONno longer sees the caller's unsaved changes, so the wrapping is left out only where sharing the session is deliberate AND the two are known not to run at the same time.What the wrapping buys inside an
APPLYtransaction depends on WHEN the body actually starts.NEWSESSION,NEWSQLincluded, creates no session while the transaction is still open — the action is deferred into the current one — and the check happens at the moment the body runs, not at the moment it is scheduled. ASCHEDULE DELAYis a number of milliseconds, not a barrier waiting for the apply, so it guarantees nothing either. The assistant MUST NOT count on a thread started from a global handler being isolated.A client executor is the opposite case: the action is delivered to the user's connection and runs there in its own fresh session, so wrapping it adds nothing.