Softanza
DémarrerEnglish
Softanza
Recherche · narration

Conditional Code in Softanza — Constraining Functions with W and WF

par Mansour Ayouni · dans le dépôt depuis le 2026-06-18 · essai de paradigme · sans domaine de l'Atlas

Cet article est écrit en anglais, comme la bibliothèque.

Many Softanza functions accept a constraint: a small piece of logic that decides, per item, whether it qualifies. "Find the items that are uppercase", "count the even numbers", "keep the strings longer than 3 characters". Softanza lets you express that constraint in two complementary ways — and, importantly, without eval().

Haro
o = new stzList([ "haro", "PHP", "C#", "ruby", "GO" ])

? @@( o.FindW('{ IsUppercase(@item) }') )           # textual constraint (DSL)
#--> [ 2, 3, 5 ]

? @@( o.FindWF( func x { return Q(x).IsUppercase() } ) )  # function constraint
#--> [ 2, 3, 5 ]

illustration

Both answer the same question. They differ in how the constraint is written and in the trade-offs that come with each — and that difference is the whole point.

Two paths, one idea

W — the sandboxed DSL WF — the anonymous function
You write a short text expression a real Haro function f(item)
Evaluated by the Zig engine (compile → bytecode → run) Haro itself, called natively per item
eval()? never never
Arbitrary logic (your own functions, methods, side effects) no — bounded on purpose yes
Serializable (store in config / a file / a query / accept from a user) yes — it's just a string no — a function can't be serialized
Safe with untrusted input yes — it cannot escape the sandbox no — untrusted code would run with full power
Speed compiled bytecode, very fast native Haro call, fast

They are not competitors: they cover opposite needs.

  • Reach for W when the constraint is a self-contained expression over the item, especially when it comes from outside the program (a stored filter, a rule from a config file, a query a user typed). The boundary is a feature.
  • Reach for WF when you need real Haro logic — calling your own functions, invoking methods, anything the DSL deliberately doesn't allow.

Why not "just evaluate any Haro code from text"? That was the original idea, and it was implemented with eval(). eval() is bad on two fronts: it is slow (re-parsed and re-run per item) and unsafe (a textual constraint from an untrusted source could run anything). Softanza splits the goal instead: the textual path becomes a bounded, fast, safe DSL; the "any Haro code" need moves to anonymous functions, where it is both fast and trustable. eval() is used by neither.

The W DSL

A W condition is a string (optionally wrapped in { } for readability). Inside it you have:

Placeholders - @item — the current item - @i — its 1-based position - @numberofitems — the list's length - @accumulator / @value — the running value (in reduce-style contexts) - @char — the current character (in character-walking contexts)

Operators — + - * / %, comparisons = != < <= > >=, logical and or not.

Builtins (a curated, growing set) - type tests: isString, isNumber, isList, isBool, isNull - predicates: IsUppercase, IsLowercase, IsEven, IsOdd, IsPositive, IsNegative - text: len, upper, lower, trim, reversed, contains, startswith, endswith, left, right, substr, indexof, replace - numbers: abs, sqrt, floor, ceil, round, min, max - conversion: number, string, type

Haro
o = new stzList([ 4, 7, 10, 3, 8 ])

? @@( o.FindW('{ @item > 5 }') )                #--> [ 2, 3, 5 ]
? @@( o.FindW('{ IsEven(@item) }') )            #--> [ 1, 3, 5 ]
? o.CheckW('{ isNumber(@item) }')               #--> TRUE
? o.CountW('{ @item > 5 and IsEven(@item) }')   #--> 2

illustration

The DSL is case-insensitive for names (IsUppercase, isuppercase, ISUPPERCASE are the same), so you can write predicates the way they read.

One W, not W + WXT

Historically there were two textual forms — W (fast, limited) and WXT (expressive, slow) — because the pure-Haro eval() implementation forced a choice between speed and expressiveness. With the engine DSL there is no such trade-off: a single W is both fast and fully expressive. WXT remains as a deprecated alias of W.

