2 releases

0.12.2 Aug 10, 2025
0.12.1 Jul 4, 2025

#2814 in Rust patterns


Used in 5 crates (2 directly)

MIT license

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 is null. It is the close to () in Rust
  • Bool, a type representing a boolean value, can be either true or false. It is mostly returned by equality operators (like ==). It is the same as bool in Rust
  • Number, a type representing a 64-bit floating point number (like in Javascript). It is the same as an [f64] in Rust
  • String, a type representing an UTF-8 encoded sequence of characters. It is the same as a String in Rust
  • Array, a type representing a list of values. It is the same as a [Vec] in Rust
  • Object, a type representing a key-value pair of values. It is the same as a HashMap ordered by order of insertion in Rust
  • Function, 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 a Range of i32s 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 the Null type

  • 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 the Context (that's why eval needs 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