Step 2: Make Regex a compile-time parameter, use compile-time operations
We just created this Regex at compile time.
Do we read it at run-time? Do we put it into a global or something?
No! We're going to only read it at compile-time.
"But Evan, we need to read the Regex at run-time! Otherwise, how does the run-time program even know what to do? How does it know what functions to call, what branches to take, etc.?"
Great question! The answer is that we can make all those decisions at compile time.
I know that doesn't make any sense, but bear with me.
Here's how we change our program.
Before:
fn _match_node(
    nodes: List[RegexNode],
    node_idx: Int,
    text: String,
    start_pos: Int
) -> MatchResult:
    var node = nodes[node_idx]
    if node.isa[LiteralNode]():
        ref literal_node = node[LiteralNode]
        return _match_literal(nodes, literal_node, text, start_pos)
    elif node.isa[RepeatNode]():
        ref repeat_node = node[RepeatNode]
        return _match_repeat(nodes, repeat_node, text, start_pos)
    elif ...
After:
fn _match_node[nodes: List[RegexNode], node_idx: Int](
    text: String,
    start_pos: Int
) -> MatchResult:
    alias node = nodes[node_idx]
    @parameter
    if node.isa[LiteralNode]():
        alias literal_node = node[LiteralNode]
        return _match_literal[nodes, literal_node](text, start_pos)
    elif node.isa[RepeatNode]():
        alias repeat_node = node[RepeatNode]
        return _match_repeat[nodes, repeat_node](text, start_pos)
    elif ...
There are two main changes:
- The nodes and node_idx run-time parameters are now compile-time parameters, because they're inside [...]. We're using Mojo's generics system to hold these values at compile-time.
 - The @parameter if. @parameter means "at compile time" in Mojo-speak, so this if is run at compile-time, not run-time. This is like a constexpr if in C++ terms, or in C terms it's more like #define than if.
 
That means _match_node is now a generic function, a template.
There's only one nodes value (from the Regex), but there are 28 different node_idxs for this Regex. That means we'll have 28 different instantiations of this _match_node, one for each node_idx.
Previously, we had a very simple call tree, with only one _match_node, that was recursive. The call tree looked like this:
Now, we have 28 versions of _match_node, with a call tree that looks like this:
- main
 - match[Regex instance]
 - _match_node[List(...), 0] (for whole expr)
 - _match_node[List(...), 1] (for first \w+)
 - _match_node[List(...), 2] (for \w)
 - _match_node[List(...), 3] (for (\+\w*)?)
 - _match_node[List(...), 4] (for \+\w*)
 - _match_node[List(...), 5] (for \+)
 - _match_node[List(...), 6] (for \w*)
 - _match_node[List(...), 7] (for \w)
 - _match_node[List(...), 8] (for @)
 - _match_node[List(...), 9] (for (\d+\.\d+\.\d+\.\d+|\w+\.\w+))
 - _match_node[List(...), 10] (for \d+\.\d+\.\d+\.\d+)
 - _match_node[List(...), 11] (for \d+)
 - _match_node[List(...), 12] (for \d)
 - _match_node[List(...), 13] (for \.)
 - _match_node[List(...), 14] (for \d+)
 - _match_node[List(...), 15] (for \d)
 - _match_node[List(...), 16] (for \.)
 - _match_node[List(...), 17] (for \d+)
 - _match_node[List(...), 18] (for \d)
 - _match_node[List(...), 19] (for \.)
 - _match_node[List(...), 20] (for \d+)
 - _match_node[List(...), 21] (for \d)
 - _match_node[List(...), 22] (for \w+\.\w+)
 - _match_node[List(...), 23] (for \w+)
 - _match_node[List(...), 24] (for \w)
 - _match_node[List(...), 25] (for \.)
 - _match_node[List(...), 26] (for \w+)
 - _match_node[List(...), 27] (for \w)
 
28 versions, each different only in their node_idx compile-time parameter.
To understand that better, let's look at the _match_node[List(...), 1] function for the first \w+.
When the compiler instantiates it as _match_node[List(...), 1], it would look like below. The @parameter if is evaluated at compile time, so only one of its branches is included. I'll leave them in as comments to illustrate:
fn _match_node[List(...), 1](  # Node 1 is a RepeatNode
    text: String,
    start_pos: Int
) -> MatchResult:
#   alias node = nodes[node_idx]
#   @parameter
#   if node.isa[LiteralNode]():
#       alias literal_node = node[LiteralNode]
#       return _match_literal[nodes, literal_node](text, start_pos)
#   elif node.isa[RepeatNode]():
#       alias repeat_node = node[RepeatNode]
        return _match_repeat[List(...), RepeatNode(2, 1, 0)](text, start_pos)
#   elif ...
The benefit here is that we don't have to do the node.isa[LiteralNode], node.isa[RepeatNode], etc. at run-time. That's pretty nice, it eliminates a bit of overhead.
It also executed the alias statements for us too, so we don't have to do those lookups (and bounds checks) at run-time. Another nice speedup!
We'll see the biggest benefit in the next section though.