2 releases
| 0.12.2 | Aug 10, 2025 |
|---|---|
| 0.12.1 | Jul 4, 2025 |
#2814 in Rust patterns
Used in 5 crates
(2 directly)
310KB
7K
SLoC
pochoir's minimal language
"hello " + "world!" |> len() == 12
pochoir's expressions use a hand-crafted, functional and interpreted language used to quickly transform
variables. It is highly functional and aims to keep the code readable while being extremely
compact, that's why there are, for example, no comments. It also has a complete integration and
compatibility with Rust's custom functions which can be defined by using the Function value, and inserted in the Context
that's why there are no function declarations.
Types
Generally, functions and operations are strict about which types can be used. For example, it
is not possible to add a number to a string (e.g 42 + " apples" does not work), you need to
convert the number to a string first using the to_string function (e.g to_string(42) + " apples" works).
The language supports 7 types:
Null, a type representing a thing that is not present. It is returned by an empty expression, an undefined variable or when indexing an array or an object with a field which does not exist. The value having this type isnull. It is the close to()in RustBool, a type representing a boolean value, can be eithertrueorfalse. It is mostly returned by equality operators (like==). It is the same asboolin RustNumber, a type representing a 64-bit floating point number (like in Javascript). It is the same as an [f64] in RustString, a type representing an UTF-8 encoded sequence of characters. It is the same as aStringin RustArray, a type representing a list of values. It is the same as a [Vec] in RustObject, a type representing a key-value pair of values. It is the same as aHashMapordered by order of insertion in RustFunction, a type representing a function defined in Rust which can be called in a script.Range, a type representing a range of positive numbers. It is the same as aRangeofi32s in Rust. It can be created using..or..=
Operators
Because there are no mutable "variables" (only static values passed in the Context or
intermediate values defined with the = operator), it is not possible to mutate values (it is
however possible to redefine them), that's why there are no loops and no mutable assignment
operators (+=, -=, …). Instead, all the transformations directly return the result. By the
way, it is strongly encouraged to use the pipe operator (|>) to give the returned value of a
function as the first argument of the next function instead of having a big soup of
function calls as arguments. For example,
compute(get_db(request_get("https://crates.io"), "file.txt")) should be rewritten as
request_get("https://crates.io") |> get_db("file.txt") |> compute().
The language supports all typical operators:
- Equality check operators:
==,!=; - Mathematical operators:
+,-,/,*(and parentheses to preserve the precedence); - Comparison operators:
<,>,<=,>=; - Logical operators:
&&,||; - Conditional operator:
<cond> ? <expr_if_true> : <expr_if_false>. - Definition operator:
=(only used to define intermediate constants or to redefine a single value in the context, complex assignments are not supported)
The types used on each side of the operator must have the same type, except Null values
which can be compared to all other types (only using == and !=).
Functions and Rust integration
The language features a tight integration with Rust by declaring functions in Rust and using
them in expressions. Each type passed as argument is automagically deserialized as a Rust type
(using its Deserialize implementation) and can directly be used. The number of parameters
and their type are checked as well when they are called. The return type must be
FunctionResult<T> (FunctionResult<T> is an alias for Result<T, Box<dyn std::error::Error>>)
where T implements IntoValue and is also magically converted into a value (without serializing!)
before executing the rest of the script.
A typical Rust-defined function to convert some emoji codes to their unicode equivalent can be defined like this:
use pochoir_lang::{Value, FunctionResult};
fn emoji(val: String) -> FunctionResult<String> {
Ok(match val.as_str() {
":tada:" => "🎉",
":rocket:" => "🚀",
_ => return Err(format!("unknown emoji code: `{val}`").into()),
}.to_string())
}
And then converted to a function using the Function::new function and inserted into the
global context using Context::insert:
#
use pochoir_lang::{Function, Context};
use pochoir_lang::eval;
let mut context = Context::new();
context.insert("emoji", Function::new(emoji));
let content = r#"emoji(":tada:") + emoji(":rocket:")"#;
assert_eq!(eval("inline-code", content, &mut context, 0).expect("failed to evaluate the code").to_string(), "🎉🚀".to_string());
The functions are normal Values so they can be namespaced using an object.
For example, you can define a function length in the namespace String using:
use pochoir_lang::{Value, Function, Context, object, eval};
use std::error::Error;
let mut context = Context::new();
context.insert("String", object! {
"length" => Function::new(|val: String| Ok(val.len())),
});
let content = r#"String.length('hello')"#;
assert_eq!(eval("inline-code", content, &mut context, 0).expect("failed to evaluate the code"), Value::Number(5.0));
You can specify optional parameters using an Option or checking if a Value is Value::Null, for instance:
use pochoir_lang::{Function, Context, Value, eval};
let mut context = Context::new();
context.insert("format_name", Function::new(|firstname: String, surname: Option<String>| {
if let Some(surname) = surname {
Ok(format!("{firstname} {surname}"))
} else {
Ok(firstname.to_string())
}
}));
let content = r#"format_name("Ada") == "Ada" && format_name("Ada", "Lovelace") == "Ada Lovelace""#;
assert_eq!(eval("inline-code", content, &mut context, 0).expect("failed to evaluate the code"), Value::Bool(true));
Note that all optional arguments must be the last arguments given to a function. Also note that named arguments are not supported, optional arguments must be given in the order they are defined.
Default functions
Some Rust functions are defined by default in the language: check the functions module to
see all of them.
Opinions
-
The language being used primarily with the rest of
pochoir's templating engine, it should be able to check if the property of an object or an array (or the attribute of a component) exists without raising an error if the property does not exist. That's the raison d'être of theNulltype -
The language does not support (yet) references, so all arguments given in functions need to be owned because functions will manipulate them
Examples
An expression checking the length of the content variable to display the according
adjective. It can, for example, be used to add a quick hint of the length of a blog post content.
word_count(content) > 100 ? word_count(content) > 1000 ? "very long" : "long" : "short"
An expression doing some maths. It outputs 507.
42 * 12 + (24 - 6) / ((5 - 7) * -3)
An expression formatting a position given as an array of two elements named pos. Following
the value of the pretty_print variable, it displays the coordinates using the
x: <value>, y: <value> notation or using a tuple.
pretty_print
? "x: " + to_string(pos[0]) + ", y: " + to_string(pos[1])
: "(" + to_string(pos[0]) + ", " + to_string(pos[1]) + ")"
What is currently supported
- Support all basic types (
String,Number,Bool,Array,Object,Function,Range) - Support all basic operators (
+,-,/,*,==,!=,<=,>=,<,>,&&,||) and the order of operations (with parentheses) - Support defining ranges using syntax like
2..3,2..=4,..3,2..(just..is not supported) - Support the conditional operator (
cond ? expr_if_true : expr_if_false) - Support indexing arrays, objects and strings (
array[1],object["key"],object.key,"hello"[2..4],"world"[3],"hello world"[6..]) - Support nesting function calls and the pipe operator (
fn1(fn2("argument")),fn2("argument") |> fn1()) - Support defining intermediate constants (with the
=operator) and redefining values passed in theContext(that's whyevalneeds a mutable reference, also with the=operator) - Good Rust functions integration
- Good error reporting
Playground
A web playground using WebAssembly is available at https://encre-org.gitlab.io/pochoir-playground
to try the syntax out.
Dependencies
~1–2MB
~35K SLoC