A YAMLScript-inspired DSL for producing nested data structures. Lisp-flavored surface syntax, written entirely in Rust, statically compiled.
(def env "production")
{:app "trongoscript-demo"
:env env
:port (if (= env "production") 443 8080)
:auth {:enabled true
:scopes ["read" "write"]}
:db (include "db.trongo")}YAMLScript proved the value of embedding a small, evaluable expression language inside YAML / JSON / TOML configuration files. Trongoscript carries the same idea into a pure-Rust stack:
- One binary, no runtime — statically compiled; no JVM, no native shared libs, no Clojure interop.
- Same DSL in three modes — evaluate a
.trongodocument, render an ERB-style template, or substitute!trongosnippets inside an existing JSON/YAML/TOML tree. - Secure by default — no ambient filesystem, network, or process capabilities. Hosts opt in to a sandboxed directory; nothing else is reachable.
- Extensible by host — additional functions can be registered with the
inventorycrate's distributed slice, or per-instance viaEvalConfig::with_host_builtin.
| Crate | Purpose |
|---|---|
trongoscript-parser |
nom-based parser → AST |
trongoscript-stdlib |
Built-in functions, registered with inventory |
trongoscript |
Evaluator, sandbox, template renderer, embed walker |
trongoscript-cli |
trongo CLI (eval / render / embed) |
# Evaluate a document
trongo eval -i config.trongo --format yaml
# Sandboxed I/O (include / read-file / list-files)
trongo eval -i main.trongo --root ./scripts
# Mount extra directories into the sandbox: ./bob/x is /x, ./tom/y is /tom/y
trongo eval -i main.trongo --root ./bob --mount /tom=./tom
# ERB-like template
trongo render -i page.erb --root ./views
# Substitute !trongo snippets in a YAML/JSON/TOML doc
trongo embed -i config.yaml --format yaml --root .
# Language reference
trongo reference;; Atoms
42 3.14 -7 true false nil
"string" :keyword symbol
;; Compound
(head arg ...) ; list — function call or special form
[a b c] ; vector
{:k v :k2 v2} ; map (alternating keys/values; commas are whitespace)
;; Special forms
(let [n v ...] body...)
(if cond then else?)
(when cond body...)
(do expr...)
(def name value)
(and ...) (or ...)
(quote x) ; also 'x
(quasiquote x) ; also `x
(unquote y) ; also ~y, only inside quasiquote
(unquote-splicing ys) ; also ~@ys, only inside quasiquote
;; First-class functions
(fn [x y] (+ x y)) ; anonymous closure
(fn [head & tail] tail) ; rest args collected into a list
(defn add [x y] (+ x y)) ; sugar for (def add (fn [x y] ...))
((fn [x] (* x x)) 5) ; immediately invoked → 25
;; Higher-order builtins — core
(map (fn [x] (* x 2)) [1 2 3]) ; → [2 4 6]
(filter (fn [n] (> n 0)) [-1 0 1 2]) ; → [1 2]
(reduce (fn [a x] (+ a x)) 0 [1 2 3 4]) ; → 10
(reduce (fn [a x] (+ a x)) [1 2 3 4]) ; seed = first elem → 10
;; Higher-order builtins — CL-style family
;; mapcar, mapc, maplist, mapl, mapcan, mapcon
;; every, some, notany, notevery
;; count-if / -if-not, find-if / -if-not
;; position-if / -if-not, member-if / -if-not
;; remove-if / -if-not
;; assoc-if / -if-not, rassoc-if / -if-not
;; subst-if / -if-not (whole-tree)
;; substitute-if / -if-not (flat sequence)
;; sort, stable-sort (pred is 2-arg less-than)
;; funcall, apply, identity, constantly, complement
;; remove-duplicates, tree-equal
;; union, intersection, set-difference, set-exclusive-or
;;
;; Destructive variants (delete-if, nsubst-if, ...), `multiple-value-call`,
;; and CL's sequence `merge` are intentionally omitted — Trongoscript
;; values are immutable JSON, there are no multiple values, and `merge`
;; is already taken by the map-merging builtin.
;; Macros — defmacro defines a code-rewriting form. Parameters that
;; appear bare in the body are substituted with the corresponding
;; *unevaluated* argument; combine with `` ` ``, `~`, `~@` for richer
;; templates.
(defmacro unless [c body] (if c nil body))
(defmacro inc [x] `(+ ~x 1))
(defmacro sum-of [& xs] `(+ ~@xs))
(macroexpand '(unless cond expr)) ; → (if cond nil expr)
;; Sandboxed I/O — only available when the host configures a sandbox root.
;; Relative paths resolve against the current file's directory; absolute
;; paths are absolute within the sandbox's virtual filesystem.
(include "path.trongo")
(read-file "data.txt")
(read-file "/tom/y") ; from a directory mounted at /tom
(list-files "subdir")
use trongoscript::{eval_source, Context, EvalConfig, Sandbox, Value};
// Rooted at ./scripts, with ./shared grafted in at /shared.
let sb = Sandbox::new("./scripts")?.mount("/shared", "./shared")?;
let mut ctx = Context::new(EvalConfig::default().with_sandbox(sb));
let v: Value = eval_source(
r#"{:count (+ 1 2 3) :name (upcase "alice")}"#,
&mut ctx,
)?;Hosts can extend the language with their own functions:
use std::sync::Arc;
use trongoscript::{Context, EvalConfig, BuiltinResult, Value};
let host = Arc::new(|_ctx: &mut Context, args: &[Value]| -> BuiltinResult {
Ok(Value::String(format!("hello {}", args.len())))
});
let cfg = EvalConfig::default().with_host_builtin("hello", host);trongoscript has no ambient capabilities:
- Cannot open sockets, run subprocesses, or touch the filesystem unless the
embedder passes a
Sandbox. - Reading and listing are the only filesystem operations that exist. There is no write, delete, or execute builtin, so every mount is read-only.
- Scripts see a virtual filesystem, not host paths. An absolute path like
/etc/passwdis absolute in sandbox-space and resolves inside the sandbox; the host's/is unreachable. - Sandbox paths are canonicalized; symlinks that escape their mount are
refused, as is
..traversal outside it...never crosses a mount boundary, so a script under/tomcannot climb into the root mount. - Per-file size, include depth, and listing-entry limits prevent pathological inputs.
- Include cycles are detected and rejected.
See crates/trongoscript/src/sandbox.rs
for the full security model.
MIT OR Apache-2.0.