Inner DSL Reference¶
The inner DSL is the language inside a library function's body — the sequence of statements that runs every time the function matches a source statement. It combines two concerns into one block:
- Output via
write \...``, which appends a (possibly interpolated) string to the function's body contribution. - State via
set/append/prepend/merge/delete, which mutate the accumulatedcontextmap.
Plus control flow (if / else / for / loop) and primitive
host calls (env, arg, read_file, os, arch, …). It does
not execute user-script code — library authors compose primitives
to describe what each match contributes to the final output.
Legacy form: the inner DSL also lives inside a
run:block when a library uses the older two-block shape. Both shapes work; new libraries should prefer the unified body.
Tokens & expressions¶
The inner DSL reuses the engine's lexer and value-expression parser. Values may be:
- Numbers, strings, templates,
true,false,null. - Bare or dotted identifier paths — these resolve, in order, against:
inner-DSL locals (loop variables), captures from the matched function,
the root
contextmap. - Indexed reads: a path may carry
[<expr>]index steps —context.buf[i],context.known[name],context.grid[i][j],context.rows[i].name,context.buf[(sub n 1)],context.buf[-1]. A map parent is keyed by the index's string form; a list parent is keyed by an integer (negative counts from the end). This is the read-side twin of theset context.buf[i] …write target, so a value written by index reads back by the same index. A missing key or out-of-range index isnil(falsy) in value position and renders empty inside a${…}template — soif context.seen[k]guards cleanly without afor-scan. Seesamples/value-index-read/. - Lists
[...]and objects{...}. - Comparison expressions:
a == b,a != b,a < b,a <= b,a > b,a >= b. - Unary
not expr. - Parenthesized sub-calls:
(regex_match name "^[a-z]+$").
Statements¶
write <expr>¶
Appends EXPR (coerced to string) to the function's output buffer.
EXPR is most commonly a backtick literal with ${EXPR} interpolations:
Backtick literals are multi-line; the bytes inside (including
newlines, tabs, leading whitespace) are emitted verbatim. ${EXPR}
holes accept any value expression: paths (name, context.foo,
body), helper calls (indent 4 body, pascalCase name), or
literals.
write has no effect on context — pair it with set/append/etc.
in the same function body when both output and state mutation are
needed.
set <path> <value>¶
Assigns a value to a field path on the context (or to a local).
set context.name "Alice"
set context.config.api.url "https://example.com"
set context.scripts[key] cmd
set context.buf[i] " ; (rewritten in place)" # list element by index
Paths use:
- .<field> for map field access.
- [<expr>] for dynamic indexing. When the parent is a map, the
expression's string form becomes the key. When the parent is a
list, the expression must evaluate to an integer index and the
element is overwritten in place (negative indices count from the
end, so -1 is the last element). Out-of-range indices error.
Overwriting a list element by index is what enables retroactive
rewrites of buffered output — e.g. an optimizer that buffers its
instruction stream in context.buf and later nulls out a dead store, or
back-patches a jump offset once the target is known. See
samples/list-index-assign/
for a dead-store eliminator built this way.
append <list-path> <value>¶
Appends to a list field. Creates the list if it doesn't exist yet. With
an index target (append context.rows[i] value) it appends to the
nested list stored at element i.
prepend <list-path> <value>¶
Like append but inserts at the front (also works on an indexed nested
list, prepend context.rows[i] value).
merge <map-path> <map-value>¶
Shallow-merges a map into a map field. The value must be a map expression.
delete <path>¶
Removes a field or list index.
if <expr> … (else …) end¶
Library-side conditional. The expression is evaluated; if truthy,
the body runs. An optional else arm handles the falsy case.
else if cond chains naturally.
if (regex_match name "^_")
set context.private true
end
if optional
write `${name}?: any;
`
else
write `${name}: any;
`
end
for <var> in <expr> … end (alias: loop)¶
Iterates a list, binding the variable in each iteration's local
scope. for and loop are synonyms.
Note: this iterates within a single function body. It does NOT
iterate user-script code — that's what block: functions are for.
Plain calls¶
A line that starts with an identifier that isn't a statement keyword is treated as a primitive call:
The only primitive call defined is error <message> (abort transpilation).
regex_match is also callable but is most useful in expression position
((regex_match v "...")).
Truthiness¶
| Value | Truthy? |
|---|---|
nil / null |
no |
false |
no |
"" (empty string) |
no |
0, 0.0 |
no |
empty list [] |
no |
empty map {} |
no |
| anything else | yes |
Captures inside the function body¶
When you reference a capture by name, you get the evaluated value:
- A string literal
"foo"becomes the Go string"foo"(no surrounding quotes). - A number becomes
int64orfloat64. - A list becomes
[]anyof evaluated items. - An object becomes
map[string]any. - A bare identifier (when the source has e.g.
import json) becomes its literal name as a string ("json").
This means append context.imports name correctly stores "json" without
extra quoting — useful for the file template's for ... in ... end loop.
Engine-injected locals¶
In addition to your function's captures, these locals are always available inside the function body:
| Local | Available where | What it is |
|---|---|---|
body |
write literals AND state-mutation statements |
The rendered output of the function's inner block (block functions only). In a state mutation like append context.styles {name: name, body: body}, this lets you stash rendered text back into context. |
top_level |
write literals AND state-mutation statements |
Boolean. true when the function call is being rendered at the file's outermost program block; false once we're inside any block's body. |
depth |
write literals AND state-mutation statements |
Integer. 0 at the top level, 1 inside one nested block, 2 inside two, etc. |
line |
write literals AND state-mutation statements |
Integer. The 1-indexed source line of the statement's first token. Stamp it onto emitted output for source↔output mapping: <p data-capy-line="${line}">…</p>. |
col |
write literals AND state-mutation statements |
Integer. The 1-indexed source column of the statement's first token. |
top_level is the convenience boolean — most uses only need
if top_level … else … end to branch between "this call appears at
file scope" and "this call appears inside a block body". Concrete
win: a single NAME = VALUE syntax can be a declaration at file
scope (wrap in <script> for an HTML target, emit a var line for
a JS target, etc.) and a bare reassignment inside a handler body,
with no extra keyword required from the user.
line / col give a host editor source↔output mapping for free: a
library that writes data-capy-line="${line}" lets the editor do
querySelector('[data-capy-line="N"]') for scroll-sync or inline
error underlines — no source mutation, works inside every region.
If a user-defined capture happens to be named body, top_level,
depth, line, or col, the capture wins: the engine local
only gets injected when there is no capture of the same name.
Context paths¶
Paths must be rooted at context (or at a local introduced by a loop).
You cannot directly mutate captures.
set context.imports.json true # ok
set context.imports[name] true # ok — `name` is a capture
set name "json" # ERROR — captures are read-only
Putting it together¶
# transpile-py-style import handling
function import
arg literal "import"
arg capture name ident
if (regex_match name "^[a-z][a-z_]*$")
append context.imports name
end
if not (regex_match name "^[a-z][a-z_]*$")
error "invalid module name"
end
end
What's NOT here (and why)¶
The inner DSL is intentionally small. It does not have:
- User-defined inner functions. Compose with multiple library functions or with
loop. elsebranches. Use twoifstatements or invert withnot.- Arithmetic operators (
+,-, …). Compute at template time with helpers, or accumulate into a count.
These omissions keep the runtime tiny and predictable. If you find yourself wanting them often, open a feature request.