kenma 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []