CREXX

REXX Language implementation

View the Project on GitHub adesutherland/CREXX

Compiler exits

This chapter introduces cREXX compiler exits: what they are, how to enable them, and how to use them effectively. It also shows concrete examples, including how variable shadowing works inside exit-generated code.

What are compiler exits?

A compiler exit is a compile-time hook written in Rexx that can inspect tokens and inject replacement code into the current compilation unit. Exits are packaged as a module that the compiler (rxc) loads on demand.

This chapter primarily describes the current general-exit model. The ongoing ADDRESS / REXXSAA work has also introduced a certified/system-exit model for core-team-curated exits that may own reserved keywords and use a richer compiler-facing planning contract. The working design record is address_rexxsaa_working.md.

Typical uses:

Injected code is grafted into the AST and then compiled like normal source.

Enabling exits

The compiler looks for an exit bundle (a packed .rxbin) in the import path.

  rxc -i /path/to/bin -o out in.rexx
  RXCP_EXIT_MODULE=rxcexits rxc -i /path/to/bin -o out in.rexx

Notes:

Built-in demo exits

The project ships a small bundle of exits primarily for demonstration and testing:

You can find their sources under compiler/exits/ in the repository.

Certified/System Exits

Current user exits are intended for non-reserved keywords. The compiler now also has certified/system exits for core-team-policed language features.

Current certified exits:

Key distinctions in the current model:

Treat the working note as the design authority for the evolving model: address_rexxsaa_working.md.

Example: Dumping variables and arrays

Given

options levelb
import rxfnsb
main: procedure
  a = .int[5]
  a[1] = 42
  dump a
  return 0

Compiling with the exits bundle:

RXCP_EXIT_MODULE=rxcexits rxc -i ./cmake-build-debug/bin -o test ./path/to/file.rexx

At compile time, dump a is replaced with a small loop that prints every element:

a (.int[5])[1] = 42
a (.int[5])[2] = 0
a (.int[5])[3] = 0
a (.int[5])[4] = 0
a (.int[5])[5] = 0

Shadowing and scoping inside exits

Exit replacements are grafted back into the main AST and then normalized like ordinary source. Structured replacements are supported, including nested DO ... END, IF ... THEN/ELSE, and nested instruction blocks. Local scopes for those generated blocks are materialized during the normal symbol-building passes, so identifiers created by injected code do not collide with or overwrite variables in the host program.

Practical consequences:

Illustration (conceptual)

options levelb
main: procedure
  i = .int; i = 7
  dump i   -- exit may internally use a loop variable named i
  say i    -- still prints 7; host i is untouched
  return 0

Behind the scenes, any generated grouped blocks are compiled with their own local scopes so that temporary loop counters and helper variables remain confined to the exit-generated block structure.

Writing your own exit (overview)

An exit is a Rexx module that exports a process procedure taking a token descriptor and returning a replacement string or an empty string.

High-level shape

namespace myexits expose process

process: procedure
  arg ti = .token
  -- Inspect ti.get_type(), ti.get_text(), ti.get_value_type(), ...
  -- Optionally build and return source replacement text
  return ""

Package your exit module into a bundle (.rxbin) and place it in the compiler’s import path, then set RXCP_EXIT_MODULE to the bundle name.

Planning Hooks

Current behaviour:

Planned direction:

See the working note for the proposal details: address_rexxsaa_working.md.

Tips

Troubleshooting