kenma 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 4ede02761e159c70e342821d38b8cea9d672e09b7c92ce66b9c21d20933e2b0a
4
+ data.tar.gz: 7a905f6782bc2189df6568ee2ea4c69a5ac5a2ca09da7b49b007094053d0bb1d
5
+ SHA512:
6
+ metadata.gz: bc4f726c242c41f70d66496585bfe5eac8b79426887e1806f17783f7b714e762e0da817fbcbc5d8d49390c58429b587cb07cebe993bae8058a5874ca91f32aa7
7
+ data.tar.gz: f21744fcc6f3dddef6c7768277b0fb9fb45607c43d06328162ee27bb5c9159c8fbb7506db2bd0c36583fca5b3b3b3fd88a5b155d039a2256097fa3e0e7e81a62
@@ -0,0 +1,23 @@
1
+ name: Ruby CI
2
+
3
+ on: [push, pull_request]
4
+
5
+ jobs:
6
+ test:
7
+
8
+ runs-on: ubuntu-latest
9
+
10
+ strategy:
11
+ matrix:
12
+ ruby-version: [2.6.4, 2.7.0, 2.7.2, 3.0.1]
13
+
14
+ steps:
15
+ - uses: actions/checkout@v2
16
+ - name: Set up Ruby ${{ matrix.ruby-version }}
17
+ uses: ruby/setup-ruby@v1
18
+ with:
19
+ ruby-version: ${{ matrix.ruby-version }}
20
+ - name: Install dependencies
21
+ run: bundle install
22
+ - name: Run tests
23
+ run: bundle exec rake
@@ -0,0 +1,18 @@
1
+ name: Ruby
2
+
3
+ on: [push,pull_request]
4
+
5
+ jobs:
6
+ build:
7
+ runs-on: ubuntu-latest
8
+ steps:
9
+ - uses: actions/checkout@v2
10
+ - name: Set up Ruby
11
+ uses: ruby/setup-ruby@v1
12
+ with:
13
+ ruby-version: 3.0.1
14
+ - name: Run the default task
15
+ run: |
16
+ gem install bundler -v 2.2.15
17
+ bundle install
18
+ bundle exec rake
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in kenma.gemspec
6
+ gemspec
7
+
8
+ gem "rake", "~> 13.0"
9
+
10
+ gem "rspec", "~> 3.0"
11
+ gem "super_diff"
12
+ gem "rensei", github: "osyo-manga/gem-rensei", branch: :master
data/README.md ADDED
@@ -0,0 +1,119 @@
1
+ [![Ruby CI](https://github.com/osyo-manga/gem-kenma/actions/workflows/kenma.yml/badge.svg)](https://github.com/osyo-manga/gem-kenma/actions/workflows/kenma.yml)
2
+
3
+ # Kenma
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'kenma'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle install
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install kenma
20
+
21
+ ## Usage
22
+
23
+ ### Macro function
24
+
25
+ ```ruby
26
+ require "kenma"
27
+
28
+ using Kenma::Refine::Source
29
+
30
+ # Define Macro module
31
+ module CatMacro
32
+ using Kenma::Macroable
33
+
34
+ # Define Macro function
35
+ # Macro function is
36
+ # input -> RubyVM::AST::Node
37
+ # output -> RubyVM::AST::Node
38
+ def cat!(num = ast { 1 })
39
+ # ast return AST::Node in block
40
+ # $num is bind variable `num`
41
+ ast { "にゃーん" * $num }
42
+ end
43
+ macro_function :cat!
44
+ end
45
+
46
+ body = proc {
47
+ use_macro! CatMacro
48
+ puts cat!
49
+ puts cat!(3)
50
+ }
51
+ # Apply Macro functions
52
+ puts Kenma.compile_of(body).source
53
+ # => begin puts(("にゃーん" * 1)); puts(("にゃーん" * 3)); end
54
+ ```
55
+
56
+ ### Macro defines
57
+
58
+ ```ruby
59
+ require "kenma"
60
+
61
+ using Kenma::Refine::Source
62
+ using Kenma::Refine::Nodable
63
+
64
+ # Macro module
65
+ # defined macros in
66
+ # priority is
67
+ # node macro > function macro > pattern macro
68
+ module MyMacro
69
+ using Kenma::Macroable
70
+
71
+ # Node macro
72
+ # Replace the AST of a specific node with the AST of the return value
73
+ def frozen_string(str_node, parent_node)
74
+ ast { $str_node.freeze }
75
+ end
76
+ macro_node :STR, :frozen_string
77
+
78
+ # Function macro
79
+ # Replace the AST from which the function is called with the AST of the return value
80
+ def cat!(num_not = ast { 1 })
81
+ ast { "にゃーん" * $num_not }
82
+ end
83
+ macro_function :cat!
84
+
85
+ # Pattern macro
86
+ # Replace the AST that matches the pattern with the AST of the return value
87
+ def frozen(node, name:, value:)
88
+ ast { $name = $value.freeze }
89
+ end
90
+ macro_pattern pat { $name = $value }, :frozen
91
+ end
92
+
93
+
94
+ body = proc {
95
+ use_macro! MyMacro
96
+
97
+ "にゃーん" # => "にゃーん".freeze
98
+
99
+ puts cat! # => puts "にゃーん"
100
+ puts cat!(3) # => puts "にゃーん" * 3
101
+
102
+ value = [1, 2, 3] # => value = [1, 2, 3].freeze
103
+ }
104
+
105
+ result = Kenma.compile_of(body)
106
+ puts result.source
107
+ # => begin "にゃーん".freeze(); puts(("にゃーん" * 1)); puts(("にゃーん" * 3)); (value = [1, 2, 3].freeze()); end
108
+ ```
109
+
110
+
111
+ ## Development
112
+
113
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
114
+
115
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
116
+
117
+ ## Contributing
118
+
119
+ Bug reports and pull requests are welcome on GitHub at https://github.com/osyo-manga/gem-kenma.
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "kenma"
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require "irb"
15
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,9 @@
1
+ #!/bin/bash -ex
2
+ RBENV_VERSION=2.6.4 bundle install
3
+ RBENV_VERSION=2.7.0 bundle install
4
+ RBENV_VERSION=2.7.2 bundle install
5
+ RBENV_VERSION=2.7.3 bundle install
6
+ RBENV_VERSION=2.7.4 bundle install
7
+ RBENV_VERSION=3.0.0 bundle install
8
+ RBENV_VERSION=3.0.1 bundle install
9
+ RBENV_VERSION=3.1.0-dev bundle install
data/kenma.gemspec ADDED
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/kenma/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "kenma"
7
+ spec.version = Kenma::VERSION
8
+ spec.authors = ["manga_osyo"]
9
+ spec.email = ["manga.osyo@gmail.com"]
10
+
11
+ spec.summary = "AST Macro in Ruby"
12
+ spec.description = "AST Macro in Ruby"
13
+ spec.homepage = "https://github.com/osyo-manga/gem-kenma"
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.6.0")
16
+
17
+ # Specify which files should be added to the gem when it is released.
18
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
19
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
20
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
21
+ end
22
+ spec.bindir = "exe"
23
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
24
+ spec.require_paths = ["lib"]
25
+
26
+ # Uncomment to register a new dependency of your gem
27
+ # spec.add_dependency "example-gem", "~> 1.0"
28
+
29
+ # For more information and examples about making a new gem, checkout our
30
+ # guide at: https://bundler.io/guides/creating_gem.html
31
+
32
+ spec.add_dependency "rensei", "~> 0.2.0"
33
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "./refine/nodable.rb"
4
+
5
+ module Kenma
6
+ module Iteration
7
+ using Kenma::Refine::Nodable
8
+
9
+ KENMA_ITERATION_MACRO_EMPTY_NODE = Object.new.freeze
10
+ private_constant :KENMA_ITERATION_MACRO_EMPTY_NODE
11
+
12
+ module_function
13
+
14
+ def each_node(node, &block)
15
+ return Enumerator.new { |y|
16
+ each_node(node) { |node|
17
+ y << node
18
+ }
19
+ } unless block
20
+ return node unless node.node?
21
+
22
+ node.children.map { |node|
23
+ each_node(node) { |child| block.call(child, node) }
24
+ }
25
+ block.call(node) if block
26
+ end
27
+
28
+ def convert_node(node, parent = nil, &block)
29
+ return node unless node.node?
30
+
31
+ children = node.children
32
+ converted_children = children
33
+ .map { |child| convert_node(child, node) { |node, parent| block.call(node, parent) || KENMA_ITERATION_MACRO_EMPTY_NODE } }
34
+ .reject { |it| KENMA_ITERATION_MACRO_EMPTY_NODE == it }
35
+
36
+ if converted_children == children
37
+ node
38
+ else
39
+ [node.type, converted_children]
40
+ end.then { |node| block.call(node, parent) }
41
+ end
42
+
43
+ def find_convert_node(node, pat, &block)
44
+ convert_node(node) { |node|
45
+ if result = pat === node
46
+ block.call(node, **result)
47
+ else
48
+ node
49
+ end
50
+ }
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../macroable.rb"
4
+
5
+ module Kenma
6
+ module Macro
7
+ module Basic
8
+ using Kenma::Macroable
9
+ using Kenma::Refine::Source
10
+ using Kenma::Refine::Nodable
11
+
12
+ def symbolify!(args)
13
+ [:LIT, [args.source.to_sym]]
14
+ end
15
+ macro_function :symbolify!
16
+
17
+ def stringify!(args)
18
+ if args.respond_to?(:source)
19
+ [:STR, [args.source]]
20
+ else
21
+ [:STR, [args.to_s]]
22
+ end
23
+ end
24
+ macro_function :stringify!
25
+
26
+ def unstringify!(args)
27
+ RubyVM::AbstractSyntaxTree.parse(bind.eval(args.source)).children.last
28
+ end
29
+ macro_function :unstringify!
30
+
31
+ def node_bind!(args)
32
+ bind.eval(args.source)
33
+ end
34
+ macro_function :node_bind!
35
+
36
+ def eval!(args)
37
+ data = bind.eval(args.source)
38
+ RubyVM::AbstractSyntaxTree.parse(data.inspect).children.last
39
+ end
40
+ macro_function :eval!
41
+
42
+ # $node
43
+ def NODE_GVAR(node, parent)
44
+ name = node.children.first.to_s.delete_prefix("$").to_sym
45
+ if bind.local_variable_defined?(name)
46
+ bind.local_variable_get(name)
47
+ else
48
+ node
49
+ end
50
+ end
51
+
52
+ # $left = right
53
+ def NODE_GASGN(node, parent)
54
+ left, right = node.children
55
+ name = left.to_s.delete_prefix("$").to_sym
56
+ if bind.local_variable_defined?(name)
57
+ [:GASGN, [bind.local_variable_get(name), right]]
58
+ else
59
+ node
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../macroable.rb"
4
+ require_relative "./macro_node.rb"
5
+
6
+ module Kenma
7
+ module Macro
8
+ module FrozenConstant
9
+ using Kenma::Macroable
10
+
11
+ def _frozen_constant_decl(node, parent)
12
+ left, right = node.children
13
+ ast { $left = $right.freeze }
14
+ end
15
+ macro_node :CDECL, :_frozen_constant_decl
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../macroable.rb"
4
+
5
+ module Kenma
6
+ module Macro
7
+ module FrozenStringLiteral
8
+ using Kenma::Macroable
9
+
10
+ def frozen_string(node, parent)
11
+ ast { $node.freeze }
12
+ end
13
+ macro_node :STR, :frozen_string
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../macroable.rb"
4
+ require_relative "./macro_node.rb"
5
+
6
+ module Kenma
7
+ module Macroable
8
+ refine Module do
9
+ def macro_function(name)
10
+ macro_functions << name
11
+ module_function name
12
+ extend Kenma::Macroable
13
+ end
14
+
15
+ def macro_functions
16
+ @macro_functions ||= []
17
+ end
18
+ end
19
+ end
20
+
21
+ module Macro
22
+ module MacroFunction
23
+ using Kenma::Macroable
24
+ using Kenma::Refine::Source
25
+
26
+ using Module.new {
27
+ refine MacroFunction do
28
+ using Kenma::Macroable
29
+ def macro_functions
30
+ singleton_class.ancestors.grep(Kenma::Macroable).map(&:macro_functions).inject([], &:+)
31
+ end
32
+
33
+ def send_macro_function(method_name, args, &block)
34
+ converted_args = compile(args)
35
+ send(method_name, *converted_args&.children&.compact, &block)
36
+ end
37
+ end
38
+ }
39
+
40
+ private
41
+
42
+ def _FCALL_send_macro_function(node, parent)
43
+ return node if parent&.type == :ITER
44
+ method_name, args = node.children
45
+ if macro_functions.include?(method_name)
46
+ send_macro_function(method_name, args)
47
+ else
48
+ node
49
+ end
50
+ end
51
+ macro_node :FCALL, :_FCALL_send_macro_function
52
+
53
+ def _ITER_send_macro_function(node, parent)
54
+ fcall, scope = node.children
55
+ method_name, args = fcall.children
56
+ if macro_functions.include?(method_name)
57
+ send_macro_function(method_name, args) { scope }
58
+ else
59
+ node
60
+ end
61
+ end
62
+ macro_node :ITER, :_ITER_send_macro_function
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../macroable.rb"
4
+
5
+ module Kenma
6
+ module Macroable
7
+ refine Module do
8
+ def macro_node(node_type, name)
9
+ define_method("NODE_#{node_type}") { |node, parent|
10
+ send(name, node, parent)
11
+ }
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../macroable.rb"
4
+ require_relative "./macro_node.rb"
5
+
6
+ module Kenma
7
+ using Kenma::Refine::Nodable
8
+
9
+ class PatternCapture < Struct.new(:pat_node)
10
+ def initialize(pat_node)
11
+ self.pat_node = pat_node
12
+ end
13
+
14
+ def match(node)
15
+ _match(node, pat_node)
16
+ end
17
+ alias_method :===, :match
18
+
19
+ private
20
+
21
+ def _match(node, pat)
22
+ return {} if node == pat
23
+ return nil if node.nil? || pat.nil?
24
+ return nil if !node.node? || !pat.node?
25
+
26
+ if pat.type == :GASGN && node.type.to_s =~ /ASGN|CDECL/
27
+ match_result = _match(node.children.last, pat.children.last)
28
+ if match_result
29
+ tag = pat.children.first.to_s.delete_prefix("$").to_sym
30
+ { tag => node.children.first }.merge(_match(node.children.last, pat.children.last))
31
+ else
32
+ nil
33
+ end
34
+ elsif pat.type == :GVAR
35
+ tag = pat.children.first.to_s.delete_prefix("$").to_sym
36
+ { tag => node }
37
+ # Add Support: pat { [*$node] } === ast { [a, b, c] } # => { node: [a, b, c] }
38
+ # Not Support: pat { [$first, *$node] } === ast { [a, b, c] } # => nil
39
+ elsif pat.type === :SPLAT && pat.children.first.type == :GVAR
40
+ tag = pat.children.first.children.first.to_s.delete_prefix("$").to_sym
41
+ { tag => node }
42
+ elsif node.type == pat.type && node.children.size == pat.children.size
43
+ node.children.zip(pat.children).inject({}) { |result, (src, pat)|
44
+ if match_result = _match(src, pat)
45
+ result.merge(match_result)
46
+ else
47
+ break nil
48
+ end
49
+ }
50
+ else
51
+ nil
52
+ end
53
+ end
54
+ end
55
+
56
+ module Macroable
57
+ refine Kernel do
58
+ def pat(&block)
59
+ pat_node(RubyVM::AbstractSyntaxTree.of(block).children.last)
60
+ end
61
+
62
+ def pat_node(node)
63
+ PatternCapture.new(node)
64
+ end
65
+ end
66
+
67
+ refine Module do
68
+ def macro_pattern(pattern, name)
69
+ macro_patterns[name] = pattern
70
+ extend Kenma::Macroable
71
+ end
72
+
73
+ def macro_patterns
74
+ @macro_patterns ||= {}
75
+ end
76
+ end
77
+ end
78
+
79
+ module Macro
80
+ module MacroPattern
81
+ using Kenma::Macroable
82
+ using Kenma::Refine::Source
83
+
84
+ using Module.new {
85
+ refine MacroPattern do
86
+ using Kenma::Macroable
87
+ def macro_patterns
88
+ singleton_class.ancestors.grep(Kenma::Macroable).map(&:macro_patterns).inject({}, &:merge)
89
+ end
90
+ end
91
+ }
92
+
93
+ def node_missing(node, parent)
94
+ macro = macro_patterns.lazy.reverse_each.map { |name, pat| [name, pat.match(node)] }.find { |name, captured| captured }
95
+ if macro
96
+ if macro[1].empty?
97
+ send(macro[0], node)
98
+ else
99
+ send(macro[0], node, **macro[1])
100
+ end
101
+ else
102
+ super(node, parent)
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../macroable.rb"
4
+ require_relative "./macro_function.rb"
5
+
6
+ module Kenma
7
+ module Macro
8
+ module UseMacro
9
+ using Kenma::Macroable
10
+ using Kenma::Refine::Nodable
11
+ using Kenma::Refine::Source
12
+
13
+ def initialize(context = {})
14
+ super
15
+ extend *use_macros.reverse unless use_macros.empty?
16
+ end
17
+
18
+ def use_macro!(node)
19
+ macro_mod = bind.eval(node.source)
20
+ use_macros << macro_mod
21
+ extend macro_mod
22
+ nil
23
+ end
24
+ macro_function :use_macro!
25
+
26
+ private
27
+
28
+ def use_macro(mod)
29
+
30
+ end
31
+
32
+ def use_macros
33
+ scope_context[:use_macros] ||= []
34
+ end
35
+
36
+ def scope_context_switch(context, &block)
37
+ super(context) { |converter|
38
+ converter.extend *use_macros.reverse unless use_macros.empty?
39
+ block.call(converter)
40
+ }
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "./node_converter.rb"
4
+ require_relative "./refine/node_to_a.rb"
5
+
6
+ module Kenma
7
+ module Macroable
8
+ refine Kernel do
9
+ def ast(context = {}, &body)
10
+ bind = body.binding unless context[:bind]
11
+ node = RubyVM::AbstractSyntaxTree.of(body).children.last
12
+ Kenma.compile(node, { bind: bind }.merge(context))
13
+ end
14
+ end
15
+
16
+ # TODO: duplicate Reifne::Nodable
17
+ refine Array do
18
+ def type; self[0]; end
19
+ def children; self[1]; end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "./refine/source.rb"
4
+ require_relative "./refine/nodable.rb"
5
+ require_relative "./refine/node_iteration.rb"
6
+
7
+ module Kenma
8
+ class NodeConverter
9
+ using Kenma::Refine::Source
10
+ using Kenma::Refine::Nodable
11
+
12
+ def initialize(context = {})
13
+ @scope_context = context
14
+ end
15
+
16
+ def convert(node)
17
+ _convert(node) { |node, parent|
18
+ method_name = "NODE_#{node.type}"
19
+ send_node(method_name, node, parent)
20
+ }
21
+ end
22
+
23
+ private
24
+
25
+ KENMA_MACRO_EMPTY_NODE = Object.new.freeze
26
+ private_constant :KENMA_MACRO_EMPTY_NODE
27
+
28
+ attr_reader :bind
29
+ attr_reader :scope_context
30
+
31
+ def bind
32
+ scope_context[:bind]
33
+ end
34
+
35
+ def scope_context_switch(context, &block)
36
+ self.class.new(scope_context.merge(context)).then(&block)
37
+ end
38
+
39
+ def _convert(node, &block)
40
+ return node unless node.node?
41
+
42
+ if node.type == :SCOPE
43
+ return scope_context_switch(scope_context) { |converter|
44
+ children = [*node.children.take(node.children.size-1), converter.convert(node.children.last)]
45
+ .reject { |it| KENMA_MACRO_EMPTY_NODE == it }
46
+ send_node(:NODE_SCOPE, [:SCOPE, children], node)
47
+ }
48
+ end
49
+
50
+ children = node.children
51
+ converted_children = children
52
+ .map { |node| _convert(node) { |child| block.call(child, node) || KENMA_MACRO_EMPTY_NODE } }
53
+ .reject { |it| KENMA_MACRO_EMPTY_NODE == it }
54
+
55
+ if converted_children == children
56
+ node
57
+ else
58
+ [node.type, converted_children]
59
+ end.then { |node| block.call(node, nil) }
60
+ end
61
+
62
+ def send_node(method_name, node, parent)
63
+ if respond_to?(method_name, true)
64
+ result = send(method_name, node, parent)
65
+ if result == node
66
+ node_missing(node, parent)
67
+ else
68
+ result
69
+ end
70
+ else
71
+ node_missing(node, parent)
72
+ end
73
+ end
74
+
75
+ def node_missing(node, parent)
76
+ node
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "./node_converter.rb"
4
+ require_relative "./macro/use_macro.rb"
5
+ require_relative "./macro/basic.rb"
6
+ require_relative "./macro/macro_function.rb"
7
+ require_relative "./macro/macro_node.rb"
8
+ require_relative "./macro/macro_pattern.rb"
9
+
10
+ module Kenma
11
+ class PreProcessor < Kenma::NodeConverter
12
+ include Macro::UseMacro
13
+ include Macro::Basic
14
+ include Macro::MacroFunction
15
+ include Macro::MacroPattern
16
+
17
+ alias_method :compile, :convert
18
+
19
+ def self.compile(node, context = {})
20
+ new(context).compile(node)
21
+ end
22
+
23
+ def self.compile_of(body, context = {})
24
+ bind = body.binding unless context[:bind]
25
+ compile(RubyVM::AbstractSyntaxTree.of(body), { bind: bind }.merge(context))
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rensei"
4
+
5
+ module Kenma
6
+ module Refine
7
+ module Nodable
8
+ refine Object do
9
+ def node?; false; end
10
+ end
11
+
12
+ refine RubyVM::AbstractSyntaxTree::Node do
13
+ def node?
14
+ true
15
+ end
16
+ end
17
+
18
+ refine Hash do
19
+ def type; self[:type]; end
20
+
21
+ def children; self[:children]; end
22
+
23
+ def node?; !empty?; end
24
+ end
25
+
26
+ refine Array do
27
+ def type; self[0]; end
28
+
29
+ def children; self[1]; end
30
+
31
+ def node?
32
+ self.size == 2 &&
33
+ self[0].kind_of?(Symbol) && self[0] == self[0].upcase &&
34
+ self[1].kind_of?(Array)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rensei"
4
+ require_relative "./nodable.rb"
5
+ require_relative "../iteration.rb"
6
+
7
+ module Kenma
8
+ module Refine
9
+ module NodeIteration
10
+ def each_node(&block)
11
+ Kenma::Iteration.each_node(self, &block)
12
+ end
13
+
14
+ def convert_node(&block)
15
+ Kenma::Iteration.convert_node(self, &block)
16
+ end
17
+
18
+ def find_convert_node(pat, &block)
19
+ Kenma::Iteration.find_convert_node(self, pat, &block)
20
+ end
21
+
22
+ refine Array do
23
+ include Kenma::Refine::NodeIteration
24
+ end
25
+
26
+ refine RubyVM::AbstractSyntaxTree::Node do
27
+ include Kenma::Refine::NodeIteration
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rensei"
4
+ require_relative "./nodable.rb"
5
+
6
+ module Kenma
7
+ module Refine
8
+ module NodeToArray
9
+ refine RubyVM::AbstractSyntaxTree::Node do
10
+ using Module.new {
11
+ [
12
+ Object,
13
+ NilClass,
14
+ FalseClass,
15
+ TrueClass,
16
+ String,
17
+ Hash,
18
+ Array,
19
+ Symbol,
20
+ Numeric,
21
+ Regexp,
22
+ ].each { |klass|
23
+ refine klass do
24
+ def to_a(*)
25
+ self
26
+ end
27
+ end
28
+
29
+ }
30
+ }
31
+
32
+ def to_a
33
+ [type, children.map { |it| it.to_a }]
34
+ end
35
+ end
36
+
37
+ refine Array do
38
+ using Module.new {
39
+ [
40
+ Object,
41
+ NilClass,
42
+ FalseClass,
43
+ TrueClass,
44
+ String,
45
+ Hash,
46
+ Symbol,
47
+ Numeric,
48
+ Regexp,
49
+ ].each { |klass|
50
+ refine klass do
51
+ def to_a(*)
52
+ self
53
+ end
54
+ end
55
+
56
+ }
57
+ }
58
+
59
+ using NodeToArray
60
+
61
+ def to_a(*)
62
+ map { |it| it.to_a }
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rensei"
4
+
5
+ module Kenma
6
+ module Refine
7
+ module Source
8
+ refine RubyVM::AbstractSyntaxTree::Node do
9
+ if RUBY_VERSION >= "3.1.0"
10
+ def source
11
+ super.tap { |it|
12
+ break Rensei.unparse(self) if it.nil?
13
+ }
14
+ end
15
+ else
16
+ def source
17
+ Rensei.unparse(self)
18
+ end
19
+ end
20
+ end
21
+
22
+ refine Hash do
23
+ def source; Rensei.unparse(self.dup); end
24
+ end
25
+
26
+ refine Array do
27
+ def source; Rensei.unparse(self.dup); end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kenma
4
+ VERSION = "0.1.0"
5
+ end
data/lib/kenma.rb ADDED
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "./kenma/version"
4
+ require_relative "./kenma/pre_processor.rb"
5
+ require_relative "./kenma/iteration.rb"
6
+
7
+ module Kenma
8
+ using Kenma::Refine::Source
9
+
10
+ def self.compile(body, context = {})
11
+ PreProcessor.compile(body, context)
12
+ end
13
+
14
+ def self.compile_of(body, context = {})
15
+ PreProcessor.compile_of(body, context)
16
+ end
17
+
18
+ def self.macro_eval(context = {}, &body)
19
+ body.binding.eval Kenma.compile_of(body).source
20
+ end
21
+ end
data/rake_spec_all.sh ADDED
@@ -0,0 +1,6 @@
1
+ #!/bin/bash -ex
2
+ RBENV_VERSION=2.6.4 bundle exec rake spec
3
+ RBENV_VERSION=2.7.0 bundle exec rake spec
4
+ RBENV_VERSION=2.7.2 bundle exec rake spec
5
+ RBENV_VERSION=3.0.1 bundle exec rake spec
6
+ RBENV_VERSION=3.1.0-dev bundle exec rake spec
@@ -0,0 +1,56 @@
1
+ # a < b < c
2
+ # ↓
3
+ # a < b && b < c
4
+ require "kenma"
5
+
6
+ using Kenma::Refine::Source
7
+
8
+ module ChainingComparisonOperatorsMacro
9
+ using Kenma::Macroable
10
+ using Kenma::Refine::NodeToArray
11
+
12
+ def chaining_comparison_operators(node, parent)
13
+ case node.to_a
14
+ in [:OPCALL, [[:OPCALL, [left, op1, [:LIST, [middle, nil]]]], op2, [:LIST, [right, nil]]]]
15
+ ast { $left.send(eval!(op1), $middle) && $middle.send(eval!(op2), $right) }
16
+ else
17
+ node
18
+ end
19
+ end
20
+ macro_node :OPCALL, :chaining_comparison_operators
21
+ end
22
+
23
+ body = proc {
24
+ use_macro! ChainingComparisonOperatorsMacro
25
+
26
+ 0 <= value < 10
27
+ }
28
+
29
+ result = Kenma.compile_of(body)
30
+ puts result.source
31
+
32
+ body = proc {
33
+ use_macro! ChainingComparisonOperatorsMacro
34
+
35
+ def check(value)
36
+ # to 0 <= input && input < 10
37
+ if 0 <= value < 10
38
+ "OK"
39
+ else
40
+ "NG"
41
+ end
42
+ end
43
+ check(10)
44
+
45
+ puts check(5) # => "OK"
46
+ puts check(20) # => "NG"
47
+ }
48
+ result = Kenma.compile_of(body)
49
+ puts result.source
50
+ # => begin def check(value)
51
+ # (if (0.send(:<=, value) && value.send(:<, 10))
52
+ # "OK"
53
+ # else
54
+ # "NG"
55
+ # end)
56
+ # end; check(10); puts(check(5)); puts(check(20)); end
@@ -0,0 +1,24 @@
1
+ # debug! 1 + 2
2
+ # ↓
3
+ # puts "1 + 2 # => #{1 + 2}"
4
+ require "kenma"
5
+
6
+ using Kenma::Refine::Source
7
+
8
+ module DebugMacro
9
+ using Kenma::Macroable
10
+
11
+ def debug!(expr)
12
+ ast { puts "#{stringify! $expr} # => #{$expr}" }
13
+ end
14
+ macro_function :debug!
15
+ end
16
+
17
+ body = proc {
18
+ use_macro! DebugMacro
19
+
20
+ debug! 1 + 2 + 3
21
+ debug! [1, 2, 3].map { _1 + _1 }
22
+ }
23
+ puts Kenma.compile_of(body).source
24
+ # => begin puts("#{"((1 + 2) + 3)"} # => #{((1 + 2) + 3)}"); puts("#{"[1, 2, 3].map() { (_1 + _1) }"} # => #{[1, 2, 3].map() { (_1 + _1) }}"); end
@@ -0,0 +1,26 @@
1
+ require "kenma"
2
+
3
+ using Kenma::Refine::Source
4
+
5
+ module MultiUsingMacro
6
+ using Kenma::Macroable
7
+ using Kenma::Refine::NodeToArray
8
+
9
+ def using(*args)
10
+ args.compact.inject(ast { {} }) { |result, name|
11
+ ast { $result; using $name }
12
+ }
13
+ end
14
+ macro_function :using
15
+ end
16
+
17
+
18
+ body = proc {
19
+ use_macro! MultiUsingMacro
20
+
21
+ using Hoge, Foo, Bar
22
+ }
23
+
24
+ result = Kenma.compile_of(body)
25
+ puts result.source
26
+ # => begin begin begin {}; using(Hoge); end; using(Foo); end; using(Bar); end;
@@ -0,0 +1,24 @@
1
+ require "kenma"
2
+
3
+ using Kenma::Refine::Source
4
+
5
+ module ShorthandHashLiteralMacro
6
+ using Kenma::Macroable
7
+
8
+ def shorthand_hash_literal(node, args:)
9
+ args.children.compact.inject(ast { {} }) { |result, name|
10
+ ast { $result.merge({ symbolify!($name) => $name }) }
11
+ }
12
+ end
13
+ macro_pattern pat { ![*$args] }, :shorthand_hash_literal
14
+ end
15
+
16
+ body = proc {
17
+ use_macro! ShorthandHashLiteralMacro
18
+
19
+ ![a, b, c]
20
+ }
21
+
22
+ result = Kenma.compile_of(body)
23
+ puts result.source
24
+ # => {}.merge({ a: a }).merge({ b: b }).merge({ c: c });
metadata ADDED
@@ -0,0 +1,90 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: kenma
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - manga_osyo
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2021-09-10 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rensei
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 0.2.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.2.0
27
+ description: AST Macro in Ruby
28
+ email:
29
+ - manga.osyo@gmail.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - ".github/workflows/kenma.yml"
35
+ - ".github/workflows/main.yml"
36
+ - ".gitignore"
37
+ - ".rspec"
38
+ - Gemfile
39
+ - README.md
40
+ - Rakefile
41
+ - bin/console
42
+ - bin/setup
43
+ - bundle_install_all.sh
44
+ - kenma.gemspec
45
+ - lib/kenma.rb
46
+ - lib/kenma/iteration.rb
47
+ - lib/kenma/macro/basic.rb
48
+ - lib/kenma/macro/frozen_constant.rb
49
+ - lib/kenma/macro/frozen_string_literal.rb
50
+ - lib/kenma/macro/macro_function.rb
51
+ - lib/kenma/macro/macro_node.rb
52
+ - lib/kenma/macro/macro_pattern.rb
53
+ - lib/kenma/macro/use_macro.rb
54
+ - lib/kenma/macroable.rb
55
+ - lib/kenma/node_converter.rb
56
+ - lib/kenma/pre_processor.rb
57
+ - lib/kenma/refine/nodable.rb
58
+ - lib/kenma/refine/node_iteration.rb
59
+ - lib/kenma/refine/node_to_a.rb
60
+ - lib/kenma/refine/source.rb
61
+ - lib/kenma/version.rb
62
+ - rake_spec_all.sh
63
+ - sample/chaining_comparison_operators_macro.rb
64
+ - sample/debug_macro.rb
65
+ - sample/multi_using_macro.rb
66
+ - sample/shorthand_hash_literal_macro.rb
67
+ homepage: https://github.com/osyo-manga/gem-kenma
68
+ licenses:
69
+ - MIT
70
+ metadata: {}
71
+ post_install_message:
72
+ rdoc_options: []
73
+ require_paths:
74
+ - lib
75
+ required_ruby_version: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: 2.6.0
80
+ required_rubygems_version: !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ version: '0'
85
+ requirements: []
86
+ rubygems_version: 3.3.0.dev
87
+ signing_key:
88
+ specification_version: 4
89
+ summary: AST Macro in Ruby
90
+ test_files: []