raku.gg / grammars

Grammar Actions

2026-03-24

Parsing text into a match tree is only half the job. You usually want to transform that tree into something useful, like a data structure, an AST, or computed values. That is where grammar action classes come in. They let you attach transformation logic to each rule in your grammar.

What Are Actions?

An action class is a regular Raku class whose methods correspond to the rules in your grammar. Each method receives the match object for its rule and can transform it using .make and .made:
grammar KeyValue { token TOP { <;pair>;+ % \n } token pair { <;key>; '=' <;value>; } token key { \w+ } token value { \N+ } } class KeyValue-Actions { method TOP($/) { make $<pair>;.map(*.made).Hash; } method pair($/) { make $<key>;.made => $<value>;.made; } method key($/) { make $/.Str } method value($/) { make $/.Str.trim } } my $input = q:to/END/.trim; host=localhost port=8080 debug=true END my $result = KeyValue.parse($input, actions => KeyValue-Actions.new); say $result.made; # {debug => true, host => localhost, port => 8080}

.make and .made

These are the two essential methods:

Think of .make as "this is what this node means" and .made as "what did that child node mean?"

grammar NumberList { token TOP { <;number>;+ % ',' } token number { \d+ } } class NumberList-Actions { method TOP($/) { make $<number>;.map(*.made).list; } method number($/) { make $/.Int; # Convert string match to actual Int } } my $r = NumberList.parse("10,20,30", actions => NumberList-Actions.new); say $r.made; # (10 20 30) say $r.made.sum; # 60

The $/ Parameter

Action methods receive $/, the current match object. You can access named captures with $<name> (shorthand for $/<name>) and positional captures with $0, $1, etc.
grammar Assignment { token TOP { <;variable>; \s* '=' \s* <;expression>; } token variable { <;[a..z]> <;[a..z 0..9]>* } token expression { .+ } } class Assignment-Actions { method TOP($/) { make { var => $<variable>;.made, expr => $<expression>;.made, } } method variable($/) { make $/.Str } method expression($/) { make $/.Str.trim } } my $r = Assignment.parse("count = 42", actions => Assignment-Actions.new); say $r.made; # {expr => 42, var => count}

Building a Data Structure

Here is a more complete example that parses an INI-style config file into nested hashes:
grammar INI { token TOP { <;section>;+ %% \n* } token section { <;header>; \n <;entry>;+ %% \n } token header { '[' <;name>; ']' } token name { <;[\w.-]>+ } token entry { <;key>; \s* '=' \s* <;value>; } token key { <;[\w.-]>+ } token value { \N+ } } class INI-Actions { method TOP($/) { make $<section>;.map(*.made).Hash; } method section($/) { make $<header>;.made => $<entry>;.map(*.made).Hash; } method header($/) { make $<name>;.Str; } method entry($/) { make $<key>;.Str => $<value>;.Str.trim; } } my $ini = q:to/END/.trim; [database] host=localhost port=5432 [app] debug=true name=MyApp END my $config = INI.parse($ini, actions => INI-Actions.new); say $config.made; # {app => {debug => true, name => MyApp}, database => {host => localhost, port => 5432}}

Action Methods Are Optional

You do not need to write an action method for every rule. If a rule has no corresponding action method, .made on its match will return Nil. Only write actions for rules where you need to transform data:
grammar Tagged { token TOP { <;tag>;+ % \s+ } token tag { '#' <;name>; } token name { \w+ } } class Tagged-Actions { method TOP($/) { make $<tag>;.map(*.made).list; } method tag($/) { make $<name>;.Str; # Skip the '#' prefix } # No action for 'name' -- not needed } my $r = Tagged.parse("#raku #perl #coding", actions => Tagged-Actions.new); say $r.made; # (raku perl coding)

Actions with State

Since action classes are regular objects, they can carry state:
grammar Words { token TOP { <;word>;+ % \s+ } token word { \w+ } } class WordCounter { has %.counts; method TOP($/) { make %!counts; } method word($/) { %!counts{$/.Str.lc}++; } } my $actions = WordCounter.new; Words.parse("the cat sat on the mat the cat", actions => $actions); say $actions.counts; # {cat => 2, mat => 1, on => 1, sat => 1, the => 3}

The .ast Shorthand

.ast is an alias for .made. Some people prefer it for clarity when building abstract syntax trees:
grammar Expr { token TOP { <;term>; } token term { <;number>; | <;string>; } token number { '-'? \d+ ['.' \d+]? } token string { '"' <;( <;-["]>* )> '"' } } class Expr-Actions { method TOP($/) { make $<term>;.ast } method term($/) { make $<number>; ?? $<number>;.ast !! $<string>;.ast; } method number($/) { make $/.Num } method string($/) { make $/.Str } } my $r = Expr.parse('"hello"', actions => Expr-Actions.new); say $r.ast; # hello say $r.ast.^name; # Str

Pattern: Bottom-Up Transformation

The general pattern for action classes is bottom-up: leaf rules transform their text into typed values, and parent rules combine their children's .made values into larger structures. This naturally mirrors the grammar hierarchy and keeps each action method simple and focused.

In the next grammar post, we will build a complete calculator grammar with proper operator precedence using these techniques.