name: nimonyplugins description: Build and debug Nimony compile-time plugins for code generation and DSL rewrites. Use when replacing Nim macros with Nimony plugins, writing or reviewing plugin-backed templates or module/type/import plugins, fixing generated plugin output, or diagnosing NIF traversal and source-level plugin errors.
Nimony Plugins
Use this skill when writing or reviewing compile-time rewrites for Nimony.
Plugins are the Nimony replacement for macros. For new compile-time DSL rewrites, use a plugin-backed template such as template foo*(spec: string): untyped {.plugin: "fooplugin".}. Do not write Nim macros.
Nimony plugin code targets the plugins module, not Nim macro APIs.
Rules
Setup
- Resolve the nimony executable:
readlink -f "$(command -v nimony)". - If the resolved path ends with
/bin/nimony, open../src/nimony/lib/plugins.nimfrom there. - Otherwise open
src/nimony/lib/plugins.nimunder the executable's directory. - Plugin modules are compiled with Nimony itself. The compiler invokes a separate
nimony cinvocation per plugin and caches the result. - The plugin path in
{.plugin: "path"}is relative to the directory of the source file that contains the pragma, not the call site.
Plugin Kinds
There are four kinds of plugins. All share the same plugins API.
- Template plugin:
template foo(...) {.plugin: "path".}— invoked at each call site. The input is wrapped in aStmtsSnode containing the arguments. UsefirstChild(n)to skip past it. - Module plugin:
{.plugin: "path".}as a top-level statement — receives the entire module after semantic analysis. Must output the complete transformed module. - Type plugin:
type T {.plugin: "path".} = ...— invoked for every module that usesT. Receives two inputs:paramStr(1)for the module AST andparamStr(3)for the type definition. - Import plugin:
import (path/foo) {.plugin: "std/v2".}— importspath/fooprocessed through thestd/v2plugin.
End-To-End Shape
- Expose the rewrite through a public template such as
template foo*(spec: string): untyped {.plugin: "fooplugin".}. - Keep the plugin logic in a separate plugin module.
- Start with
import plugins. Use theReplacerAPI for selective tree transforms or the low-levelNifCursor/NifBuilderAPI for direct construction. - For the low-level API:
let root = loadPluginInput(). For type plugins, also loadloadPluginInput(paramStr(3))for the triggering type definitions. - Read the relevant input node, build output, then finish with
saveTree(resultTree),saveReplacer(r), orsaveTree(errorTree("invalid plugin input")). - Keep runtime helpers in the public module. Keep NIF traversal and code generation in the plugin module.
- Template plugins can be hidden inside imported modules so callers do not see the
.pluginpragma.
Mental Model
NifBuilderis the mutable COW builder. Copying aNifBuildershares the payload; the next mutation detaches it.NifCursorwraps aCursor, which is a reference-counted shared pointer into token data. Copying aNifCursorincrements the refcount.NifCursors keep data alive even after the sourceNifBuilderis destroyed.snapshottakesvar NifBuilder(borrows, does not consume). It callsbeginReadunder the hood, which shares buffer ownership. The tree stays writable; mutation detaches the buffer via COW.snapshotrequires a non-empty tree. Guard withisEmpty(tree)first.- Treat
NifBuilderas owned mutable output. TreatNifCursoras a stable read handle that independently owns its data.
Replacer API
loadReplacer()loads input intoReplacer;saveReplacer(r)writesr.dest.keep r, Kindcopies one child and advances.drop r, Kindskips one child.replace r, Kind, replacementskips one child and emits aNifCursororNifBuilder.keepTag r:copies the current node tag, processes children, closes the output node, and advances past the input close.loopKeepTag r:keeps the current node tag and iterates all children.replaceHead r, NewTag, info:enters the current input node while emitting a different output tag.peek r:runs read-ahead logic and restores the cursor afterward; do not emit insidepeek.getCursor(r)andsetCursor(r, c)save and restore source cursor position.r.destis the output builder for synthetic children emitted alongside Replacer operations.- Kind annotations are mandatory: use
Any,Expr,Type,Stmt,Def,Sym,Dot,Lit,Nested, or a concrete tag such asCallX,CallS,AsgnS, orObjectT.
Low-Level Construction
createTree()creates empty output.createTree(kind; children...)andcreateTree(kind, info; children...)build a validated node in one call.kindmust be aNimonyType,NimonyExpr,NimonyStmt,NimonyOther, orNimonyPragmaenum value — passing the enum catches typos at compile time.withTree(kind, info): bodyis the normal way to emit a balanced node.- Use manual
addParLe/addParRionly when conditional structure makeswithTreeawkward. createTree(kind, children...)produces validated trees. If the structure is wrong, the result is replaced with anErrTnode. Trees built viawithTreeoraddParLe/addParRiare not validated.- Use
NoLineInfoonly for genuinely synthetic output. Preserve sourceinfowhen output is derived from input nodes.
BindSym — Hygienic Symbol References
Use bindSym to emit symbol references that resolve at plugin definition scope rather than at the user's call site.
- At plugin sem time,
echoresolves against the plugin module's imports and folds to the fully-qualified symbol name. - Single match emits one
Symbolatom; multiple matches emit a(cchoice ...)subtree. - Use
brOpenfor Nim-style mixin semantics,brForceOpento always wrap in(ochoice ...)even with one match. bindSymis a{.magic.}proc —namemust be a string literal.
Traversal
skip(node)skips the whole current subtree.firstChild(n)returns a bounded cursor at the first child of aParLenode — safe for iteration withwhile c.hasMore.hasMore(n)returns true while there are more children before the closing).into n: bodyenters the current node, runsbodyto process children, advances past).loopInto n: bodyenters the node, iterates all children, leaves.balancedTokens n: bodydeep-scans allParLenodes in a subtree (read-only).takeTree(t, var node)advances the reader — use it for single-token stepping when you need payload access, or for subtree consumption.- Copy a
NifCursorfor lookahead without committing movement on the original. - Use
kind,stmtKind,exprKind,typeKind,otherKind, andpragmaKindto inspect the current node. - Use
symId,symText,identText,stringValue,charLit,intValue,uintValue, andfloatValueto read payload.
Subtree Reuse
takeTree(t, var node)copies the current subtree and advances the reader.addSubtree(t, node)copies the current subtree without advancing the reader.add(t, childTree)appends a whole generated tree.copyInto(t, var node): bodycopies the opening tag, runsbodyto process children, closes the node, and advances past the matching).- Reuse existing subtrees when they are already correct. Do not rebuild them token by token without a reason.
Errors And IO
- Use
errorTree(msg)for synthetic plugin errors. - Use
errorTree(msg, info),errorTree(msg, at), orerrorTree(msg, at, orig)when location matters. renderTree(tree)renders raw NIF for inspection (omits line info).renderNode(node)renders the current subtree for inspection (omits line info).loadPluginInput()reads the default plugin input fromparamStr(1)and returns aNifCursor.saveTree(tree)writes the default plugin output toparamStr(2), preserving line info.loadReplacer()reads input and returns aReplacerready for transformation.saveReplacer(r)writes the Replacer's output toparamStr(2).
Type System
LineInfois packed source location metadata.NoLineInfois the zero value.SourcePoshaslineandcolfields (1-based, or 0 when invalid).SymIdis an opaque symbol handle. Use withaddSymUse/addSymDef.TagIdis a raw NIF tag identifier.isValid(info),filePath(info),lineCol(info)decode source locations.
Workflow
- Resolve the real API file.
Open the
plugins.nimused by the exactnimonyyou will run. - Decide the public entrypoint.
Export a template such as
template foo*(spec: string): untyped {.plugin: "fooplugin".}from the user-facing module. - Read the plugin input.
Use
loadPluginInput()orloadReplacer(). - Parse string DSL input before emitting NIF. Convert the DSL string into ordinary Nim objects, then emit from that parsed representation.
- Build output.
Use
Replacerfor selective transforms. UseNifBuilderdirectly withwithTree, subtree reuse, and helper procs when constructing output from scratch. Emit hygienic references withbindSyminstead of rawaddSymUsestrings. - Finish explicitly.
End with
saveTree(resultTree),saveReplacer(r), orsaveTree(errorTree("invalid plugin input")).
Common Mistakes
| Mistake | Why it's wrong |
|---|---|
| Writing a Nim macro for a new Nimony DSL | Plugins are the compile-time rewrite mechanism in Nimony |
| Mixing the public template and the plugin rewrite logic in one module | It tangles runtime API and NIF generation logic |
Treating NifBuilder as a read cursor |
NifBuilder is output storage; NifCursor is the read handle |
Using inc on a NifCursor |
inc does not exist for NifCursor. Use skip for subtrees, takeTree for single atoms, or firstChild and hasMore for iteration |
Confusing takeTree with addSubtree |
One advances the reader and the other does not |
Assuming a NifCursor is invalidated when its source NifBuilder is mutated or destroyed |
The Cursor refcount keeps the data alive; NifBuilder mutation detaches the buffer via COW |
| Snapshotting an empty tree | snapshot(tree) asserts on empty input |
| Rebuilding correct input subtrees atom by atom | It is slower, noisier, and easier to get wrong than subtree reuse |
| Crashing on invalid plugin input | Emit errorTree("invalid plugin input") so the compiler reports a source-level plugin error |
Assuming withTree output is validated |
Only createTree(kind, children) validates; withTree and addParLe/addParRi do not |
Using addSymUse("echo", ...) for symbols that exist in the user's scope |
bindSym resolves the reference at plugin definition scope |
References
references/template_plugin.md— Template plugin: compile-time 256-element popcount lookup tablereferences/module_plugin.md— Module plugin entrypoint and full-module output contractreferences/type_plugin.md— Type plugin: field-aware passthrough with paramStr(3)references/replacer_api.md— Replacer API contracts, safe skeletons, lookahead rules, and misuse boundaries
Changelog
- 2026-04-09: Initial skill.
- 2026-04-11: Added plugin-backed templates, loadPluginInput/saveTree flow.
- 2026-04-15: Added plugin kinds, StmtsS protocol, validation scope.
- 2026-04-17: Updated NifCursor as shared-pointer wrapper.
- 2026-04-18: Renamed Node to NifCursor, Tree to NifBuilder.
- 2026-06-15: Updated for current
pluginsAPI: Replacer,bindSym, bounded traversal,copyInto, source info helpers, and Nimony self-compilation. - 2026-06-17: Rewrote Replacer reference.