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, in module or class, 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
  • 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.

Macro.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)

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.