Array Languages #12: ML Pipelines as X_eTaL Macros Softwarewrighter published the Flow library in X_eTaL-ML, which lets developers write machine-learning pipelines as left-to-right macros that expand into ordinary right-to-left X_eTaL code. The library's demo builds a two-layer network over four points with hand-set weights that separates the XOR inputs, printing class assignments `1 2 2 1` that match a hand computation to the last digit, and a four-stage stable softmax pipeline yields `0.0900 0.2447 0.6652` for `z := 1 2 3`. The release also introduces the `c"..."` code string, which marks macro contents as X_eTaL so renderers draw each stage as code, and the `▷` pipeline token, which is valid only inside a macro's text and an error in ordinary code. Array Languages 12: ML Pipelines as X eTaL Macros 2173 words • 11 min read • Abstract | Resource | Link | |---|---| | The library | Flow https://github.com/softwarewrighter/X eTaL-ML/tree/main/libs/Flow in X eTaL-ML · its reference https://github.com/softwarewrighter/X eTaL-ML/blob/main/libs/Flow/docs/README.md | | The design | docs/spatial.md https://github.com/softwarewrighter/X eTaL/blob/main/docs/spatial.md , “Pipelines”, in X eTaL | | The thread | 11: equations https://blog.softwarewrighter.com/2026/10/17/array-languages-xetal-equations/ · 5: X eTaL-ML https://blog.softwarewrighter.com/2026/10/11/array-languages-xetal-ml/ | | Comments | Discord https://discord.com/invite/Ctzk5uHggZ | Three directions Each notation keeps its own reading direction, and the macro expansion is where they meet: - Equations read inside out, as the paper prints them: sums outermost first, the summand inside. - Pipelines read left to right: the data first, then each transform in turn. - X eTaL reads right to left, as APL does. Both macros expand to ordinary X eTaL, which xetal expand shows. A network, left to right The library is Flow , in X eTaL-ML, beside the neural-network functions its stages call. Its demo is a small two-layer network over four points, written one stage per line: "nn:" u̲se< "NN" "flow:" u̲se< "Flow" Y ← @ ᶠˡᵒʷp̲ipe< c" X ▷ it ⁿⁿd̲ense W1 ▷ ⁿⁿr̲elu ▷ it ⁿⁿd̲ense W2 ▷ ⁿⁿs̲oftmax" Read it the way it is written: start from the data X , apply the first dense layer, then a ReLU, then the second dense layer, then a softmax. The figure shows each stage and the shape it produces. By hand, in X eTaL, the same network reads right to left, with the last step first: ⁿⁿs̲oftmax ⁿⁿr̲elu X ⁿⁿd̲ense W1 ⁿⁿd̲ense W2 The network’s weights are set by hand so that it separates the four points of XOR: the demo prints the class each point gets, 1 2 2 1 , the two points that differ in one input in one class and the other two in the other. It also computes the network by hand, and the two results are identical, to the last digit. The c"..." is a code string, new in the language this week. Its value is an ordinary string, the same characters the macro has always received, but the c tells every tool that the inside is X eTaL, so the renderer draws each stage as code, as above, rather than as quoted text. It can run across as many lines as it needs. Between the stages is | , typed as two characters and drawn as ▷ ; it is a token of its own, which means something only inside a macro’s text and is an error in ordinary code. The rules Stages are separated by ▷ . The first stage is the data. Every later stage is one of three things: - A function, applied to the previous value: ⁿⁿr̲elu , a train, '+ r̲/ , or a quoted lambda. - An expression that names the previous value it : it ⁿⁿd̲ense W1 , it − m , m − it . Where order matters, it is written, never guessed. - An expression of names bound earlier in the pipeline, which is how a model branches and joins. A label keeps a stage’s value for later stages: $m , typed with a dollar sign and drawn ▹m , at the end of a stage. A later stage that names m reads it, so a value can fan out to several stages and several values can fan in to one; ▹ Q, K, V takes a tuple apart. A shape after the label, as in ▹h 4 3 , is a promise the macro checks. Names: a stable softmax The stable softmax subtracts the maximum before exponentiating, so large scores cannot overflow: @ ᶠˡᵒʷp̲ipe< c"z ▷ 'm̲ax r̲/ it ▹m ▷ e̲xp z − m ▹e ▷ e ÷ '+ r̲/ e" Start from the scores z ; take their maximum and label it m ; raise e to z − m and label it e ; divide e by its sum. The third stage reads z and m and the fourth reads e twice: that is fan-out and fan-in, with nothing more than names. Four stages, in the order the math is explained, and with z := 1 2 3 the result is the library’s own softmax, 0.0900 0.2447 0.6652 . What it becomes What is it ? An anaphor is a word that points back to something already said: in “take the scores and normalize it”, it means the scores. Programming borrowed the word for macros that bind a name for the code written at their call; the best known is the anaphoric aif in Paul Graham’s On Lisp , which binds it to the value its test produced. In a pipeline, it is the previous stage’s value, and writing it says where that value goes, which matters wherever order does: it − m is not m − it . It is it because it reads as English and is short, and Kotlin and Groovy use the same word for the same job. Why it is safe. Hygiene is the question of whether names a macro writes can collide with names in the code around its call. In X eTaL hygiene is a choice, as in Common Lisp rather than Scheme. The pipeline macro’s own names come fresh from the language, g1:stage , g2:stage and so on, so they cannot collide with anything. it is the deliberate exception, declared in the macro’s contract with binds: it : a stage may read it, but may not bind an it of its own, and a label named it is refused. The macro writes ordinary X eTaL, laid out as a program, one line per stage. This is the network above, from xetal expand : Y ← { @ → g1:stage ← X g2:stage ← { @ → it ← g1:stage it ⁿⁿd̲ense W1 } @ g3:stage ← ⁿⁿr̲elu g2:stage g4:stage ← { @ → it ← g3:stage it ⁿⁿd̲ense W2 } @ g5:stage ← ⁿⁿs̲oftmax g4:stage g5:stage } @ Each stage’s value gets a name of its own, fresh from the language so it cannot clash with the program’s. A function stage is applied to the stage before it; a stage that says it gets a small block where it is that value. Nothing is evaluated twice, and the last stage is the result. Why direction matters A residual connection is “double it, then add back the input”. Written by hand in X eTaL as 2 × x + x , it reads right to left as 2 × x + x , which is not a residual. As a pipeline it reads the way it is meant: @ ᶠˡᵒʷp̲ipe< c"x ▹s ▷ 2 × it ▷ it + s" With x := 1 2 3 4 it gives 3.0 6.0 9.0 12.0 . Shapes, checked A shape written after a name is a contract. Here the second stage promises a vector of length n , which is 3, and breaks it: @ ᶠˡᵒʷp̲ipe< c"x ▷ 2 × it ▹y n ▷ y + 1" php error shape : stage 2 y : shape 4, expected 3 -- tests/shape-fails.xtl:6:1 | 6 | @ flow:p ipe< c"x | 2 it $y n | y + 1" | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The error names the stage and its label, the shape it got and the one promised, and the line it came from, shown, as an error message shows source, as typed. For now the check runs when the program runs, which is better than a wrong answer but later than it could be. A shape check that proves or refutes these promises before the program runs is planned in X eTaL; the run-time check stays as the backstop. A reader that cannot drop text The pipeline macro is a reader, like the equation macro, and it was written after the equation macro’s reader was caught dropping text and giving silent wrong answers. So every rule of this one uses all of its text or refuses the call when the program is compiled, naming what it could not use: error bad-pipeline : a shape is extents at the stage's end, nothing after it: 4 extra error bad-pipeline : the first stage is the data; nothing precedes it: it 2 error bad-pipeline : a name is bound once in a pipeline: m error bad-pipeline : a stage is a function nn:r elu, f g , '{ x - ... } or an expression naming the previous value it or a name bound before it : 3 error bad-pipeline : $ labels a stage and touches the name or tuple it gives $m, $ Q, K : x $ m The library’s tests go further than single cases. The five pipelines from the design, linear regression, the stable softmax, layer norm, a small MLP and the residual, must each equal their hand-written versions, written on one line and one stage per line. A differential test runs every kind of stage, in every order, against plain X eTaL. And ᶠˡᵒʷt̲ext< writes back what the macro read, so a test can check that it read exactly what was written. What comes next A dense layer’s bias no longer has to be reshaped by hand: the stage b ⁿⁿb̲ias it adds it along the right axis, and refuses a bias of the wrong length. Next in X eTaL-ML are the real models as pipelines, each shown four ways, side by side: as the equation of post 11, as a pipeline, as the expansion, and as plain X eTaL. And in X eTaL, every code string is now read with the rest of the file: a pipeline’s string is lexed when the program is, so a stray character in a stage is an error before the macro runs. Full checking inside, linting and type-checking each stage on its own, applies where a macro takes plain X eTaL; a pipeline’s string mixes X eTaL with its own notation, ▷ and labels, so its stages are still checked as the macro expands them. Because a pipeline is ordinary X eTaL once expanded, it can go where any X eTaL program goes, including the GPUs of post 7 https://blog.softwarewrighter.com/2026/10/13/array-languages-xetal-gpu/ and post 10 https://blog.softwarewrighter.com/2026/10/16/array-languages-xetal-mojo-metal/ , and the step-by-step Blender videos of post 9 https://blog.softwarewrighter.com/2026/10/15/array-languages-xetal-blender/ . Part 12 of the Array Languages series. View all parts https://blog.softwarewrighter.com/series/ array-languages Comments or questions? SW Lab Discord https://discord.com/invite/Ctzk5uHggZ or YouTube @SoftwareWrighter https://www.youtube.com/@SoftwareWrighter .