Imported from yaml/yamlscript (
ai/codex/.agents/skills/yamlscript/SKILL.md). Install upstream withnpx skills add yaml/yamlscript --skill yamlscript. Copyright stays with the author.
YAMLScript Skill
Setup
Ensure ys version 0.3.2 is available for testing:
[[ -x /tmp/ys-skill/bin/ys-0.3.2 ]] ||
source <(curl -sL https://in-1.cc) --local ys \
YAMLSCRIPT-VERSION=0.3.2 PREFIX=/tmp/ys-skill
YS=/tmp/ys-skill/bin/ys
Optionally clone the source for looking up stdlib functions, DWIM support, and docs:
[[ -d /tmp/ys-skill/yamlscript ]] ||
git clone --depth 1 https://github.com/yaml/yamlscript \
/tmp/ys-skill/yamlscript
# Key files:
# core/src/ys/std.clj — YS standard library
# core/src/ys/dwim.clj — functions with auto arg-placement
# doc/ — language documentation
Workflow
-
Write correct Clojure first — Clojure is unambiguous; get the logic right before worrying about YS syntax
-
Convert to YAMLScript — apply the rules below
-
Test every attempt before presenting it:
# Single-line expressions $YS -pe 'expr' # Multi-line programs $YS -c - <<<'!ys-0 ...' -
Iterate until the output is correct and idiomatic
-
Lint the source with the
ys-lint.ysscript that ships next to this SKILL.md (same skill directory). Run it against every.ysfile you wrote or edited:/path/to/skill/ys-lint.ys FILE...ys-lint.ysflags possible surface-form mistakes the compiler can't see because they vanish at the AST stage:.nth(N)vs.N,.nth(var)vs.$var,x + 1vs.++,x - 1vs.--,.first()/.last()vs.0/.$(or:first/:last),vector(...)and inlineV+(...)vs+[...], inlineM+(...)vs+{...}, avoidableapplycalls vs direct splats (f(xs*)/f: xs*),str(bareVar)vsbareVar:S,quot(a b)anda.quot(b)vsa // b,rem(a b)vsa % b, parenthesized simple integer-looking divisions such as(n / d)vs(n // d),:zero?vs.!when falsey-zero semantics are OK,or(/and(calls vs||/&&, anyKW (cond):test-expression paren-wrap (forif,if-not,when,when-not,while), anyKW [...]:bracketed binding form for the reliably-strippable keywords (binding,if-let/if-lets/if-some,let,loop,when-first/when-let/when-lets/when-some,with-open) and for the iteration keywords (each,for,doseq,dotimes) when the binding starts with a named var (not a_-var or destructure pattern),then: nil/else: nilvswhen/when-not, directthen: falseunderifas a candidate for reversedwhen, a zero-arg methodx.foo()vs the colon chainx:foo, a=>:whose value is a call / colon-chain / dot-chain / spaced binary op vs a pair form (f: args/x: .m(a)/a OP: b), a direct=>:child under anifblock vsthen:/else:,say: ''vs baresay:,x.join(' ')vs the colon chainx:joins,slurp/spitvsread/write, displaced Clojure I/O names such asprintlnandread-line, retiredrequirevsuse, plain-YAML structural checks such as scalarthen:/else:branches that can be positionalifbranches, a widerecur:/looparg list that should be comma-separated, and lines over 79 cols.Most linter rules match source text with regex; a few strip the top
!ys-0tag, load the file as plain YAML, and inspect the YAML shape. Every hit is still a candidate, not a verdict. False positives are expected: a long line may be a literal task string the program can't shorten; anx - 1inside a generated string isn't a.--candidate; an identifier that happens to look like a pattern may not be one. Inspect every reported line, fix the real mistakes, and explicitly justify each hit you treat as a false positive. This step is required: the working program isn't done until you have walked every lint hit and either fixed it or accepted it with reason.
Program Tag
- Always use
!ys-0— the short idiomatic form !yamlscript/v0and!yamlscript/v0/are legacy — do not use!ys-0= code mode;!ys-0:= data mode
YS vs Clojure Standard Library
Prefer YS stdlib functions (ys.std) over their Clojure equivalents —
they are more powerful and polymorphic (e.g. reverse works on strings,
replace defaults the replacement to "", rng works on chars).
If performance is a concern, fall back to the specific Clojure function
for that case.
Most Math/* functions are exposed in ys.std (sqrt, sqr,
floor, abs, pow, etc.). Drop the Math/ prefix when a YS
builtin exists; it's more idiomatic.
Common Mistakes
Patterns Codex gets wrong most often. Scan these before writing any YS.
Use if for two-branch conditionals — cond only for 3+ branches
cond is only appropriate when there are three or more mutually
exclusive branches. Any time you have a single predicate plus an
else: (one real branch and one fallback), use if instead. This is
the single most common conditional mistake.
cond: x == 0: a / else: b→if x == 0: \n then: a \n else: bcond: pred: x / else: recur(...)→if pred: \n then: x \n else: recur(...)
if can drop the then: and else: keys when both branches are
pair-form children (mapping entries), because YS reads the two
children positionally regardless of their keys. The keys can even
collide:
if (n % d) == 0:
recur: quot(n d) d cnt.++
recur: n d.++ cnt
This also lets the then-branch be a nested if X: pair while the
else-branch is an explicit else::
if n >= 2:
if (n % d) == 0:
recur: ...
recur: ...
else: cnt
Bare-scalar branches (plain identifiers, calls, expressions) can also
drop then: / else: when both branches are simple direct scalar
values with no whitespace and neither branch starts with YAML syntax
characters such as quotes, brackets, braces, block-scalar markers, or
tags:
if prime?(candidate):
count.++
count
Prefer this over:
if prime?(candidate):
then: count.++
else: count
The linter flags {if ...: {then: scalar, else: scalar}} shapes by
loading the file as plain YAML and checking the branch values, but
only when both scalar branch source values contain no whitespace. This
rule deliberately skips quoted strings and other YAML-special starts.
This is not the same as using =>: under if, which is never correct.
Bare-scalar branches are still fragile when mixed with mapping entries:
mixing a bare scalar with an else: mapping entry is invalid YAML.
When in doubt, keep then: and else: explicit unless both branches
are pair-form children or both are simple direct scalar branch values
with no whitespace and no YAML-special start.
When both branches are bare scalars, a trailing + on the if line
folds the next two indented lines into one plain scalar that YS reads
as the remaining positional args of if:
if n == 1: +
'1'
factors(n).join(' x ')
Compiles to (if (= n 1) "1" (str/join " x " (factors n))). Use this
when both branches are short bare expressions and the symmetry reads
better than then:/else: keys.
Inner conditionals nested inside a cond clause are usually
two-branch and should be if. Before writing cond:, count the
clauses: if it's two (one predicate + else:), rewrite as if. Three
or more clauses (excluding else:) keeps cond.
Scan every cond: in the file before finishing — if it has only one
non-else: clause, it's wrong.
=>: only when no pair form works
A YAML mapping context — defn body, do: block, conditional
branch block, loop body, etc. — requires every line to be a
key: value pair. =>: is the fallback key when the expression
genuinely cannot be written as a pair.
Use =>: for atomic values (no other pair form exists):
- bare identifiers:
=>: x,=>: result - bare numeric/literal atoms:
=>: 42,=>: nil,=>: true,=>: :foo - bare interpolated strings:
=>: "$s$check" - bare data-collection literals:
=>: +[1 2 3],=>: +{a: 1}
Never use =>: as a direct branch key of an if construct.
Even when the branch result is an atom that would normally allow
=>:, an if branch already has semantic keys. Use then: or
else: instead:
if done?:
then: result
else:
recur: next
not:
if done?:
=>: result
else:
recur: next
Restructure compound expressions into a pair:
- Function call → fn-call pair
name: args=>: f(a b)→f: a b=>: vec(out)→vec: out=>: foo()→foo:=>: recur(i.++ b nx)→recur: i.++ b nx=>: V+(re im)→V+: re im
- Dot/property or method chain → chain-pair
receiver: .member=>: x.y→x: .y=>: a.b(c).d(e)→a: .b(c).d(e)=>: row.assoc(w best)→row: .assoc(w best)=>: meta.from.split('/wiki/').$→meta.from: .split('/wiki/').$
- Binary operator → op-pair
lhs OP: rhs=>: a + b→a +: b=>: n == psum→n ==: psum=>: is-thu || is-wed-leap→is-thu ||: is-wed-leap
conddefault arm: useelse:not=>:
Drop single-use indirection: a result =: expr whose only use
is a trailing =>: result folds into a single trailing pair:
result =: r:sqr == n; =>: result→r:sqr ==: nresult =: row.conj(s); =>: result→row: .conj(s)
Colon-chains must convert to dot-chains in chain-pair position —
chain-pair only supports a leading .:
=>: out:V→vec: out=>: stack:pop:pop.conj(x)→stack: .pop().pop().conj(x)
Op-pair / pair-form quirks:
%:with a scalar value is the pair form of the remainder operator:a %: bcompiles to(rem a b). With a mapping value, trailing%is the legacy generic form-map marker instead.- An operator-pair scalar value must contain exactly one form. A compound
expression such as
a %: b - cis one form, buta %: b cis invalid. Mapping values retain their mapping and form-map semantics. - A pair value cannot begin with a quoted string followed by more
args.
format: '%+.4f' x yfails to parse. Workarounds:- Promote the string into the key:
format '%+.4f': x y - Force it into the value with
+:format: +'%+.4f' x y
- Promote the string into the key:
Never write x + 1 or x - 1 — use .++ / .--
Increment and decrement by 1 are common enough to have their own postfix operators. Use them anywhere — assignment values, argument positions, return values, loop bodies, string interpolation:
v + 1→v.++v - 1→v.--(3 * v) + 1→(3 * v).++recur: i + 1→recur: i.++
.++ and .-- compile to inc+ / dec+ (polymorphic). This is the
single most-forgotten rule — scan every + 1 and - 1 before
finishing.
Exception: do not rewrite f + 1 when f is a function and the
expression is a partial application. For example, (rotate + 1) means
"a function that calls rotate with 1 as its first argument"; it is
not numeric increment and must not become rotate.++.
Never write .nth(N) or .nth(bareVar) — use .N / .$var
Index access has terse dot-forms that should be preferred over the
explicit .nth(...) call:
v.nth(0)→v.0(literal integer index)v.nth(12)→v.12s.nth(0)→s.0(works on strings too)parts.nth(2)→parts.2v.nth(i)→v.$i(bare variable index)v.nth(idx)→v.$idxm.nth(ip)→m.$ip
.nth(expr) is only correct when the index is a computed
expression — e.g. v.nth(i.--), v.nth((row * 4) + c),
v.nth(i + g). The .$var form takes a single bare variable; it
does not accept compound expressions.
Scan every .nth( in the file before finishing — if the argument is a
literal integer or a bare variable, rewrite to the dot/dollar form.
.first() / .last() — use .0 / .$ or :first / :last
The call form x.first() and x.last() is verbose. Two terser
alternatives, each with its own niche:
.0/.$— positional access. Use when the value is a vector or pair and you're thinking "first/last element by index":pair.first()→pair.0tuple.last()→tuple.$sorted.first()→sorted.0
:first/:last— colon-chain. Use when the value is a sequence and you want the seq operations' "head/tail" framing:tri.last()→tri:lastlines.first()→lines:first
Either reads better than the call form. Pick by whether the data is
indexable (.0/.$) or seq-like (:first/:last); both compile
to the same thing for vectors, so when in doubt use the dot form.
Never write vector(...) literals — use vector syntax
vector(a b c) (the fn call) and vector literal syntax build the
same thing. Always prefer vector syntax — it reads as a literal, not
a function call.
Use +[...] only when the vector literal is the entire YAML value
plain scalar and therefore needs the leading + escape:
vector(a b c d)→+[a b c d]vector(nt ny)→+[nt ny]vector()→+[]
Inside YeS expressions, function arguments, lambdas, method calls, or
any other expression context where [ is not the first character of
the YAML value, use bare [...] with no +:
digits.map(\(vector(_ s)))→digits.map(\([_ s]))rest.conj(vector(v ns))→rest.conj([v ns])
Never write \(+[...]), foo(+[...]), or obj.method(+[...]).
There + is not an escape; it is parsed as addition/concatenation.
Use vec(coll) only when you're converting an existing collection,
not when listing elements.
Prefer +[] / +{} for collection literals
Use +[...] and +{...} for collection literals when the literal
starts the YAML value. They read as literals and should be the default
for short vectors and maps in value position, including maps with
computed values:
pair =: +[name score]
node =: +{:char ch :freq freq}
If the literal is inside a YeS expression, drop the + because the
literal no longer starts the YAML value:
items.map(\([name score]))
nodes.conj({:char ch :freq freq})
Use V+ and M+ mainly when the collection constructor is the pair
key, especially for block form or when the call layout is clearer as a
YAML pair:
M+: :a 1, :b 2
V+:
item-a
item-b
item-c
Avoid inline V+(...) / M+(...) when a +[...] / +{...} literal
is equally clear.
Avoid defensive :V
Do not add :V just because a value is lazy or because the next form
will iterate it. Prefer leaving sequence-producing calls as sequences,
then run the program without materializing first.
Add :V only when the program needs vector behavior:
- indexed access with
.N/.$i - vector-style
conjorder - repeated traversal where laziness would be surprising
- output must visibly print as a vector
- a later operation specifically requires an indexed collection
When unsure, remove :V and run the program. Keep it only if the
program fails or the output semantics change.
Prefer direct splat calls over apply
YS 0.3.2 can splat collection-producing expressions directly into a
call. When the collection is already a value or fits naturally in a
scalar expression, call the target function and append * to the
collection instead of wrapping the call in apply:
apply(max xs)→max(xs*)apply max: xs→max: xs*apply(min-key score xs)→min-key(score xs*)apply(max 0 map(count lines))→max(0 map(count lines)*)apply(concat groups)→concat(groups*)apply f: args→f: args*
The splatted argument can be a variable (xs*), a parenthesized
expression (([f] + args)*), a call result (make-args()*), a dot-chain
(rows.map(count)*), or a colon-chain (freqs:vals*). Regular arguments
may appear before it, and calls may contain multiple splats.
For operator functions, use a named callable such as add(xs*),
mul(xs*), or le(xs*); a parenthesized operator head such as
(+ xs*) also works.
Use :join / join: when the operation is conceptually joining strings:
apply(str pieces) can become pieces:join. Use str(pieces*) when the
variadic str call itself is the clearer expression.
Keep apply when the collection is naturally produced by an indented
block and forcing it into a scalar expression would make the code less
clear:
apply max-key last:
map _ xs:
fn(x): score(x)
Prefer :S colon-chain over str(bareVar)
Single-argument str(x) where x is a bare identifier has a terser
colon-chain form x:S. Use that:
str(c)→c:Sstr(n)→n:S
:S reads as "convert to String" — that's exactly what the call is
doing. Use it whenever the intent is stringification.
Don't rewrite str(bareVar) as "$bareVar". Interpolation is
for composing a string from parts, not for stringification — even
when the result happens to look the same. And the two are not always
equivalent: at the interpolation boundary the value's original type
can leak through (a Character can come back as a Character),
whereas str(x) and x:S always produce a real String. The
difference shows up in places where the result is used as a map
key, a regex operand, or an in? test — Soundex's code-map
lookup is a real example where "$c" misses entries that c:S
finds.
str(...) with multiple args (e.g. str(a b c) for concatenation) is
unrelated — keep it. The rule is only about the single-bare-var case.
Prefer .! over :zero? when falsey-zero semantics are OK
YS falsey semantics make numeric 0 false, so .! is usually the
terser way to test "zero result" in numeric code. Prefer it for counts,
remainders, and loop totals when the value is known to be numeric:
(i % 5).!over(i % 5):zero?count.!overcount == 0orcount:zero?total.!overzero?(total)
Use :zero? / zero?(...) only when you specifically need the
stricter predicate and must distinguish numeric zero from nil, false,
empty strings, or empty collections.
Use conditional assignment for "if true update, else keep same value"
YS 0.3.2 supports conditional assignment targets. When an assignment would set a target to a new value only when a condition is true, and otherwise keep the same target value, put the condition in the target:
foo :if pred =: bar
This means "if pred, assign bar to foo; otherwise keep foo".
Prefer it over the verbose self-fallback shape:
foo =:
if pred:
then: bar
else: foo
The same syntax works with update assignments:
count :if prime?(candidate) +=: 1
over:
count =:
if prime?(candidate):
then: count.++
else: count
It also works with destructuring and functional/modified assignment
operators (+=:, *=:, ||=:, .=: etc.):
a b c :if (x == y) =: d e f
total :if include? +=: n
data :if found .=: assoc(k v)
The condition after :if must be a single form. Parenthesize compound
conditions: a b :if (x == y) =: c d, not
a b :if x == y =: c d.
else: not do: for the else branch of if
When the then-branch is a single form and the else-branch is multiple
forms, introduce the else block with else:, not do:. do: compiles
but is not idiomatic.
when/when-not for one-armed conditionals returning nil
If a branch of if or cond returns nil, the conditional is really
one-armed — use when (or when-not) instead. when returns nil when
the test is false, so the explicit nil branch is dead weight. A cond
with one real arm and a nil fallback is the loudest version of this
mistake.
cond: x.!: nil / else: real→when x: realcond: m: i / else: nil→when m: iif cond: form / else: nil→when cond: formwhen X.!→when-not X
When an if has then: false, consider reversing the condition and
using when for the else branch. when returns nil when its test is
false, and nil often works anywhere false was only used as "no result":
if letters.# < 2:
then: false
else:
every?: ...
can become:
when letters.# >= 2:
every?: ...
Only make this rewrite when nil is acceptable in place of false. Keep
the explicit if when callers require a strict Boolean false.
declare is not needed in YAMLScript
YS resolves defn references across the whole file, so mutual
recursion works regardless of definition order. Don't reach for
declare: name — it's a Clojure habit and adds noise:
# correct — F is defined first and references M defined later
defn F(n):
if n.!: 1 (n - M(F(n.--)))
defn M(n):
if n.!: 0 (n - F(M(n.--)))
No reserved symbols in YS or Clojure
Any symbol can be used as a local binding. Names that shadow stdlib
functions (next, count, key, name, val, type, class,
first, last, rest, map, line, done, etc.) are fine. Don't
invent abbreviations like nxt, cnt, k, or done? just to avoid
the stdlib name.
# correct
next =: next-board(b)
when next: recur(next)
# wrong reaching for `nxt` to avoid shadowing `next`
nxt =: next-board(b)
when nxt: recur(nxt)
Pick the clearest name from the domain. The only reason to avoid a particular symbol in a scope is if you need to use the original value in that same scope.
Style Defaults
The choices below have no single right answer in YAMLScript. The skill
ships with the defaults listed here, but they are overridable from a
project's AGENTS.md. If a project's AGENTS.md contradicts a
default, follow the project.
These are stylistic only — anything in Common Mistakes, Key Rules, or Anti-Patterns is not negotiable.
Prefer subject-first chains over nested calls
When a function has an obvious "subject" argument (the thing being
extended, transformed, queried, or tested), prefer the subject-first
chain form over a nested function call. Less parenthesizing is usually
better, and subject:op / subject:op1:op2 reads left-to-right:
n:random-bracketsoverrandom-brackets(n)s:balanced?overbalanced?(s)s:seqoverseq(s)brackets:seq:shuffleovershuffle(seq(brackets))m.assoc(:k v)overassoc(m :k v)when args are neededxs.conj(x)overconj(xs x)when args are neededs.split('/')oversplit(s '/')when args are needed
Use colon chains for zero-argument calls and subject-first unary calls:
b:a is preferred over a(b), and c:b:a is preferred over
a(b(c)), when the chained order is the natural data flow.
Use the bare-function form when arguments are co-equal (e.g.
merge(a b c), concat(xs ys zs)) or when there is no natural
receiver.
To override: in AGENTS.md, write "prefer bare-function form
(assoc(m k v)) over receiver-first chains".
Vectors of short strings
For a static vector of short word-like strings, prefer qw(a b c)
over =:: ['a', 'b', 'c']:
colors =: qw(red green blue)overcolors =:: ['red', 'green', 'blue']
qw produces a vector of strings. Use the data-mode literal when the
elements contain spaces or non-word characters.
Default argument values
For a defn arg with a long default value, prefer setting it in the
body with ||=: over a long signature line:
defn main(text=nil):
text ||=: 'The quick brown fox jumps over the lazy dog'
over
defn main(text='The quick brown fox jumps over the lazy dog'):
Short defaults (numbers, short strings, keywords) belong in the
signature: defn main(n=10):.
Block form vs chain for multi-arg calls
For a call with three or more substantial args, prefer block form with one arg per line over a single-line chain:
concat:
quicksort(less)
vector(p)
quicksort(more)
over
concat: quicksort(less) vector(p) quicksort(more)
Two args fit fine on one line.
Key Rules
Formatting
- Lines must not exceed 79 columns. This is a hard limit, not a
suggestion. Target 20/40/60 columns as the natural "square" sizes for
most lines. YAML/YS gives you many ways to split:
- Block form: replace a chain with an indented block
- Intermediate variables: assign a sub-expression to a name
- Plain scalar folding: a plain (unquoted) YAML scalar folds at any
whitespace — break before a binary operator and indent the
continuation:
user =: ENV.RC_USER || die('set RC_USER (botpassword username)') - Double-quoted line fold: a
"..."string can be split at any space — YAML folds the newline (and the continuation's leading whitespace) into a single space. Indent the continuation to read cleanly:say: "map my-add over pairs: $(map(my-add [1 2 3] [10 20 30]):joins)" - Double-quoted backslash continuation: a
"..."string can be split with\at end of line, even when there's no whitespace to fold at. Useful for long URLs, identifiers, or any unbroken token:url =: "https://en.wikipedia.org/w/api.php?action=query\ &titles=Rosetta_Code&format=json" - Block scalars (
|,>): for multi-line literal text
- End the file with exactly one newline. No trailing blank line.
The last byte should be one
\nafter the last code line, not two.
Strings
- Single quotes unless interpolation or escapes needed
"Hello, $name!"notstr('Hello, ' name '!')"Result: $(x * y)"for expression interpolation"Now: $now()"for a bare function or method call. The shortened$ident(args)form works for plain identifiers (letters, digits, underscore, hyphen) and static calls like"$System/currentTimeMillis()". Prefer it over$(ident(args))when the call is a single function or method on a bare name.- Interpolation stops parsing the identifier at
?or!, so predicate names break the shortened form: write"$(all-equal?(xs))", not"$all-equal?(xs)"(which interpolates only$all-equaland leaves?(xs)as literal text). Reach for$(...)for operators, chains, anything beyond one call, or any identifier containing?or!. say: |with a multi-line block — all lines interpolated and printed::(double colon) is sugar for!(mode-toggle tag).a:: b=a: ! b— toggles between code and data mode:- In code mode (default
!ys-0),::switches value to data - In data mode,
::switches value back to code say:: hello— data mode: literal string"hello", not variable lookup (quoted'hello'is already literal either way)say:: |— data mode: literal block scalar (no interpolation)json/dump::with indented YAML — build data structures natively instead ofjson/dump: +{...}with escaped mapshttp/post url::— pass YAML maps as options- Inside a
::data block,key:: exprtoggles back to code:model:: model= YAML keymodelwith the value of variablemodel content:: |with$var— block scalar with interpolation::only works on mapping pair values (key-value syntax). For sequence entries, use the explicit!tag:- ! exprto evaluateexpras code within data mode
- In code mode (default
:::enters code-value mode for a mapping or sequence value. Collection structure and mapping keys are data, while scalar values and sequence elements are code:
Code-value mode recurses through block and flow mappings and sequences. It can be entered from code mode, data mode, or code-value mode. A nestedresult =::: first: foo() nested: second: bar() items: - foo() - bar():::is therefore valid but redundant. The value after:::must be a mapping or sequence; a scalar is an error. Usekey:: valueto make an entire mapping value subtree data again. Use- ! valuefor the same data override on a sequence element. Inside an overridden data subtree, ordinary::re-enters code mode. An unmarked mapping is always structural in code-value mode, so write block-form code as a scalar call to a helper function instead. Prefer:::when most values in a collection are computed. For a mostly literal collection with only one or two computed values, data mode with::on those values is often clearer.- In code-value mode,
:when condition:::conditionally splices mapping entries into the surrounding map:
When the condition is false, the spliced entries are absent. The remaining entries keep their source-key insertion order. The triple colon is required so the splice body is also code-value mode. Do not put block-form code under an ordinary key because that nested mapping is structural. Use a conditional splice for an optional key, or compute the value in a helper or intermediate variable and use that scalar result.result =::: always: current :when include-extra::: extra: make-extra() !<fn>tag — avoids an extra indent level:each i xs: !sayinstead of nestingsay:as a separate pair inside the body- CLI args that look like numbers are auto-converted —
num()not needed. Do NOT defensively coerce with:int/:Neither.defn main(n=10):is enough;ethiopian(a b)works with no coercion whena/bcame from the command line. Two globals expose the raw and converted views:ARGV— all CLI args as raw strings (no conversion)ARGS— all CLI args with numeric-looking values converted UseARGVwhen the task is about string handling of numeric-looking input (e.g. "increment a numerical string"); otherwiseARGSand named/positional params with defaults are fine.
+for simple concatenation at end of dot chain, notstr()n * 'str'— integer times string repeats it:n * ' 'for an indent ofnlevels. Replacesapply: str repeat(n ' '). Order doesn't matter for this case:'str' * nalso works.- Interpolation auto-stringifies —
"$x"works for any value, nostr(x)needed inside"..."or$(...) uc1(s)— capitalize first character;uc(s)— all uppercasejoin(sep coll)— join with separator;join(coll)— no separator. Alsocoll:join(colon chain) andjoin: coll(pair form) for the no-separator variant. Prefer these when the operation is conceptually joining strings:apply(str chunks)→chunks:join. When preserving the variadic call is clearer, use a direct splat:str(chunks*).joins(coll)— join with a single space.xs.join(' ')is alwaysxs:joins— the colon chain says "join with one space" directly.say: row.join(' ')→say: row:joins.splitandjoinhave their own arg-swapping (not in DWIM list)qw(word1 word2 ...)— quoted word list; creates a vector of strings without needing quotes around each wordwords(s)— split string on whitespace; colon chain:text:wordslines(s)— split string on newlines; colon chain:text:linesin?(x coll)— membership test; works on strings, vectors, sets, maps. Dot form:w.in?(fruits). Flipped:has?(coll x)replace(s pat repl)— replace all matches; supports$1groupsreplace(s pat)— remove all matches (replacement defaults to"")replace1(s pat repl)— replace first match only- In scalar expressions, escape YAML-special sequences:
:\→ literal:(colon-space would trigger colon-chain)\#→ literal#(space-hash would start a YAML comment)
File I/O
- Never use
slurp/spit- these are the Clojure names and YS does not expose them. It spells themreadandwrite, the only idiomatic forms:read(file)— read a whole file to a string (wasslurp). Colon chain:file:read, e.g.FILE:read:lines.write(file content)— write a string to a file (wasspit).
FILEis bound to the running program's own source path, handy for a program that reads data embedded in itself.
Comments
# ...— standard YAML comment to end of line. Use it between structures, after values, and at file top/bottom.- A
#comment terminates the surrounding YAML scalar, so it cannot appear inside a multi-line plain-scalar expression such as a dot-chain spread across lines. \"..."— YS expression-level comment. Opens with\", closes at the next". Use it to annotate steps inside a multi-line expression where#would break the scalar:defn scramble(s): s \"input string" .lc() \"lowercase" .split() \"split into chars" .shuffle() .join() .uc1() \"capitalize first char"\"..."constraints:- The body cannot contain
"(no escape mechanism). - The body is still lexed by YAML, so YAML-special sequences must
be escaped the same way they are in any plain scalar:
:\for:(colon-space),\#for#(space-hash). See Strings. - Not usable inside YAML string literals (
"...",'...') or regex literals/.../. - Not usable as a standalone YAML block element — only inside an in-progress multi-line expression.
- The body cannot contain
Function Definitions
defn name(args):form with parens- Default args over multi-arity:
defn greet(name='World'): - For multi-line default text, use a top-level block scalar variable:
Avoids the YAML plain scalar restriction that forbidsdflt =: |- line one line two defn main(text=dflt)::inside default values written as\n-escaped double-quoted strings defn-for private helpers- Destructuring in parameter lists:
defn score([a b]):bindsaandbto elements of a pair argument — saves an intermediatea b =: pairline. Works for bothdefnandfn. mainwith default args for CLI programs. Defaults should be values a user could actually type on the command line — strings and numbers, not vectors or maps. Ifmainneeds a collection, default a string and parse it in the body (seeARGV/ARGS).mainarg-list shapes:main(name)— exactly one named arg (auto-converted if numeric)main(_)— exactly one arg, unnamed (arity matters, name doesn't)main(*)— any number of args, unnamedmain(*args)— any number of args, named
- Define functions top-down:
mainfirst, then helpers in call order — this is idiomatic YAMLScript
Function Calls
- Top level: mapping pair —
say: 'hello' a: b c≡a b: c≡a b c:— the colon splits a call into before/after segments; choose the split that reads naturally. Promote the "subject" of a call before the colon when it makes the call read like English:write file: contentnotwrite: file contentassoc m: k vnotassoc: m k vjoin: brackets:seq:shufflenotjoin: shuffle(seq(brackets))
- Prefer colon-chain composition for unary pipelines. Write
x:b:ainstead ofa(b(x))when each step takes the previous value as its only argument. This keeps the data flow left-to-right and avoids nested parentheses. - Higher-order function calls — when the function arg is named,
put it on the key side; the data flows to the value:
reduce f: init coll— reducing withfoverinit/collmap double: coll— mappingdoubleovercoll
- Inline-defined function for an HOF — put
_where the function arg goes; define the function as the block value:
This works for any HOF:reduce _ init coll: fn(acc x): ...body...map,filter,reduce, etc. The block value substitutes at the_. - Inline: YeS form —
inc(x)not(inc x) - Prefer
a.b(c)overb(a c)— dot chain from the receiver unless the receiver needs escaping ({},[],"",'') - Scalar
if: dot-chain the condition before it —cond.if(then else)notif(cond then else) X OP: Yat the pair level is sugar forX OP Y, for any binary operator.a +: b≡a + b;(cond) &&: body≡cond && body(body only runs ifcondis truey);(cond) ||: body≡cond || body(body only runs ifcondis falsey). The&&:/||:forms overlap functionally withwhen/when-notbut the mechanism is the operator's short-circuit, not a control structure. ScalarYmust contain exactly one form; a compound expression counts as one form. Mapping values keep their mapping and form-map semantics.
Control Flow
-
if <cond>: <then-form> <else-form>— always needs both forms.ifis the default for two-branch conditionals. Reach forcondonly when there are 3+ branches — see Common Mistakes. -
Use
whenfor one-armed conditional (no else);when-notis the inverted form (when-not X≡when X.!). See Common Mistakes for when to choosewhen/when-notoverif/cond. -
when+ expr:— likewhen, but binds_to the truey value ofexprinside the body. Use it to test-and-capture in one step:when+ schema.'$ref': say: "-type: $(ref-sym(_))" -
.when(value)— receiver acts as the test; returnsvalueif truey, else nil. Replaces the.if(value nil)pattern:only-ref?(s).when(ref-sym(s.'$ref'))notonly-ref?(s).if(ref-sym(s.'$ref') nil) -
condreturns nil when no clause matches — drop trailingelse: nil -
caserequires an explicitelse:default arm. Unlikecond(returns nil),casethrowsNo matching clause: <value>if no arm matches. A bare trailing form is parsed as anotherkey: actionpair, not a default —else:is required. -
ifaccepts three shapes:- form / form — two consecutive pairs, no keywords:
if cond: \n say: yes \n say: no - block / block — both
then:andelse:required; usingthen:forceselse: - form / block — bare then-form followed by an
else:block. Do NOT usedo:for the else block —else:is the idiomatic keyword (see Common Mistakes).
- form / form — two consecutive pairs, no keywords:
-
When both branches are simple, prefer the tersest fit:
- Single-line pair:
if cond: a bwhen both forms parse as a single plain scalar — e.g. bare symbols, function calls, ranges:if v == v2: v recur(v2),if x:odd?: print('o') print('e'). - Single-line with
+escape: when the first form starts with a YAML syntax char (',",[,{, etc.), add+to the front:if x:odd?: +'odd' 'even',if found: +match 'none'. - Chain form:
cond.if(a b)when the condition reads well as a receiver and you're not already in a mapping-pair context:x:odd?.if('odd' 'even'). - Two-pair form (newlines): when either branch is too long to
inline, fall back to
if cond: \n a-form \n b-form.
- Single-line pair:
-
Consider reversing the condition to avoid
then:— complex branch first (no keyword), simple branch aselse:— often cleaner -
elsenot:elseincond -
eachoverdoseqfor side-effecting iteration -
dotimes [_ n]:— repeat n times ignoring the index; clearer thaneach [_ (1 .. n)]:when you don't need the iteration value -
loop i 1, acc 0:— loop with named bindings (no surrounding brackets). The bracket-free form is the canonical style for binding-list forms in YS — never write the bracketed Clojure form when you can avoid it. Reliably strippable:binding,if-let,if-lets,if-some,let,loop,when-first,when-let,when-lets,when-some,with-open. For these,KW [a b c d]:is always wrong — writeKW a b, c d:(comma-separated pairs) orKW a b c d:(no commas) instead. Userecurfor tail recursion back to the loop head. -
Iteration keywords (
each,for,doseq,dotimes) also take the bracket-free binding form. A name-value binding list drops its brackets regardless of the value — even a parenthesized range or a vector literal:each [y (0 .. h)]:→each y (0 .. h):for [i (0 .. n), j (i .. n)]:→for i (0 .. n), j (i .. n):each [c [true false]]:→each c [true false]:
Two cases keep their brackets:
- Ignore-var binding whose variable is
_:dotimes [_ n]:,each [_ (1 .. n)]:. The_form must stay bracketed. - Destructure shortcut where a pattern binds each element of one
collection:
each [a b] coll:,each [k v] m:. Here[a b]is a destructure pattern (followed by the collection), not a binding list, so the brackets stay.
The
iter-bracketslint rule flags only the first case — a bracketed binding that starts with a real (letter-leading) variable name and ends the binding (...]:). It leaves_-var and destructure forms alone. -
recur— tail-call back to enclosingloopordefn; multi-arg form:recur: arg1 arg2orrecur arg1: arg2 -
forbody can be bare scalar —=>:not needed
Chaining vs Variables vs Block Form
Prefer block form — it often adds clarity that chaining hides. Do not default to chaining just because it is possible. Chaining is fine for short, obvious pipelines; block form is better for anything non-trivial, especially iteration and nested logic.
Avoid over-chaining. A long dot chain on one line is hard to read. Aim to keep chained lines short — 20-60 columns is the natural "square" range. Never exceed 79 columns (see Formatting in Key Rules).
Options when a chain gets long:
- Use block form — nest the argument as an indented block
- Assign intermediate results to named variables
- Split before
.call(onto continuation lines (but not before:call) - A YAML plain scalar can be folded onto multiple lines at any
whitespace — the value is still a single expression. For readability,
put a binary operator like
||or&&at the end of the first line and indent the continuation:
This is a stylistic choice, not a syntactic rule.user =: ENV.RC_USER || die('set RC_USER (botpassword username)')
Example — chained vs block form for iterating with a nested function:
# Chained — terse but opaque
say: fn([x] sum(digits(x))).iterate(n).drop-while(\(_ >= 10)):first
# Block form — each step is named, reads top-to-bottom
defn main(n=493): !say
first:
drop-while ge(10):
iterate _ n:
fn(x):
sum: digits(x)
# Middle ground — intermediate variable + short chain
words =: text:lc.split(/\s+/)
pairs =: words:frequencies.sort-by(val):reverse
Operators & Chaining
-
Binary operators require whitespace on both sides —
1 .. 5not1..5;a + bnota+b;a * bnota*b. This applies to all binary operators:..+-*///||&&=~!~%%%**etc. Omitting whitespace may sometimes work but is not idiomatic and may break in future versions. Exception:.(dot chain) does not need whitespace. -
Do NOT mix different operators without parentheses:
a * b * c— OK (same operator)a * b + c— NOT OK(a * b) + c— OK
-
A chain of the same comparison operator means variadic — not nested:
a >= b >= cis(>= a b c), meaninga ≥ b AND b ≥ c, not(a >= b) >= c. Same for==,<,<=,>,!=. -
Do not parenthesize a binary expression that stands alone as one side of a key/value pair. The pair itself delimits the expression, so wrapping parens are pure noise:
(r > 180): r - 360→r > 180: r - 360x =: (a * b * c)→x =: a * b * ccs =~ /[0-9]/: I(cs)(already correct — no parens needed)
This applies to both sides of the pair. Parens are still needed when the expression is not standalone — e.g. when it feeds a chain like
(d < 10).if(...)or groups mixed operators like(dir == 'w') && (row > 0):.The same rule applies to the test position of every conditional / loop control form:
if,if-not,when,when-not,while. Each takes its test as a standalone pair-key expression, so the parens are noise:if (x > 0):→if x > 0:when (xs.empty?):→when xs.empty?:while (running):→while running:
Exception: keep the parens around Clojure/Java interop special forms like
(. IN ready)or(. obj method arg). Interop is rare, and the parens make the special form clear:when (. IN ready):(correct)when . IN ready:(avoid)
This is the analog of the bracket-free binding-list form for binding-bearing keywords — same idea, but for the keywords that take a single test expression instead of a binding list.
-
..for inclusive ranges, notrange -
rng(x y)— use only for char ranges:rng(\\a \\z). For integer ranges, always use..:1 .. 5(forward) or5 .. 1(reverse).\\ais a Clojure char literal (backslash doubled in YAML block scalars);C('a')also works but is verbose. -
%=rem(remainder);%%=mod— prefera % boverrem(a b); they differ only for negative numbers -
//=quot(integer division, truncated toward zero) — prefer the operator over the call/method forms:a // boverquot(a b)ora.quot(b)- Works the same as Python's
//for non-negative operands; for negatives it truncates toward zero (Python floors). Use this for division-by-base extraction (n // 10), midpoint computation ((lo + hi) // 2), etc.
-
.!for falsey check (falsey?) — YS truth: 0 and empty collections are also falsey.x.!combines nil-check and empty-check in one, and is also the usual short form for numeric zero tests. Use:zero?/zero?(...)only when strict numeric zero semantics matter — see Common Mistakes. -
Dot chaining for calls with args:
s.replace(/x/ ''),s1.anagram?(s2) -
Colon chaining for zero-arg calls:
s:lc:frequencies:reverse -
obj.name(dot without parens) is a property/key lookup — NOT a function call. Use:nameto call a zero-arg function by name. Example:.first→ key lookup (nil);:first→(first obj)(correct) -
obj.'key'— quoted property lookup for keys that aren't valid bare identifiers (start with$, contain-, etc.):schema.'$ref',schema.'$defs'notschema.get('$ref') -
obj.$var— dynamic property/index lookup; the runtime value of$varis used as the key. Works on maps (m.$key) and vectors (v.$i). Useful when the key/index is computed:key =: "${typ}token" data: .query.tokens.$key # map lookup word =: words.$idx # vector indexOnly takes a bare variable — for computed indices use
.nth(expr):v.$(i - 1)does not work; writev.nth(i - 1)instead. Inside a dot chain that starts the value, split withdata: .query.tokens.$keyrather than=>: data.query.tokens.$key. -
Dynamic method/function calls can use the same lookup in call position:
a.$name(b)calls the member selected by runtime variablename. This is the direct YS counterpart to "send an unknown method call" examples such as Perl's$obj->$name($arg). If the local test compiler parses that as lookup plus a separate argument form, use the equivalent two-step shape:method =: a.$name method: b -
obj.N— literal-index lookup (the property name is the literal number). Works on vectors and strings:v.0,v.12,s.3. Use this for any constant index —v.nth(N)is verbose for literals. Inside\(...)lambdas,_.0,_.1,_.2, ... index the implicit arg. -
Choose the right index form:
- literal index →
v.N(e.g.v.0,emp.2) - bare variable →
v.$var(e.g.v.$i,units.$idx) - computed expression →
v.nth(expr)(e.g.v.nth(i.--),v.nth((row * 4) + c),v.nth(w - wt)) Never write.nth(N)or.nth(bareVar)— the dot/dollar forms are tighter.
- literal index →
-
Property lookup is nil-safe —
nil.fooreturnsnil(no NPE). A chain likedata.error.codeyieldsniliferroris missing, so you don't need to guard each step. Combine withwhenfor presence checks:when err.code == 'maxlag': ...works even whenerritself isnil. -
Special postfix operators are NOT property lookups — they compile to explicit function calls and work in string interpolation:
.++=inc+,.--=dec+,.#=count,.!=falsey?,.?=truey?,.$=last,.@=deref,.>=DBG,.??=boolean,.!!=not. Example:$(i.++)=inc+(i),$(xs.#)=count(xs) -
Postfix forms compile to the polymorphic
ys.std/<op>+variant (.++→inc+,.--→dec+, etc.). Prefer them for readability, and especially in argument positions:m.--.ack(1)chains cleanly, whereasack((m - 1) 1)needs paren-grouping som - 1doesn't read as two args. In a tight numeric loop where raw speed dominates, fall back to the colon-chain forms:inc/:decwhich callclojure.core/inc/clojure.core/decdirectly. -
Use
:sqrfor** 2and:cubefor** 3—x:sqrnotx ** 2,x:cubenotx ** 3. Works in any position:1.0 / _:sqr,n:cube + 1, etc. -
\(_ * 2)for inline lambdas — prefer overfn([x] x * 2)for single-expression bodies. Anonymous-function arguments are_for the first argument and_1,_2, etc. when numbered positions are needed. YAMLScript 0.3.2 no longer accepts%,%1,%2, etc. as anonymous-function arguments;%remains the binary remainder operator. Usefnonly when you need destructuring or multiple named args. Neverfn(x): body— invalid inline (:splits the expression) -
_placeholder when collection arg should come last in a chain, or to mark where a block value will be substituted -
DWIM auto-placement: the following functions detect arg types at runtime and swap order when needed — chain from any arg naturally:
applychopconscontains?dropdrop-lastdrop-whileevery?escapefilterfiltervformatinterposekeepmapmapcatmapvnot-any?nthpartitionrandom-samplere-findre-matchesre-seqreduceremoverepeatreplacesomesortsort-bysplit-atsplit-withtaketake-lasttake-whileFor functions NOT in this list, put the collection as receiver or use_. For performance-critical code, use_to skip the check. -
map-indexed(f coll)— not DWIM; use_placeholder:coll.map-indexed(f _)orcoll.map-indexed(vector _) -
group-by(f coll)— group items by function result -
partition-by(f coll)— split when function result changes -
grep(P C)— not in DWIM list but has own arg-swapping; P can be a regex (re-find), function (filter), or value (=); chain naturally:coll.grep(regex),coll.grep(fn?),coll.grep(val) -
qr("pat")— build a regex from a string (with interpolation):qr("[$chars]"). Use when/.../literals can't help (they don't interpolate). Preferqrover Clojure'sre-pattern. -
starts?(s prefix)/ends?(s suffix)— string prefix/suffix tests; dot form:s.starts?(prefix),s.ends?(suffix) -
ais YS's alias foridentity— returns its argument unchanged. Useful as a no-op transform (map a coll) or to satisfy a callback signature. -
For atoms, use the bangless aliases
swapandresetinstead of Clojure'sswap!andreset!.swap! buf: constantly(b2)becomesswap buf: constantly(b2);reset! a: 0becomesreset a: 0. -
Named operator functions — usable as first-class values or via dot syntax (e.g.
6.mul(7)):- Arithmetic:
addsubmuldiv - Comparison:
ltgtlegeeqne - Logical:
andor; YS truth variants:and?(&&&),or?(|||) — use YS falsey semantics (0/empty = false).(a ||| b)= useaif truey, elseb(likea || bin JS) For simple inline logic, prefer symbolic operators:a || boveror(a b), anda && boverand(a b). - Regex:
s =~ /pat/for match,s !~ /pat/for no-match - For simple inline comparisons, use symbolic operators directly:
limit > 2notlimit.ge(2). Named forms (ge,lt, etc.) are primarily for creating predicate values to pass to higher-order functions likefilter,drop-while,take-while. - Called with 1 arg, comparison operators return predicates:
ge(n)→(fn [x] (>= x n)),lt(n)→x < n, etc. Useful withfilter,drop-while,take-while,remove
- Arithmetic:
-
f * g— left-to-right function composition; appliesffirst then passes result tog. Example:first * say= get first, then print it. In block form, combine consecutive single-word keys with*to avoid nesting:lc * say: valueinstead ofsay:\n lc: value -
f + arg— whenfis a function,+partially applies it:map + uc1=(partial map uc1), a function that mapsuc1over a collection. Combine with*:(map + uc1) * joins * say -
f(coll*)— splat spreads collection as variadic args:min(nums*) -
.#— count/length operator, shorthand for:count -
:S— convert to string (alias forstr) -
:V— convert to vector (alias forvec) -
:V+— wrap as vector (alias forvector);x:V+≡[x] -
:I— parse string to integer (alias forparse-long) -
:N— parse string to number, int or float (handles'42'and'2.1415') -
_.0,_.1— indexed access on implicit lambda arg_ -
x.(f*)— shorthand forx.apply(f) -
x OP=: expr— augmented assignment, sugar forx =: x OP expr. Works for any binary operator:.=:(chain into receiver),+=:,*=:,||=:, etc. Example:nums .=: words().map(N)replacesnumswithnums.words().map(N).
Values & Data
-
For purely literal collections (no code inside), prefer the data-mode toggle
=::over+-escaped code-mode literals. YAML is good at data; let it do that work:a =:: [1, 2, 3]— flow seq, data mode (preferred for literals)a =: +[1 2 3]— code-mode vector literal (use when the collection mixes in computed values, e.g.+[0] + row)
-
+escape — needed only when the first non-space character of a YAML value would otherwise be a YAML syntax character ([,{,",',|,>,!,&,*). It forces the entire value to parse as a single plain scalar; YS then strips the+and reads the rest as code. The+escape must be at the front of the value and must be followed, possibly after whitespace, by one of those YAML syntax characters. If the next meaningful character is a letter, digit,_,(, or other expression character, the+is not an escape; it is just the plus operator.Two distinct reasons
+may be needed:- YAML-invalid without it.
key: 'a' 'b'— YAML sees'a'end and'b'dangle.key: +'a' 'b'makes the whole+'a' 'b'a plain scalar. - YAML-valid but YS-rejected.
key: [b c]— valid YAML (flow sequence value), but YAMLScript forbids flow collections and block sequences at code-mode value positions by design. Code mode only needs scalars and block mappings; flow forms are reserved for use as vector/map literals via+-escape. Sokey: +[b c]is the canonical form.
+is only needed at the START of a value. Once the value is a plain scalar expression, flow forms inside it are fine as arguments:foo([b c]),map(double [1 2 3]),assoc(m :k [1 2])all parse without+. The brackets are mid-expression, not at the value start. Do not carry the+from+[...]into YeS expression position.+works ONLY at the very start of a value plain scalar — anywhere else in an expression,+is addition/concatenation:+[1 2 3]— escape: vector literal+"hello" + "world"— escape on leading", then+is concat+[0] + row— escape on leading[, then+is concatf([1 2 3])— no escape needed inside the callf(+[1 2 3])— WRONG:+is addition, not an escapesieve(xs) +[]— NOT an escape: meanssieve(xs) + [](vector addition, a no-op)- Whitespace after
+is fine — useful for multi-line expressions:foo =: + [a] + [b]
- YAML-invalid without it.
-
Keyword keys need
:prefix:+{:name "Alice", :age 30} -
Flow maps need commas:
{a: 1, b: 2} -
Set literals: write
\{a b c}, not Clojure's#{a b c}(the#starts a YAML comment). Use\{}for an empty set.hash-set(...)also works but is verbose:seen =: \{}— empty sets =: \{:a :b :c}— three-element set
-
Special float literals: write
\\Inf,\\-Inf, and\\NaNfor positive infinity, negative infinity, and NaN. The\\escape stands in for#(Clojure's##Infsyntax), which YAML would otherwise treat as a comment. -
Single-character casting functions in
ys.std:Fn Casts to Fn Casts to B Boolean M Map C Character N Number D Atom deref O Ordered map F Float S Set (not String) I Integer T Type-name string K Keyword V Vector L List L+,M+,O+,V+are variadic variants that build the collection from multiple args. Prefer single-letter cast forms over long Clojure names:I(sqrt(n))is idiomatic. For collection literals, prefer+[]/+{}over inlineV+(...)/M+(...)unless the constructor is clearer as the pair key (V+:/M+:). -
=:for assignment (replacesdef/let) -
x y =: 6 7for multiple assignment -
=>:only when no pair form works: bare identifiers, atoms, interpolated strings, data-collection literals (+[1 2 3],+{a: 1}). For compound expressions, restructure into a pair — fn-callf: args, chainx: .m(a), or opa +: b. See Common Mistakes. Exception: never use=>:as a directifbranch key; writethen:orelse:instead. -
? expr : value— YAML complex key syntax; lets a multi-line expression serve as the key of a mapping pair. Useful when a pipeline is too long to fit before the:of a block pair:? each row next-row .iterate([1]) .take(n) : say: row:joins -
Inside YeS expressions (inside parens),
[...]needs no+escape
Do Semantics
- Top-level,
defn,fnbodies have implicitdo— rarely needdo:explicitly - YS code blocks are ASTs not mappings — duplicate keys are valid
Eval
eval(s)/s:eval— parse a string as YS source and run it, returning the value of the final expression. Useful when user input must execute as code: in a 24-game task, the player's expression'(8 - 2) * (7 - 3)':evalreturns24. The string is unrestricted YS, not a sandboxed arithmetic subset, so use it only on trusted input.
I/O, System & Namespaces
- Bundled namespaces such as
ys::fs,ys::str,ys::http, andys::yamlare unavailable until loaded withuse. Never userequire; it is retired and only reports a migration error. - A host can set
YS_MODULESto a comma-separated or whitespace-separated allowlist of bundled modules. Code must handle a disabled module as unavailable. WASI builds always disableys::fs,ys::http,ys::ipc, andys::pods. - Prefer one grouped mapping when loading multiple modules:
use: fs: str: - A bare short name expands under
ys::and receives the same alias.use: http fs ipc ysloads four aliases. A short name with options does not imply an alias, souse http: :allimports all ofys::httpwithout anhttpalias. - Plain
use ys::str:loads the module for qualified access asys::str/upper-casewithout referring its names. Use:asfor an alias,:getfor selected names,:allto refer all names,:notto exclude names from:all, or:noneas an explicit spelling of qualified-only lo
Truncated - read the full file at https://github.com/yaml/yamlscript/blob/010627630fb32d5d1a9a7d0e6fbe80e0e1b20856/ai/codex/.agents/skills/yamlscript/SKILL.md.
