This document describes Ruby macros and how they are implemented using Ruby's parse tree represented as nested Node objects. Key points include:
- Macros allow code to be executed at parse time by transforming the parse tree before it is executed.
- The parse tree is represented as a hierarchy of Node objects descended from Array, with nodes addressing subnodes like a normal Array.
- Implementing macros requires first parsing the code into a Node tree using RedParse, then transforming the tree before executing it.
- This allows macros to modify the code before it runs, but preprocessing is slow and there are limitations on macro usage and scoping.
9. if $Debug macro assert(cond) if RedParse::OpNode===cond and /[=!]=/===cond.op left,op,right=*cond :(fail 'expected '+^left.unparse({})+"(==#{^left}) to be "+ ^op+" "+^right.unparse({})+"(==#{^right})" unless ^cond) else :(fail "expected #{:(^^cond)}, but was not true" unless ^cond) end end else macro assert(cond) end end
10. def test_assert a=1 b=2 assert a #ok assert a!=b #ok assert(a==b) #oops, fails. msg="expected a(==1) to be == b(==2)" assert(nil) #oops, fails. msg="expected nil, but was not true" #ok, that message didn't make a lot of sense... end
11. Syntax trees are represented by trees of nested Nodes. All Nodes descend from Array, and their subnodes can be addressed by numeric index, just like normal Arrays. However, many subnodes want to have names as well, thus most (but not all) array slots within the various Node classes have names. The general rule is that Node slots may contain a Node, a VarNameToken, a plain Array, a String, or nil. However, many cases are more specific than that.
12. VarNameToken<RubyLexer::Token #represents variables and constants (ident: String) Node<Array #abstract ancestor of all nodes (except VarNameToken) +RescueNode #a rescue clause in a def of begin statement | (exceptions: Array[Value*], varname: VarNameToken|nil, action: Value) +WhenNode #a when clause in a case statement | (when: Value|Array[Value+] then: Value|nil ) +ElsifNode #an elsif clause in an if statement | (elsif: Value, then: Value|nil) +ValueNode #abstract, a node which has a value (an expression) |+ListOpNode #abstract, ancestor for nodes which are lists of ||| #things separated by some op ||+SequenceNode #a sequence of statements ||| (Array[Value*]) ||+ConstantNode #a constant expression of the form A::B::C or the like || #first expression can be anything || (Array[String|Value|nil,String+]) |+RawOpNode #ancestor of all operators (but not . :: ; , ?..:) ||| (left: Value, op: String, right: Value) ||+OpNode #ancestor of some operators |||+RangeNode #a range literal node |||+KeywordOpNode #abstract, ancestor of keyword operators ||||+LogicalNode #and or && || expressions ||||+WhileOpNode #while as an operator ||||+UntilOpNode #until as an operator ||||+IfOpNode #if as an operator ||||+UnlessOpNode #unless as an operator |||+NotEqualNode #!= expressions
13. |||+MatchNode #=~ expressions |||+NotMatchNode #!~ expressions |+LiteralNode #literal symbols, integers || (val: Numeric|Symbol|StringNode) |+StringNode #literal strings ||| (Array[(String|Value)+]) ||+HereDocNode #here documents |+StringCatNode #adjacent strings are catenated ("foo" "bar" == "foobar") || (Array[StringNode+]) |+NopNode #an expression with no tokens at all in it || (no attributes) |+VarLikeNode #nil,false,true,__FILE__,__LINE__,self || (name: String) |+UnOpNode #unary operators || (op: String, val: Value) ||+UnaryStarNode #unary star (splat) |||+DanglingStarNode #unary star with no argument ||||| (no attributes) ||||+DanglingCommaNode #comma with no rhs || (no attributes) |+ParenedNode #ugly, parenthesized expressions and begin..end || (body: Value) -OR- (parentheses) || (body: Value|nil, rescues: Array[RescueNode*], || else: Value|nil, ensure: Value|nil) (begin...end and rescue as operator) |+AssignNode #assignment (including eg +=) || (left:AssigneeList|LValue, op:String ,right:Array[Value*]|Value)