Compile-Time Macros

Generate Ricochet expression AST before bytecode while preserving the language's postfix shape and fail-closed host safety.

docs/reference/guides/macros.html
docs/wiki/macros.html
rco expand app.rco --json
Ricochet stack and MVC flow Value::String Value::Map Value::Class Value::Block bytecode VM Controller View Postgres $name "home/index" swap view [ ... ] "methodName" Method

Compile-time macros are a beta source-transformation surface. They expand parsed Ricochet AST before bytecode is generated, so generated code keeps the same postfix shape as handwritten code.

Use macros when a repeated declaration or expression pattern is clearer as a small local language feature. Macro bodies run in a restricted compile-time evaluator with no filesystem, network, process, database, clock, randomness, task, or capability access.

Expression Macros

Macro names are string literals and calls are explicit:

"unless" Macro
  ( condition body -> expansion )
  [
    [
      $condition ast_splice not if
        $body ast_splice call
      end
    ] quote_ast
  ]
end

$ready [
  "not ready" println
] "unless" macro_call

quote_ast converts a quoted block into expression AST. Inside quoted output, $arg ast_splice inserts the caller's parsed operand instead of generating a runtime $arg read.

Item Row Macros

Use quote_items when a macro call owns a whole item and should emit one or more item rows. Rows that do not match a declaration encoding stay ordinary expression items, so class-body declaration rows such as Accessor, Field, Table, and Method keep the same shape as handwritten class bodies:

"accessors" Macro
  [
    [
      "email" Accessor
      "name" Accessor
    ] quote_items
  ]
end

User Model Subclass
  "accessors" macro_call
end

quote_items is intentionally rejected inside larger expressions. Use quote_ast for expression-position expansion.

At top level, quote_items can also emit real declaration items with the macro-only row encodings below:

"function_rows" Macro
  [
    [
      [ "hello from generated function" println ] "greet" function
      ( name -> String ) [ "hello " $name + ] "greeting" function
    ] quote_items
  ]
end

"model_row" Macro
  [
    [
      [
        "users" Table
        "email" Accessor
        [ self email.get ] "label" Method
      ] User Model Subclass
    ] quote_items
  ]
end

Generated declaration bodies expand in the macro definition scope. If an imported public macro emits a function body that calls a private helper macro from the imported module, that private helper resolves from the imported module rather than from the caller.

Imports

Static imports expose public macros from the imported module during compilation. Macro declarations whose names start with _ are private to their defining module but can still be used by public macros from that module:

(( lib/macros.rco ))
"_helper" Macro
  [
    [ "from helper" println ] quote_ast
  ]
end

"say_ok" Macro
  [
    [ "_helper" macro_call ] quote_ast
  ]
end
(( main.rco ))
"lib/macros" import
"say_ok" macro_call

Unqualified calls prefer local macros. If exactly one imported module exports a matching macro, that macro is used. If multiple imports export the same name, the compiler fails and asks for a qualified call:

"lib/macros#say_ok" macro_call
"self#local_macro" macro_call

Package path imports use the same manifest and containment rules as ordinary static imports. rco expand --json now reports canonical package-aware macro module IDs with safe lock metadata when ricochet.lock is present, so package macros can be traced without exposing local cache paths.

Examples

Use the public examples to see both local and package-authored macro workflows:

rco run examples/macro_release_scorecard.rco
rco run examples/showcase/package_macro_queue_report/main.rco
Push-Location examples/showcase/package_macro_queue_report
rco expand main.rco --json

examples/macro_release_scorecard.rco shows a local declaration macro building a release scorecard function with a private helper macro for repeated count lines. examples/showcase/package_macro_queue_report shows a package-style macro module exporting a public install_queue_report macro, keeping _count_line private, and producing package-aware expansion metadata for the locked dependency.

Inspection

rco expand prints expanded source without executing runtime code:

rco expand app.rco
rco expand app.rco --json

The JSON form includes stable v1 contract metadata through schema, sources, source_map, and cache, alongside the expanded AST, macro tables, imports, traces, diagnostics, and output hash.

Current Limits

  • Macro declaration names must be string literals and cannot contain #.
  • Runtime imports do not load macros.
  • Macro bodies are fail-closed and can use only the compile-time helper surface.
  • Expansion depth, same-macro recursion, evaluator steps, and generated AST nodes are bounded.
  • quote_items declaration rows are supported only where a whole declaration item is valid.