What W deliberately cannot do

The DSL evaluates a value-level expression over the item. It does not: - call your own Haro functions or object methods, - mutate state or produce side effects, - operate on list-typed items as rich objects (a list item is seen as "not a scalar"; for per-element logic on nested items, use WF).

That last point is the usual reason to switch to WF:

Haro
o = new stzList([ 1:3, 1:3, 5 ])               # items are lists

# DSL can't introspect list items -> use a function:
? o.CheckWF( func x { return isList(x) and len(x) = 3 } )
#--> FALSE      (the 5 is not a 3-element list)

illustration

The WF family — anonymous-function constraints

WF takes a Haro function f(item) that returns true/false. It is called natively, so the constraint can be anything Haro can express.

Haro
o = new stzList([ "alpha", "BETA", "gamma", "DELTA" ])

? @@( o.FindWF(   func x { return Q(x).IsUppercase() } ) )   #--> [ 2, 4 ]
? @@( o.ItemsWF(  func x { return len(x) > 4 } ) )           #--> [ "alpha", "gamma", "DELTA" ]
? o.CheckWF(      func x { return isString(x) } )            #--> TRUE
? o.CountWF(      func x { return Q(x).IsLowercase() } )     #--> 2

illustration

The WF set mirrors the W set: FindWF/FindAllWF/PositionsWF, CheckWF/AllItemsWF, CountWF/NumberOfItemsWF, ContainsWF, ItemsWF/FilterWF/ExtractWF, ItemsAndTheirPositionsWF, UniqueItemsWF/CountUniqueItemsWF, NthItemWF/FirstItemWF/LastItemWF, the mutators RemoveWF/ReplaceWF/InsertAfterWF/InsertBeforeWF, the transform MapWF/YieldWF, and the positional CheckItemsAtWF/CheckOnWF.

PerformWF(condFunc, actionFunc) is the function form of PerformW — it transforms each matching item with actionFunc, leaving the rest unchanged:

Haro
o = new stzList([ 0, 2, 0, 3, [1,2] ])
o.PerformWF( func x { return Q(x).IsANumber() },        # which items
             func x { return Q(x).RepeatedInAPair() } )  # how to transform
? @@( o.Content() )
#--> [ [ 0, 0 ], [ 2, 2 ], [ 0, 0 ], [ 3, 3 ], [ 1, 2 ] ]

illustration

Operator indexing: o['{ … }'] (a condition string in brackets) is part of the textual DSL path, so it accepts the engine DSL only. For a constraint that needs real Haro logic, use FindWF(func …) rather than o[…].

Choosing between them — a one-line test

Can my constraint be a self-contained expression over the item? → W (and you also get serializability + safety for untrusted input).

Do I need to run real Haro logic (my functions, methods, …)? → WF.

Under the hood

  • The W DSL is compiled and evaluated by the engine's expression module (engine/src/expr.zig): a small recursive-descent compiler producing bytecode that a tight evaluator runs per item. The same engine powers Map/Filter/Reduce/Find/Count with the @item placeholder.
  • WF is plain Haro: the function is compiled once by Haro and invoked with a native call per item — no string handling on the hot path.
  • Neither path calls eval(). The conditional-code idea stays first-class as text (via W), and arbitrary logic gets a fast, trustable home (via WF).

Citer

Mansour Ayouni. « Conditional Code in Softanza — Constraining Functions with `W` and `WF` ». Softanza narrations, 2026. https://mayouni.github.io/stzsite/fr/narrations/stz-conditional-code-narration.html

Non exécuté : son code ne se charge pas tel qu'il est écrit. L'article est publié tel qu'il est écrit ; ses blocs sont des illustrations. Le code des articles est du code Haro. Là où un article a été écrit avant que la langue prenne son nom actuel, le nom a été changé et rien d'autre. Genre, domaine et série lus dans le nom du fichier, année tirée du premier commit : dérivés, jusqu'à ce que l'article porte son propre en-tête. Le fichier sur GitHub(s'ouvre dans un nouvel onglet)