Maccro
Maccro is a library to introduce macro (dynamic code rewriting), written in Ruby 100%.
require "maccro"
# name, before, after
Maccro.register(:double_less_than, 'e1 < e2 < e3', 'e1 < e2 && e2 < e3')
# This rewrites the code below
class Foo
def foo(v)
if 1 < v < 2
"hit!"
end
end
end
Maccro.apply(Foo, Foo.instance_method(:foo))
# To this (valid Ruby) code dynamically
class Foo
def foo(v)
if 1 < v && v < 2
"hit!"
end
end
end
Another example is about ActiveRecord queries.
require "maccro/builtin"
Maccro::Builtin.register(:activerecord_utilities)
Maccro.enable(path: __FILE__)
# This rewrites the code below
class Users < ApplicationRecord
def not_admin_or_under_20_age_users
Users.where(:priv != "admin" || :age < 20)
end
end
# To this automatically
class Users < ApplicationRecord
def not_admin_or_under_20_age_users
Users.where('priv != ?', "admin").or(Users.where('age < ?', 20))
end
end
Maccro also provides methods to rewrite any code in blocks:
require "maccro"
Maccro.register(:double_less_than, 'e1 < e2 < e3', 'e1 < e2 && e2 < e3')
does_sandwitch_one = ->(a, b){ a < 1 < b }
Maccro.rewrite(does_sandwitch_one).call(0, 2) #=> true
# or execute the block parameter immediately
Maccro.execute{ 0 < 1 < 2 } #=> true
Maccro comes from "Macro" and "Makkuro"(means "pure black" in Japanese).
Why Maccro?
- New macro processor can depend on Ruby's new
RubyVM::AbstractSyntaxTree
- Macro rules can be interoperable between Ruby versions (but only for Ruby 2.6 or later)
- Todo: Write other reasons
LIMITATION
Maccro can:
- run with Ruby 2.6 or later
- rewrite code, only written in methods using
def
keyword, inmodule
orclass
, or blocks - not rewrite singleton methods, which are used just after definition
- not rewrite methods from command line option (
-e
) or REPLs (irb/pry)
Maccro features below are not supported yet:
- Non-idempotent method calls
- Handling method visibilities
- Rewriting singleton methods with non-self receiver
- Placeholder validation
- Multi time match placeholder
Usage
Maccro users do:
- register rules how to rewrite methods, with code patterns
- or use built-in rules
- apply a set of registered rules to a method, or to a block
- enable automatic applying to a module/class or to a file, or globally
Terminology
- Rule
- a definition to rewrite codes, which has a name and two Ruby code snippets of Before and After
- Before
- a Ruby code snippet to match a pattern of code, which may contain a placeholder or placeholders to capture Ruby codes
- After
- a Ruby code snippet to replace matched code, which may contain a placeholder or placeholders to inject captured Ruby codes
- Placeholder
- a bare word in Before/After, to capture/replace Ruby code snippets
- its format is an alphabetical character and an integer number (>= 1) (e.g,
e1
,e100
,v1
) - alphabetical characters represents the types of Ruby code (an expression, an value, etc)
Writing Rules
When you write a new rule, it should have a symbol of unique name, and two Ruby code snippets as String, which represents the code pattern to be rewritten, and the code pattern how to rewrite it.
Maccro.register(:name_of_this_rule, 'ruby_code_before', 'ruby-code-after')
"Before" and "After" code snippets must be a valid Ruby code as themselves (that means it can be parsed without syntax error). We can check it using ruby -cw -e 'code-snippet'
.
For example, the rule below will rewrite Math.sin(@x)
to my_own_sin(@x)
.
Maccro.register(:rewrite_sin_to_mine, 'Math.sin(@x)', 'my_own_sin(@x)')
This rule matches the code exactly equal to Math.sin(x)
. The receiver must be the Math
class, method must be the Math.sin
and the argument must be the instance variable @x
.
If you want to rewrite every Math.sin
calls to my_own_sin
, arguments should be a placeholder.
Maccro.register(:rewrite_sin_to_mine, 'Math.sin(e1)', 'my_own_sin(e1)')
The placeholder e1
will match to an any expression (which returns a value / values, including literals, variables, function or method calls and if/unless). The e1
in "Before" captures the actual expression used in the rewritten method definition, and the e1
in "After" will be replaced with the captured code.
# Applying the rule: Maccro.register(:rewrite_sin_to_mine, 'Math.sin(e1)', 'my_own_sin(e1)')
# Before rewrite
def myfunc(x, y, z)
return [Math.sin(x), Math.sin(x + y), Math.sin(if x > y then z else 0 end)]
end
# After rewrite
def myfunc(x, y, z)
return [my_own_sin(x), my_own_sin(x + y), my_own_sin(if x > y then z else 0 end)]
end
A rule will match to codes of the method as much as possible, and will rewrite all matched pieces.
If the specified placeholder was v1
, vN
placeholders matches only with a value (a literal, a local variable, an instance variable, a global variable, etc), so the result will be:
# Applying the rule: Maccro.register(:rewrite_sin_to_mine, 'Math.sin(v1)', 'my_own_sin(v1)')
# Before rewrite
def myfunc(x, y, z)
return [Math.sin(x), Math.sin(x + y), Math.sin(if x > y then z else 0 end)]
end
# After rewrite
def myfunc(x, y, z)
return [my_own_sin(x), Math.sin(x + y), Math.sin(if x > y then z else 0 end)]
# 2nd and 3rd expressions doesn't match to the rule
end
Placeholder details
Placeholders should be the combination of an alphabetic character and an integer. For example, e1
, v5
, v100
, etc. Any integer numbers are available, and there are no need to be continuous numbers (you can use e2
without e1
).
Placeholders can be used in both of "Before" and "After" code snippets, and placeholders used in "After" must be in "Before" too to capture codes to be referred in "After" (otherwise, placeholders in "After" will be left as-is).
Types of placeholders are defined by alphabetic chars:
-
v
: values (local variable, instance variable, class variable and global variable, )- local variables
- instance variables
- class variables
- global variables
- thread local variables (e.g.,
$1
etc) - constants
- strings
- regular expressions
- lambda, array, hash
- literals (integer, float, symbol, range, nil, true, false, etc)
- self
-
e
: expressions, any code which returns a value or values (- all values (matches to
v
) - if, unless, case
- and, or
- calls of functions, operators
- safe call operators (
&.
) - super, yield
- match with regular expressions (
=~
) defined?
- defining methods and singleton methods
- double and trible colon
::
and:::
- dots and flip-flop
- all values (matches to
-
s
: strings -
y
: symbols -
n
: numbers -
r
: regular expressions
Using a placeholder twice (or more)
TODO: using a placeholder twice in "Before" is not implemented now (it doesn't work correctly)
If "After" code contains a placeholder twice or more, these placeholders will be replaced with the same code snippet captured in "Before".
# Applying the rule: Maccro.register(:define_my_own_power, 'power(e1, 3)', 'e1 * e1 * e1')
# Before rewrite
def myfunc(x)
return power(x, 3)
end
# After rewrite
def myfunc(x)
return x * x * x
end
Rules for non-idempotent methods (methods which has side effects)
If the captured code has side effect and it'll be used more times than "Before", it'll be broken behavior.
# Applying the rule: Maccro.register(:define_longer_one, 'longer(e1, e2)', '(e1).length >= (e2).length ? e1 : e2')
# Before rewrite
def myfunc(str1, str2)
return longer(str1.succ!, str2.succ!)
end
# After rewrite
def myfunc(str1, str2)
return (str1.succ!).length >= (str2.succ!) ? str1.succ! : str2.succ!
end
That may cause unexpected results.
TODO: implement safe_reference option
Rules for limited source pattern
If the rule should rewrite the code which is surrounded a pattern of code, the under
option will help the situation.
Maccro.register(:rewrite_range_cover, '[e1, v1, e2]', '[(e1)...(e2)].cover?(v1)', under: 'my_dsl_function($TARGET)')'
This rule matches to the code in the code captured by $TARGET
. The placehodler used in under
option pattern is independent from the placeholders in "Before" and "After".
The example behavior is:
# Before rewrite
def myfunc(v)
if v > 1
my_dsl_function([1, v, 2] ? 1 : 2)
else
[1, v, 2] ? 1 : 2
end
end
# After rewrite
def myfunc(v)
if v > 1
my_dsl_function([(1)...(2)].cover?(v) ? 1 : 2)
else
[1, v, 2] ? 1 : 2
end
end
In this example, the [1, v, 2]
for the case of v > 1
is afftected by the rule because it's in the code for the argument of my_dsl_function
, but the other (for else) is not affected because there are no method call of my_dsl_function
.
Applying Rules
To apply registered rules on methods manually, call Maccro.apply
with the module/class and its method. Maccro will try to match all rules to the mthod.
# register rules, then
Maccro.apply(MyClass, MyClass.instance_method(:foo))
When you want to try the selected rules, call apply
method with rules
keyword argument.
Maccro.apply(MyClass, MyClass.instance_method(:foo), rules: [:rule1, :rule2, :rule3])
Using Built-in Rules
Maccro has many built-in rules, for continuing less/greater-than or equal-to, for mathematical intervals and ActiveRecord utilities. You can see the list of built-in rules here: RULE.
require 'maccro/builtin'
Maccro::Builtin.register(:built_in_rule_name)
# or register all built-in rules
Maccro::Builtin.register_all
Built-in rules can be fetched via Maccro::Builtin.rule(:name)
or Maccro::Builtin.rules(:name1, :name2, :name3, ...)
. These rules can be used for rules
of Maccro.apply
.
Enabling Automatic applying
Maccro has a feature to rewrite all defined methods using TracePoint. Users can enable Maccro only for a module/class or only for a path.
require 'maccro'
Maccro.register(...)
# enable Maccro for a module (MyModule must be defined before)
Maccro.enable(target: MyModule, rules: [:name1, :name2, ...])
# or, enable Maccro for this file, with all rules registered
Maccro.enable(path: __FILE__)
module MyModule
# ...
end
Without any options, Maccro.enable
enables all rules globally. That is strongly NOT recommended in libraries.
Maccro.enable
rewrite all method definitions, defined AFTER Maccro.enable()
. The methods defined before it will not be updated.
And Maccro.enable
rewrites methods at the end of module/class definition. So you need to take care about singleton methods which are called in the class/module definition.
For example:
Maccro.enable(path: __FILE__, rules: [:rewrite_foo_to_bar])
module MyModule
def self.foo
"foo" # this should be rewritten to "bar"
end
FOO = self.foo # this value is "foo" here
end # Maccro works here
foo = self.foo # this value is "bar" here
To enable Maccro globally to rewrite all defined methods by all registered rules, require the file for that. (That is strongly NOT recommended in libraries too!)
require 'maccro/rewrite_the_world'
Or run ruby with this library.
$ ruby -rmaccro/rewrite_the_world file_to_run.rb
API
Maccro#register(name, before, after, **kwarg_options)
Register a macro rule to the global dictionary.
- name: a symbol to represents the rule
- before: a string of Ruby code which matches to be rewritten
- after: a string of Ruby code which replaces the matched part
- kwarg_options:
- under: a string of Ruby code which matches to limit the affected area (must contain
$TARGET
) - safe_reference: TODO: (NOT IMPLEMENTED NOW)
- under: a string of Ruby code which matches to limit the affected area (must contain
Maccro#apply(module, method, **kwarg_options)
Apply the registered rules (or a set of rules specified) to a method.
- module: a module/class, the applied method is defined in
- method: a method object (an instance method or a singleton method)
- kwarg_options:
- rules: an array of symbols of rule names (default: all registered rules)
Maccro#rewrite(proc=nil, **kwarg_options, &block)
Rewrite a specified proc object by the registered rules (or a set of rules specified) and return the updated (rewritten) proc object.
- proc: a Proc object to be rewritten (exclusive with
block
) - block: a block parameter to be rewritten (exclusive with
proc
) - kwarg_options:
- rules: an array of symbols of rule names (default: all registered rules)
Maccro#execute(proc=nil, **kwarg_options, &block)
Rewrite a specified proc object and call it immediately. The proc must be with no arguments.
- proc: a Proc object to be rewritten (exclusive with
block
), which must be with no arguments - block: a block parameter to be rewritten (exclusive with
proc
), which must be with no arguments - kwarg_options:
- rules: an array of symbols of rule names (default: all registered rules)
Maccro#enable(**kwarg_options)
- kwarg_options:
- target: a module/class to enable Maccro to rewrite all methods defined (exclusive with path)
- path: a file path to enable Maccro to rewrite all methods defined (exclusive with target)
- rules: an array of symbols of rule names (default: all registered rules)
Maccro.enable
can be called for different targets or paths, but calling twice for the same target/path would make troubles.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/tagomoris/maccro.
License
The gem is available as open source under the terms of the MIT License.