vernacular 0.0.1
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 +7 -0
- data/.gitignore +12 -0
- data/.rubocop.yml +18 -0
- data/.travis.yml +6 -0
- data/Gemfile +3 -0
- data/LICENSE +21 -0
- data/README.md +127 -0
- data/Rakefile +19 -0
- data/bin/console +7 -0
- data/bin/setup +6 -0
- data/lib/vernacular.rb +62 -0
- data/lib/vernacular/ast_modifier.rb +60 -0
- data/lib/vernacular/ast_parser.rb +88 -0
- data/lib/vernacular/configuration_hash.rb +20 -0
- data/lib/vernacular/modifiers/date_sigil.rb +22 -0
- data/lib/vernacular/modifiers/number_sigil.rb +13 -0
- data/lib/vernacular/modifiers/typed_method_args.rb +64 -0
- data/lib/vernacular/modifiers/typed_method_returns.rb +64 -0
- data/lib/vernacular/modifiers/uri_sigil.rb +11 -0
- data/lib/vernacular/regex_modifier.rb +25 -0
- data/lib/vernacular/source_file.rb +42 -0
- data/lib/vernacular/version.rb +3 -0
- data/vernacular.gemspec +31 -0
- metadata +150 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 552dab9651e6e6fb7732220f14ab9c1efd5b92a0
|
4
|
+
data.tar.gz: 60ec95cc675a2e7f68d5e2e06d7cfffa0ffa9c40
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 17f64c2e0948369a0004e058a0fb6f131f832f8a43f9ff601a34ba74f6855dd63b93ef6da3a711170c21d485e598763e303bbeb929596e44e0e5cbc831b5d21b
|
7
|
+
data.tar.gz: f7d4ad0f7ee1b41f97e2d50da16f6fdd1abdcde2f3ba354c5423052d912398f8a391b5b48c25ca5f1fbbd1dd1ae03aaef50d2400b73c7a453ae3ad7f6c7e3ca3
|
data/.gitignore
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
AllCops:
|
2
|
+
DisplayCopNames: true
|
3
|
+
DisplayStyleGuide: true
|
4
|
+
TargetRubyVersion: 2.3
|
5
|
+
Exclude:
|
6
|
+
- 'lib/vernacular/parser.rb'
|
7
|
+
- 'test/date_sigil_test.rb'
|
8
|
+
- 'test/number_sigil_test.rb'
|
9
|
+
- 'test/type_safe_method_args_test.rb'
|
10
|
+
- 'test/type_safe_method_returns_test.rb'
|
11
|
+
- 'test/uri_sigil_test.rb'
|
12
|
+
- 'vendor/**/*'
|
13
|
+
|
14
|
+
Style/FormatString:
|
15
|
+
EnforcedStyle: percent
|
16
|
+
|
17
|
+
Style/FrozenStringLiteralComment:
|
18
|
+
Enabled: false
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2017 Kevin Deisz
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,127 @@
|
|
1
|
+
# Vernacular
|
2
|
+
|
3
|
+
[](https://travis-ci.org/kddeisz/vernacular)
|
4
|
+
|
5
|
+
Allows extending ruby's syntax and compilation process.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
gem 'vernacular'
|
13
|
+
```
|
14
|
+
|
15
|
+
And then execute:
|
16
|
+
|
17
|
+
$ bundle
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
|
21
|
+
$ gem install vernacular
|
22
|
+
|
23
|
+
## Usage
|
24
|
+
|
25
|
+
At the very beginning of your script or application, require `vernacular`. Then, configure your list of modifiers so that `vernacular` knows how to modify your code before it is compiled.
|
26
|
+
|
27
|
+
For example,
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
Vernacular.configure do |config|
|
31
|
+
pattern = /~n\(([\d\s+-\/*\(\)]+?)\)/
|
32
|
+
modifier =
|
33
|
+
Vernacular::Modifiers::RegexModifier.new(pattern) do |match|
|
34
|
+
eval(match[3..-2])
|
35
|
+
end
|
36
|
+
config.add(modifier)
|
37
|
+
end
|
38
|
+
```
|
39
|
+
|
40
|
+
will extend Ruby syntax to allow `~n(...)` symbols which will evaluate the interior expression as one number. This reduces the number of objects and instructions allocated for a given segment of Ruby, which can improve performance and memory.
|
41
|
+
|
42
|
+
### `Modifiers`
|
43
|
+
|
44
|
+
Modifiers allow you to modify the source of the Ruby code before it is compiled by injecting themselves into the require chain through `RubyVM::InstructionSequence::load_iseq`.
|
45
|
+
|
46
|
+
### `Modifiers::RegexModifier`
|
47
|
+
|
48
|
+
Regex modifiers are by far the simpler of the two to configure. They take the same arguments as `String#gsub`. Either configure them with a string, as in:
|
49
|
+
|
50
|
+
```ruby
|
51
|
+
Vernacular::Modifiers::RegexModifier.new(/~u\((.+?)\)/, 'URI.parse("\1")')
|
52
|
+
```
|
53
|
+
|
54
|
+
or configure them using a block, as in:
|
55
|
+
|
56
|
+
```ruby
|
57
|
+
Vernacular::Modifiers::RegexModifier.new(pattern) do |match|
|
58
|
+
eval(match[3..-2])
|
59
|
+
end
|
60
|
+
```
|
61
|
+
|
62
|
+
### `Modifiers::ASTModifier`
|
63
|
+
|
64
|
+
AST modifiers are somewhat more difficult to configure. A basic knowledge of the [`parser`](https://github.com/whitequark/parser) gem is required. First, extend the `Parser` to understand the additional syntax that you're trying to add. Second, extend the `Builder` with information about how to build s-expressions with your extra information. Finally, extend the `Rewriter` with code that will modify your extended AST by rewriting into a valid Ruby AST. An example is below:
|
65
|
+
|
66
|
+
```ruby
|
67
|
+
Vernacular::Modifiers::ASTModifier.new do |modifier|
|
68
|
+
# Extend the parser to support and equal sign and a class path following the
|
69
|
+
# declaration of a functions arguments to represent its return type.
|
70
|
+
modifier.extend_parser(:f_arglist, 'f_arglist tEQL cpath', <<~PARSE)
|
71
|
+
result = @builder.type_check_arglist(*val)
|
72
|
+
PARSE
|
73
|
+
|
74
|
+
# Extend the builder by adding a `type_check_arglist` function that will build
|
75
|
+
# a new node type and place it at the end of the argument list.
|
76
|
+
modifier.extend_builder(:type_check_arglist) do |arglist, equal, cpath|
|
77
|
+
arglist << n(:type_check_arglist, [equal, cpath], nil)
|
78
|
+
end
|
79
|
+
|
80
|
+
# Extend the rewriter by adding an `on_def` callback, which will be called
|
81
|
+
# whenever a `def` node is added to the AST. Then, loop through and find any
|
82
|
+
# `type_check_arglist` nodes, and remove them. Finally, insert the
|
83
|
+
# appropriate raises around the execution of the function to mirror the type
|
84
|
+
# checking.
|
85
|
+
modifier.build_rewriter do
|
86
|
+
def on_def(node)
|
87
|
+
type_check_node = node.children[1].children.last
|
88
|
+
return super if !type_check_node || type_check_node.type != :type_check_arglist
|
89
|
+
|
90
|
+
remove(type_check_node.children[0][1])
|
91
|
+
remove(type_check_node.children[1].loc.expression)
|
92
|
+
type = build_constant(type_check_node.children[1])
|
93
|
+
|
94
|
+
@source_rewriter.transaction do
|
95
|
+
insert_before(node.children[2].loc.expression, "result = begin\n")
|
96
|
+
insert_after(node.children[2].loc.expression,
|
97
|
+
"\nend\nraise \"Invalid return value, expected #{type}, " <<
|
98
|
+
"got \#{result.class.name}\" unless result.is_a?(#{type})\nresult")
|
99
|
+
end
|
100
|
+
|
101
|
+
super
|
102
|
+
end
|
103
|
+
|
104
|
+
private
|
105
|
+
|
106
|
+
def build_constant(node, suffix = nil)
|
107
|
+
child_node, name = node.children
|
108
|
+
new_name = suffix ? "#{name}::#{suffix}" : name
|
109
|
+
child_node ? build_constant(child_node, new_name) : new_name
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
```
|
114
|
+
|
115
|
+
## Development
|
116
|
+
|
117
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
118
|
+
|
119
|
+
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 tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
120
|
+
|
121
|
+
## Contributing
|
122
|
+
|
123
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/kddeisz/vernacular.
|
124
|
+
|
125
|
+
## License
|
126
|
+
|
127
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
2
|
+
require 'rake/testtask'
|
3
|
+
|
4
|
+
Rake::TestTask.new(:test) do |t|
|
5
|
+
filepath = File.expand_path('test/test_loader.rb', __dir__)
|
6
|
+
t.ruby_opts << "-r #{filepath}"
|
7
|
+
t.warning = false
|
8
|
+
|
9
|
+
t.libs << 'test'
|
10
|
+
t.libs << 'lib'
|
11
|
+
t.test_files = FileList['test/**/*_test.rb']
|
12
|
+
end
|
13
|
+
|
14
|
+
desc 'Clear out compiled files'
|
15
|
+
task :clear do
|
16
|
+
`rm -r .iseq`
|
17
|
+
end
|
18
|
+
|
19
|
+
task default: :test
|
data/bin/console
ADDED
data/bin/setup
ADDED
data/lib/vernacular.rb
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'date'
|
2
|
+
require 'digest'
|
3
|
+
require 'parser'
|
4
|
+
require 'tempfile'
|
5
|
+
require 'uri'
|
6
|
+
|
7
|
+
require 'vernacular/ast_modifier'
|
8
|
+
require 'vernacular/ast_parser'
|
9
|
+
require 'vernacular/configuration_hash'
|
10
|
+
require 'vernacular/regex_modifier'
|
11
|
+
require 'vernacular/source_file'
|
12
|
+
|
13
|
+
Dir[File.expand_path('vernacular/modifiers/*', __dir__)].each do |file|
|
14
|
+
require file
|
15
|
+
end
|
16
|
+
|
17
|
+
# Allows extending ruby's syntax and compilation process
|
18
|
+
module Vernacular
|
19
|
+
# Module that gets included into `RubyVM::InstructionSequence` in order to
|
20
|
+
# hook into the require process.
|
21
|
+
module InstructionSequenceMixin
|
22
|
+
PARSER_PATH = File.expand_path('vernacular/parser.rb', __dir__).freeze
|
23
|
+
|
24
|
+
def load_iseq(filepath)
|
25
|
+
::Vernacular::SourceFile.load_iseq(filepath) if filepath != PARSER_PATH
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class << self
|
30
|
+
attr_reader :iseq_dir, :modifiers
|
31
|
+
|
32
|
+
def add(modifier)
|
33
|
+
modifiers << modifier
|
34
|
+
end
|
35
|
+
|
36
|
+
def clear
|
37
|
+
Dir.glob(File.join(iseq_dir, '**/*.yarb')) { |path| File.delete(path) }
|
38
|
+
end
|
39
|
+
|
40
|
+
def configure
|
41
|
+
return if @configured
|
42
|
+
|
43
|
+
@modifiers = []
|
44
|
+
yield self
|
45
|
+
|
46
|
+
hash = ConfigurationHash.new(modifiers).hash
|
47
|
+
@iseq_dir = File.expand_path(File.join('../.iseq', hash), __dir__)
|
48
|
+
FileUtils.mkdir_p(iseq_dir) unless File.directory?(iseq_dir)
|
49
|
+
|
50
|
+
class << RubyVM::InstructionSequence
|
51
|
+
prepend ::Vernacular::InstructionSequenceMixin
|
52
|
+
end
|
53
|
+
|
54
|
+
@configured = true
|
55
|
+
end
|
56
|
+
|
57
|
+
def iseq_path_for(source_path)
|
58
|
+
source_path.gsub(/[^A-Za-z0-9\._-]/) { |c| '%02x' % c.ord }
|
59
|
+
.gsub('.rb', '.yarb')
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Vernacular
|
2
|
+
# Represents a modification that will be performed against the AST between the
|
3
|
+
# time that the source code is read and the time that it is compiled down to
|
4
|
+
# YARV instruction sequences.
|
5
|
+
class ASTModifier
|
6
|
+
BuilderExtension =
|
7
|
+
Struct.new(:method, :block) do
|
8
|
+
def components
|
9
|
+
[method, block.source_location]
|
10
|
+
end
|
11
|
+
end
|
12
|
+
attr_reader :builder_extensions
|
13
|
+
|
14
|
+
ParserExtension =
|
15
|
+
Struct.new(:symbol, :pattern, :code) do
|
16
|
+
def components
|
17
|
+
[symbol, pattern, code]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
attr_reader :parser_extensions
|
21
|
+
|
22
|
+
attr_reader :rewriter_block
|
23
|
+
|
24
|
+
def initialize
|
25
|
+
@builder_extensions = []
|
26
|
+
@parser_extensions = []
|
27
|
+
yield self if block_given?
|
28
|
+
end
|
29
|
+
|
30
|
+
def build_rewriter(&block)
|
31
|
+
@rewriter_block = block
|
32
|
+
end
|
33
|
+
|
34
|
+
def extend_builder(method, &block)
|
35
|
+
builder_extensions << BuilderExtension.new(method, block)
|
36
|
+
end
|
37
|
+
|
38
|
+
def extend_parser(symbol, pattern, code)
|
39
|
+
parser_extensions << ParserExtension.new(symbol, pattern, code)
|
40
|
+
end
|
41
|
+
|
42
|
+
def modify(source)
|
43
|
+
raise 'You must first configure a rewriter!' unless rewriter_block
|
44
|
+
|
45
|
+
rewriter = Class.new(Parser::Rewriter, &rewriter_block).new
|
46
|
+
rewriter.instance_variable_set(:@parser, ASTParser.parser)
|
47
|
+
|
48
|
+
buffer = Parser::Source::Buffer.new('<dynamic>')
|
49
|
+
buffer.source = source
|
50
|
+
|
51
|
+
ast = ASTParser.parse(source)
|
52
|
+
rewriter.rewrite(buffer, ast)
|
53
|
+
end
|
54
|
+
|
55
|
+
def components
|
56
|
+
(builder_extensions + parser_extensions).flat_map(&:components) +
|
57
|
+
rewriter_block.source_location
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module Vernacular
|
2
|
+
# Handles monkeying around with the `parser` gem to get it to handle the
|
3
|
+
# various modifications that users can configure `Vernacular` to perform.
|
4
|
+
class ASTParser
|
5
|
+
def parser
|
6
|
+
source = parser_source
|
7
|
+
|
8
|
+
ast_modifiers.each do |modifier|
|
9
|
+
modifier.parser_extensions.each do |parser_extension|
|
10
|
+
source = extend_parser(source, parser_extension)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
write_parser(source)
|
15
|
+
load 'vernacular/parser.rb'
|
16
|
+
Parser::Vernacular.new(builder)
|
17
|
+
end
|
18
|
+
|
19
|
+
class << self
|
20
|
+
def parse(string)
|
21
|
+
parser.reset
|
22
|
+
buffer = Parser::Base.send(:setup_source_buffer, '(string)', 1, string,
|
23
|
+
@parser.default_encoding)
|
24
|
+
parser.parse(buffer)
|
25
|
+
end
|
26
|
+
|
27
|
+
def parser
|
28
|
+
@parser ||= new.parser
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def ast_modifiers
|
35
|
+
Vernacular.modifiers.grep(ASTModifier)
|
36
|
+
end
|
37
|
+
|
38
|
+
def builder
|
39
|
+
modifiers = ast_modifiers
|
40
|
+
|
41
|
+
Class.new(Parser::Builders::Default) do
|
42
|
+
modifiers.each do |modifier|
|
43
|
+
modifier.builder_extensions.each do |builder_extension|
|
44
|
+
define_method(builder_extension.method, &builder_extension.block)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end.new
|
48
|
+
end
|
49
|
+
|
50
|
+
def compile_parser(filepath)
|
51
|
+
output = File.expand_path('../parser.rb', __FILE__)
|
52
|
+
exec_path = Gem.activate_bin_path('racc', 'racc', [])
|
53
|
+
`#{exec_path} --superclass=Parser::Base -o #{output} #{filepath}`
|
54
|
+
File.write(output, File.read(output).gsub('Ruby24', 'Vernacular'))
|
55
|
+
end
|
56
|
+
|
57
|
+
# rubocop:disable Metrics/MethodLength
|
58
|
+
def extend_parser(source, parser_extension)
|
59
|
+
needle = "#{parser_extension.symbol}:"
|
60
|
+
pattern = /\A\s+#{needle}/
|
61
|
+
|
62
|
+
source.split("\n").each_with_object([]) do |line, edited|
|
63
|
+
if line =~ pattern
|
64
|
+
lhs, rhs = line.split(needle)
|
65
|
+
edited << "#{lhs}#{needle} #{parser_extension.pattern}\n" \
|
66
|
+
"{\n#{parser_extension.code}\n}\n#{lhs}|#{rhs}"
|
67
|
+
else
|
68
|
+
edited << line
|
69
|
+
end
|
70
|
+
end.join("\n")
|
71
|
+
end
|
72
|
+
# rubocop:enable Metrics/MethodLength
|
73
|
+
|
74
|
+
def parser_source
|
75
|
+
filepath, = Parser.method(:check_for_encoding_support).source_location
|
76
|
+
File.read(File.expand_path('../../lib/parser/ruby24.y', filepath))
|
77
|
+
end
|
78
|
+
|
79
|
+
def write_parser(source)
|
80
|
+
file = Tempfile.new(['parser-', '.y'])
|
81
|
+
file.write(source)
|
82
|
+
compile_parser(file.path)
|
83
|
+
ensure
|
84
|
+
file.close
|
85
|
+
file.unlink
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Vernacular
|
2
|
+
# Builds a hash out of the given modifiers that represents that current state
|
3
|
+
# of configuration. This ensures that if the configuration of `Vernacular`
|
4
|
+
# changes between runs it doesn't pick up the old compiled files.
|
5
|
+
class ConfigurationHash
|
6
|
+
attr_reader :modifiers
|
7
|
+
|
8
|
+
def initialize(modifiers = [])
|
9
|
+
@modifiers = modifiers
|
10
|
+
end
|
11
|
+
|
12
|
+
def hash
|
13
|
+
digest = Digest::MD5.new
|
14
|
+
modifiers.each do |modifier|
|
15
|
+
digest << modifier.components.inspect
|
16
|
+
end
|
17
|
+
digest.to_s
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Vernacular
|
2
|
+
module Modifiers
|
3
|
+
# Extends Ruby syntax to allow date sigils, or ~d(...). The date inside is
|
4
|
+
# parsed and as an added benefit if it is a set value it is replaced with
|
5
|
+
# the more efficient `strptime`.
|
6
|
+
class DateSigil < RegexModifier
|
7
|
+
FORMAT = '%FT%T%:z'.freeze
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
super(/~d\((.+?)\)/) do |match|
|
11
|
+
content = match[3..-2]
|
12
|
+
begin
|
13
|
+
date = Date.parse(content)
|
14
|
+
"Date.strptime('#{date.strftime(FORMAT)}', '#{FORMAT}')"
|
15
|
+
rescue ArgumentError
|
16
|
+
"Date.parse(#{content})"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Vernacular
|
2
|
+
module Modifiers
|
3
|
+
# Extends Ruby syntax to allow number sigils, or ~n(...). The expression
|
4
|
+
# inside is parsed and evaluated, and is replaced by the result.
|
5
|
+
class NumberSigil < RegexModifier
|
6
|
+
def initialize
|
7
|
+
super(%r{~n\(([\d\s+-/*\(\)]+?)\)}) do |match|
|
8
|
+
eval(match[3..-2]) # rubocop:disable Security/Eval
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module Vernacular
|
2
|
+
module Modifiers
|
3
|
+
# Extends Ruby syntax to allow typed method argument declarations, as in:
|
4
|
+
# def my_method(argument_a : Integer, argument_b : String); end
|
5
|
+
class TypedMethodArgs < ASTModifier
|
6
|
+
def initialize
|
7
|
+
super
|
8
|
+
|
9
|
+
extend_parser(:f_arg, 'f_arg tCOLON cpath', <<~PARSE)
|
10
|
+
result = @builder.type_check_arg(*val)
|
11
|
+
PARSE
|
12
|
+
|
13
|
+
extend_builder(:type_check_arg) do |args, colon, cpath|
|
14
|
+
location = args[0].loc.with_operator(loc(colon))
|
15
|
+
.with_expression(join_exprs(args[0], cpath))
|
16
|
+
[n(:type_check_arg, [args, cpath], location)]
|
17
|
+
end
|
18
|
+
|
19
|
+
build_rewriter { include TypedMethodArgsRewriter }
|
20
|
+
end
|
21
|
+
|
22
|
+
# Methods to be included in the rewriter in order to handle
|
23
|
+
# `type_check_arg` nodes.
|
24
|
+
module TypedMethodArgsRewriter
|
25
|
+
# Triggered whenever a `:def` node is added to the AST. Finds any
|
26
|
+
# `type_check_arg` nodes, replaces them with normal `:arg` nodes, and
|
27
|
+
# adds in the represented type check to the beginning of the method.
|
28
|
+
def on_def(node)
|
29
|
+
type_checks = build_type_checks(node.children[1].children)
|
30
|
+
if type_checks.any?
|
31
|
+
insert_before(node.children[2].loc.expression, type_checks.join)
|
32
|
+
end
|
33
|
+
|
34
|
+
super
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def build_constant(node, suffix = nil)
|
40
|
+
child_node, name = node.children
|
41
|
+
new_name = suffix ? "#{name}::#{suffix}" : name
|
42
|
+
child_node ? build_constant(child_node, new_name) : new_name
|
43
|
+
end
|
44
|
+
|
45
|
+
def build_type_checks(arg_list_node)
|
46
|
+
arg_list_node.each_with_object([]) do |arg, type_checks|
|
47
|
+
next unless arg.type == :type_check_arg
|
48
|
+
|
49
|
+
type_checks << type_check(arg)
|
50
|
+
remove(arg.loc.operator)
|
51
|
+
remove(arg.children[1].loc.expression)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def type_check(arg_node)
|
56
|
+
arg_name = arg_node.children[0][0].children[0]
|
57
|
+
type = build_constant(arg_node.children[1])
|
58
|
+
"raise ArgumentError, \"Invalid type, expected #{type}, got " \
|
59
|
+
"\#{#{arg_name}.class.name}\" unless #{arg_name}.is_a?(#{type});"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module Vernacular
|
2
|
+
module Modifiers
|
3
|
+
# Extends Ruby syntax to allow typed method return declarations, as in:
|
4
|
+
# def my_method(argument_a, argument_b) = return_type; end
|
5
|
+
class TypedMethodReturns < ASTModifier
|
6
|
+
def initialize
|
7
|
+
super
|
8
|
+
|
9
|
+
extend_parser(:f_arglist, 'f_arglist tEQL cpath', <<~PARSE)
|
10
|
+
result = @builder.type_check_arglist(*val)
|
11
|
+
PARSE
|
12
|
+
|
13
|
+
extend_builder(:type_check_arglist) do |arglist, equal, cpath|
|
14
|
+
arglist << n(:type_check_arglist, [equal, cpath], nil)
|
15
|
+
end
|
16
|
+
|
17
|
+
build_rewriter { include TypedMethodReturnsRewriter }
|
18
|
+
end
|
19
|
+
|
20
|
+
# Methods to be included in the rewriter in order to handle
|
21
|
+
# `type_check_arglist` nodes.
|
22
|
+
module TypedMethodReturnsRewriter
|
23
|
+
def on_def(method_node)
|
24
|
+
type_check_node = type_check_node_from(method_node)
|
25
|
+
return super unless type_check_node
|
26
|
+
|
27
|
+
type_node = type_check_node.children[1]
|
28
|
+
remove(type_check_node.children[0][1])
|
29
|
+
remove(type_node.loc.expression)
|
30
|
+
type_check_method(method_node, type_node)
|
31
|
+
|
32
|
+
super
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def build_constant(node, suffix = nil)
|
38
|
+
child_node, name = node.children
|
39
|
+
new_name = suffix ? "#{name}::#{suffix}" : name
|
40
|
+
child_node ? build_constant(child_node, new_name) : new_name
|
41
|
+
end
|
42
|
+
|
43
|
+
def type_check_node_from(method_node)
|
44
|
+
type_check_node = method_node.children[1].children.last
|
45
|
+
return if !type_check_node ||
|
46
|
+
type_check_node.type != :type_check_arglist
|
47
|
+
type_check_node
|
48
|
+
end
|
49
|
+
|
50
|
+
def type_check_method(method_node, type_node)
|
51
|
+
expression = method_node.children[2].loc.expression
|
52
|
+
type = build_constant(type_node)
|
53
|
+
|
54
|
+
@source_rewriter.transaction do
|
55
|
+
insert_before(expression, "result = begin\n")
|
56
|
+
insert_after(expression, "\nend\nraise \"Invalid return value, " \
|
57
|
+
"expected #{type}, got \#{result.class.name}\" unless " \
|
58
|
+
"result.is_a?(#{type})\nresult")
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module Vernacular
|
2
|
+
module Modifiers
|
3
|
+
# Extends Ruby syntax to allow URI sigils, or ~u(...). The expression
|
4
|
+
# inside contains a valid URL.
|
5
|
+
class URISigil < RegexModifier
|
6
|
+
def initialize
|
7
|
+
super(/~u\((.+?)\)/, 'URI.parse("\1")')
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Vernacular
|
2
|
+
# Represents a modification to Ruby source that should be injected into the
|
3
|
+
# require process that modifies the source via a regex pattern.
|
4
|
+
class RegexModifier
|
5
|
+
attr_reader :pattern, :replacement, :block
|
6
|
+
|
7
|
+
def initialize(pattern, replacement = nil, &block)
|
8
|
+
@pattern = pattern
|
9
|
+
@replacement = replacement
|
10
|
+
@block = block
|
11
|
+
end
|
12
|
+
|
13
|
+
def modify(source)
|
14
|
+
if replacement
|
15
|
+
source.gsub(pattern, replacement)
|
16
|
+
else
|
17
|
+
source.gsub(pattern, &block)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def components
|
22
|
+
[pattern, replacement] + (block ? block.source_location : [])
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Vernacular
|
2
|
+
# Represents a file that contains Ruby source code that can be read from and
|
3
|
+
# compiled down to instruction sequences.
|
4
|
+
class SourceFile
|
5
|
+
attr_reader :source_path, :iseq_path
|
6
|
+
|
7
|
+
def initialize(source_path, iseq_path)
|
8
|
+
@source_path = source_path
|
9
|
+
@iseq_path = iseq_path
|
10
|
+
end
|
11
|
+
|
12
|
+
def dump
|
13
|
+
source = File.read(source_path)
|
14
|
+
Vernacular.modifiers.each do |modifier|
|
15
|
+
source = modifier.modify(source)
|
16
|
+
end
|
17
|
+
|
18
|
+
iseq = RubyVM::InstructionSequence.compile(source)
|
19
|
+
digest = ::Digest::MD5.file(source_path).digest
|
20
|
+
File.binwrite(iseq_path, iseq.to_binary("MD5:#{digest}"))
|
21
|
+
iseq
|
22
|
+
rescue SyntaxError, RuntimeError
|
23
|
+
nil
|
24
|
+
end
|
25
|
+
|
26
|
+
def load
|
27
|
+
if File.exist?(iseq_path) &&
|
28
|
+
(File.mtime(source_path) <= File.mtime(iseq_path))
|
29
|
+
RubyVM::InstructionSequence.load_from_binary(File.binread(iseq_path))
|
30
|
+
else
|
31
|
+
dump
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.load_iseq(source_path)
|
36
|
+
new(
|
37
|
+
source_path,
|
38
|
+
File.join(Vernacular.iseq_dir, Vernacular.iseq_path_for(source_path))
|
39
|
+
).load
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
data/vernacular.gemspec
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
lib = File.expand_path('../lib', __FILE__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require 'vernacular/version'
|
6
|
+
|
7
|
+
Gem::Specification.new do |spec|
|
8
|
+
spec.name = 'vernacular'
|
9
|
+
spec.version = Vernacular::VERSION
|
10
|
+
spec.authors = ['Kevin Deisz']
|
11
|
+
spec.email = ['kevin.deisz@gmail.com']
|
12
|
+
|
13
|
+
spec.summary = "Allows extending ruby's syntax and compilation process"
|
14
|
+
spec.homepage = 'https://github.com/kddeisz/vernacular'
|
15
|
+
spec.license = 'MIT'
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
18
|
+
f.match(%r{^(test|spec|features)/})
|
19
|
+
end
|
20
|
+
spec.bindir = 'exe'
|
21
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
22
|
+
spec.require_paths = ['lib']
|
23
|
+
|
24
|
+
spec.add_dependency 'parser', '~> 2.4'
|
25
|
+
spec.add_dependency 'racc', '~> 1.4'
|
26
|
+
|
27
|
+
spec.add_development_dependency 'bundler', '~> 1.15'
|
28
|
+
spec.add_development_dependency 'minitest', '~> 5.10'
|
29
|
+
spec.add_development_dependency 'rake', '~> 12.0'
|
30
|
+
spec.add_development_dependency 'rubocop', '~> 0.49'
|
31
|
+
end
|
metadata
ADDED
@@ -0,0 +1,150 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: vernacular
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Kevin Deisz
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-09-09 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: parser
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2.4'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '2.4'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: racc
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.4'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.4'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: bundler
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.15'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.15'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: minitest
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '5.10'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '5.10'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rake
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '12.0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '12.0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rubocop
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0.49'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0.49'
|
97
|
+
description:
|
98
|
+
email:
|
99
|
+
- kevin.deisz@gmail.com
|
100
|
+
executables: []
|
101
|
+
extensions: []
|
102
|
+
extra_rdoc_files: []
|
103
|
+
files:
|
104
|
+
- ".gitignore"
|
105
|
+
- ".rubocop.yml"
|
106
|
+
- ".travis.yml"
|
107
|
+
- Gemfile
|
108
|
+
- LICENSE
|
109
|
+
- README.md
|
110
|
+
- Rakefile
|
111
|
+
- bin/console
|
112
|
+
- bin/setup
|
113
|
+
- lib/vernacular.rb
|
114
|
+
- lib/vernacular/ast_modifier.rb
|
115
|
+
- lib/vernacular/ast_parser.rb
|
116
|
+
- lib/vernacular/configuration_hash.rb
|
117
|
+
- lib/vernacular/modifiers/date_sigil.rb
|
118
|
+
- lib/vernacular/modifiers/number_sigil.rb
|
119
|
+
- lib/vernacular/modifiers/typed_method_args.rb
|
120
|
+
- lib/vernacular/modifiers/typed_method_returns.rb
|
121
|
+
- lib/vernacular/modifiers/uri_sigil.rb
|
122
|
+
- lib/vernacular/regex_modifier.rb
|
123
|
+
- lib/vernacular/source_file.rb
|
124
|
+
- lib/vernacular/version.rb
|
125
|
+
- vernacular.gemspec
|
126
|
+
homepage: https://github.com/kddeisz/vernacular
|
127
|
+
licenses:
|
128
|
+
- MIT
|
129
|
+
metadata: {}
|
130
|
+
post_install_message:
|
131
|
+
rdoc_options: []
|
132
|
+
require_paths:
|
133
|
+
- lib
|
134
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
140
|
+
requirements:
|
141
|
+
- - ">="
|
142
|
+
- !ruby/object:Gem::Version
|
143
|
+
version: '0'
|
144
|
+
requirements: []
|
145
|
+
rubyforge_project:
|
146
|
+
rubygems_version: 2.6.13
|
147
|
+
signing_key:
|
148
|
+
specification_version: 4
|
149
|
+
summary: Allows extending ruby's syntax and compilation process
|
150
|
+
test_files: []
|