QuasiQuoters
QuasiQuoters (QQ) are Nic's mechanism for typed, user-defined compile-time literals. They let you embed domain-specific languages directly in your Nic code with full type safety and zero runtime overhead.
Basic Syntax
A QuasiQuoter uses backtick syntax with a named quoter:
let re = regex`\d{4}-\d{2}-\d{2}`;
let query = sql`SELECT * FROM users WHERE age > 18`;
let grammar = parser`
expr := term (("+" | "-") term)*
`;The pattern is: name followed by a backtick string. The name must be a QuasiQuoter binding in scope.
How It Works
When the compiler encounters a QuasiQuoter expression like sqlSELECT...``:
- The parser produces an AST node with the quoter name and raw string
- The compiler looks up
sqlin scope - it must have typeQuasiQuoter[T] - At compile time, the quoter's
parsefunction is called with the raw string - The quoter returns a Nic AST expression of type
T - The AST replaces the original QQ node - no QQ exists at runtime
This means QQ processing happens entirely at compile time with zero runtime overhead.
The QuasiQuoter Type
A QuasiQuoter is simply a struct with a parse function:
type QuasiQuoter[T] = {
parse: fn(input: string, span: Span) -> Result[Expr, QQError],
options: QQOptions,
};
type QQOptions = {
dedent: bool = false, // Remove common indentation
trim_edges: bool = false, // Trim leading/trailing whitespace
};The parse function receives the raw string content and returns either:
- A Nic AST expression that will replace the QQ
- An error with source location information
Using Standard QuasiQuoters
Regex
import std.regex(regex);
fn main() -> unit {
let email_pattern = regex`[a-z]+@[a-z]+\.com`;
let date_pattern = regex`\d{4}-\d{2}-\d{2}`;
if email_pattern.matches("test@example.com") {
println("Valid email!");
}
}The regex quoter compiles the regular expression at compile time, catching syntax errors early and optimizing the pattern.
SQL
import std.sql(sql);
fn get_user(db: *Database, id: i64) -> Result[User, DbError] {
let query = sql`SELECT * FROM users WHERE id = ${id}`;
return db.execute(query);
}The sql quoter validates SQL syntax at compile time and handles parameter interpolation safely.
Interpolation
QuasiQuoters support interpolation using ${expr} syntax:
let name = "Alice";
let greeting = template`Hello, ${name}! Today is ${get_date()}.`;Interpolated expressions:
- Are type-checked normally by the compiler
- Are passed to the quoter as AST nodes (not evaluated values)
- Let the quoter decide how to use them (inline, as parameters, etc.)
The quoter receives a list of parts:
type QQPart = Raw(string) | Interp(Expr);Creating Your Own QuasiQuoter
Here's a simple quoter that creates uppercase strings:
pub let upper: QuasiQuoter[string] = QuasiQuoter {
parse: fn(input: string, span: Span) -> Result[Expr, QQError] {
let uppercased = input.to_uppercase();
// Return an AST node for a string literal
Ok(Expr::StringLit(uppercased))
},
options: QQOptions { dedent: false, trim_edges: true },
};
// Usage:
let message = upper`hello world`; // "HELLO WORLD"A more practical example - a JSON quoter:
pub let json: QuasiQuoter[JsonValue] = QuasiQuoter {
parse: fn(input: string, span: Span) -> Result[Expr, QQError] {
match parse_json(input) {
Ok(value) => Ok(json_to_expr(value)),
Err(e) => Err(QQError {
message: "Invalid JSON: " + e,
span: span,
}),
}
},
options: QQOptions { dedent: true, trim_edges: true },
};
// Usage:
let config = json`
{
"name": "myapp",
"version": "1.0.0",
"features": ["async", "networking"]
}
`;Compile-Time Guarantees
QuasiQuoters run in a sandboxed compile-time environment:
Allowed:
- Pure computations on the input string
- Access to interpolated AST nodes
- Metadata: filename, line/column, compiler version
Not Allowed:
- File I/O or network access
- Side effects or mutation
- Environment variables or system calls
This ensures:
- Builds are deterministic and reproducible
- QQ results can be cached
- No security risks from running untrusted code
Multiline and Indentation
The dedent option is useful for embedded DSLs with significant whitespace:
let grammar = parser`
program := statement*
statement := assignment | expression
assignment := identifier "=" expression ";"
expression := term (("+" | "-") term)*
term := factor (("*" | "/") factor)*
factor := number | identifier | "(" expression ")"
`;With dedent: true, the common leading whitespace is removed before parsing.
Error Handling
QQ errors include source location information:
let bad_regex = regex`[unclosed`;
// Error at main.nic:5:17
// Invalid regex: unclosed character class
// 5 | let bad_regex = regex`[unclosed`;
// ^~~~~~~~~Errors from the quoter's parse function are reported with the correct source span.
Use Cases
QuasiQuoters are ideal for:
- Regular expressions: Compile-time validation and optimization
- SQL queries: Syntax checking and parameter safety
- Parser generators: Grammar definitions
- Templates: HTML, text templates with interpolation
- Shader code: GLSL, HLSL embedded in Nic
- Configuration: JSON, TOML, YAML literals
- Protocol definitions: GraphQL, Protobuf schemas
Summary
| Concept | Description |
|---|---|
| Syntax | name\raw content`` |
| Type | QuasiQuoter[T] where T is the result type |
| Interpolation | ${expr} for embedding Nic expressions |
| Execution | Compile-time only, zero runtime cost |
| Sandbox | Pure functions, no I/O or side effects |
| Options | dedent and trim_edges for whitespace handling |
QuasiQuoters bring the power of domain-specific languages to Nic while maintaining type safety, compile-time error checking, and zero runtime overhead.