Skip to content

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:

nic
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...``:

  1. The parser produces an AST node with the quoter name and raw string
  2. The compiler looks up sql in scope - it must have type QuasiQuoter[T]
  3. At compile time, the quoter's parse function is called with the raw string
  4. The quoter returns a Nic AST expression of type T
  5. 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:

nic
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

nic
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

nic
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:

nic
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:

nic
type QQPart = Raw(string) | Interp(Expr);

Creating Your Own QuasiQuoter

Here's a simple quoter that creates uppercase strings:

nic
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:

nic
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:

nic
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:

nic
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

ConceptDescription
Syntaxname\raw content``
TypeQuasiQuoter[T] where T is the result type
Interpolation${expr} for embedding Nic expressions
ExecutionCompile-time only, zero runtime cost
SandboxPure functions, no I/O or side effects
Optionsdedent 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.

Released under the MIT License.