grammy 0.10 → 0.11

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d714a645eea4ccadea369eafd8adf22ced89afc16b114fc80fa913bd5d06b73a
4
- data.tar.gz: 480c0b3c9eea43baf7702359ae8b27105c44ae87d8f88d3c86a56101b8c77b16
3
+ metadata.gz: 62d6469d52259f78e46fdfcea47da7ac7c8a9b5b77e7cbcd3e685902c3410f98
4
+ data.tar.gz: 3abad7fd1e8c3fbc6f6ed6e9710c5bfb97728ef0efeadd01f15062bd1a38f971
5
5
  SHA512:
6
- metadata.gz: f1bcba79afd6e4280db5be638510576fdd810feb6628f58ab68fb7666dcb5b58ef8cbeacc03ae03b4104af4a8b6d477ec6df90a5b539b4d2265016b261ae6a0d
7
- data.tar.gz: e67c989b6a0759a0d189c782b8cb5b17fb8737e210bd373ce76dd6a9fe219a3c3ac28c276e2937537a7bd0b752dd8f5514c41db6727c2f06b259b193f861d3fd
6
+ metadata.gz: f301f90f661a08429f5bb956f030bc60b8950e0b108acbf7633eb2b1980d27021476602d640901c4dbc4de159bc651f0ac75bd634b6bc769c68149bf2a3e3721
7
+ data.tar.gz: e84cd332a524c119d615f7ba814edcd2ccabd2debe91e83f1da133cbd11b9475455d924a62a339706f050741d2a236a0dc8fc014d8cc2c8a6c8f40ca5cba3c6a
data/docs/TODO.md CHANGED
@@ -1,5 +1,141 @@
1
1
  # TODO
2
2
 
3
+ ## IMMEDIATE (DO NOT COMMIT!) - Check these off below
4
+
5
+ - [ ] Update gem
6
+ - Also allow other person to use `preserves` gem name
7
+
8
+ - [ ] AI thread:
9
+
10
+ ~~~markdown
11
+ Prompt: Allow user-defined combinators to wrap non-terminal rules
12
+
13
+ ## Problem
14
+
15
+ Currently, user-defined combinators in Grammy can only wrap terminals (matchers), not rules (non-terminals).
16
+
17
+ Example that DOESN'T work:
18
+ ```ruby
19
+ class MyGrammar < Grammy::Grammar
20
+ start :expr
21
+ rule(:expr) { parens(number) }
22
+ rule(:number) { reg(/\d+/) } # Non-terminal rule
23
+
24
+ def parens(exp) = str("(") + exp + str(")")
25
+ end
26
+ ```
27
+
28
+ When `number` is called as a rule, it executes immediately and returns a `ParseTree`, not a `Matcher`. The sequence combinator (`+`) then fails because it can't work with ParseTrees.
29
+
30
+ Example that DOES work (using a terminal):
31
+ ```ruby
32
+ class MyGrammar < Grammy::Grammar
33
+ start :expr
34
+ rule(:expr) { parens(number) }
35
+ terminal(:number, /\d+/) # Terminal - returns a Matcher
36
+
37
+ def parens(exp) = str("(") + exp + str(":")
38
+ end
39
+ ```
40
+
41
+ ## Goal
42
+
43
+ Make it possible to pass rule references (non-terminals) to user-defined combinators, so `parens(number)` works whether `number` is a terminal or a rule.
44
+
45
+ ## Context
46
+
47
+ - File: `lib/grammy/grammar.rb`
48
+ - When a rule is called within a rule block, it should return something that can be composed with combinators
49
+ - The grammar DSL currently uses `instance_eval` to evaluate rule blocks
50
+ - Test file: `spec/grammar/user_defined_combinator_spec.rb`
51
+
52
+ ## Constraints
53
+
54
+ - Don't break existing functionality with terminals
55
+ - Maintain the clean DSL syntax
56
+ - Keep the rule execution model intact for normal rule calls
57
+ - Should work with all combinators (`+`, `|`, `[]`, etc.)
58
+
59
+ ## Test Case
60
+
61
+ ```ruby
62
+ class UserDefinedGrammar < Grammy::Grammar
63
+ start :us
64
+ rule(:us) { parens(number) }
65
+ rule(:number) { reg(/\d+/) } # Should work as a rule now
66
+
67
+ def parens(exp) = str("(") + exp + str(")")
68
+ end
69
+
70
+ # Should parse "(123)" successfully
71
+ ```
72
+ ~~~
73
+
74
+ - [ ] COMMIT: BUGFIX: rules are able to reference other nested rules
75
+ - lib/grammy/grammar.rb and tests
76
+
77
+ - [ ] Tree.each specs
78
+ - git fixup where I added `Enumerable`
79
+ - move specs to proper place
80
+ - commit message should mention pre-order traversal.
81
+ - [ ] Add AST transformation DSL
82
+ - I may have already tried to commit it, but missed `lib/ast*` and `spec/ast_spec.rb`
83
+ - Does `tree.each` traversal help?
84
+ - [ ] AI-assisted refactoring (suggestions, missing, etc)
85
+ - [x] tests
86
+ - [ ] code
87
+ - [ ] Rename Matcher to Primitive or Combinator?
88
+ - [ ] I suppose `custom.rb` would be the only other file in `combinators`
89
+ - [ ] README
90
+ - [ ] TODO
91
+ - [ ] add `Makefile` / `Rakefile` rules for common tasks (AI) (BELOW)
92
+ - [ ] publish to RubyGems (BELOW)
93
+ - [ ] check that the `CHANGELOG` is updated
94
+ - [ ] update version (BELOW)
95
+ - [ ] update dependencies (BELOW)
96
+ - [ ] Ruby
97
+ - [ ] gems
98
+ - [ ] RuboCop (see if any rules need updated config)
99
+ - [ ] bun
100
+
101
+ - [x] START ON STONE 0.10!
102
+
103
+ - [ ] CHANGELOG
104
+
105
+ - [ ] https://guides.rubygems.org/security/
106
+
107
+ - [ ] combinator aliases (BELOW)
108
+ - [ ] `zero_or_more`
109
+ - [ ] `one_or_more`
110
+ - [ ] `zero_or_one`, `optional` (and maybe `_opt`?)
111
+ - [ ] update README/TODO
112
+
113
+ - Release VERSION 0.11
114
+
115
+ - [ ] indentation (BELOW)
116
+ - [ ] README updates
117
+ - [ ] CHANGELOG
118
+
119
+ - Release VERSION 0.12
120
+
121
+ - [ ] line continuation (BELOW)
122
+ - [ ] README updates
123
+ - [ ] CHANGELOG
124
+
125
+ - Release VERSION 0.13
126
+
127
+ - [ ] error handling (BELOW)
128
+ - [ ] CHANGELOG
129
+
130
+ - Release VERSION 0.14
131
+
132
+ - [ ] AI prompts
133
+ - [ ] Copilot
134
+ - [ ] Claude
135
+ - [ ] ChatGPT
136
+ - [ ] easier debugging (lib/extensions/debug.rb)
137
+ - [ ] CHANGELOG
138
+
3
139
  Here's a list of things I'd like to complete to make Grammy **great**.
4
140
 
5
141
  ## Scanner
@@ -37,7 +173,7 @@ Here's a list of things I'd like to complete to make Grammy **great**.
37
173
  - [x] tree transformation DSL
38
174
  - [ ] AST builder
39
175
  - [ ] actions in grammar rules
40
- - [ ] user-defined combinators
176
+ - [x] user-defined combinators
41
177
  - [ ] error handling
42
178
  - [ ] error messages
43
179
  - [ ] error recovery
@@ -141,7 +277,7 @@ Here's a list of things I'd like to complete to make Grammy **great**.
141
277
 
142
278
  - [x] publish a gem
143
279
  - [x] `gemspec`
144
- - [x] upload to RubyGems
280
+ - [x] publish to RubyGems
145
281
  - [ ] CLI
146
282
  - [ ] `bin/grammy`
147
283
  - [ ] install with the gem
@@ -0,0 +1,5 @@
1
+ # Temporary workaround for SSL certificate verification issues
2
+ # Used by Makefile when downloading remote RuboCop configs.
3
+ require "openssl"
4
+ OpenSSL::SSL.__send__(:remove_const, :VERIFY_PEER)
5
+ OpenSSL::SSL::VERIFY_PEER = OpenSSL::SSL::VERIFY_NONE
@@ -0,0 +1,13 @@
1
+ class Array
2
+ unless self.respond_to?(:wrap)
3
+ def self.wrap(object)
4
+ if object.nil?
5
+ []
6
+ elsif object.respond_to?(:to_ary)
7
+ object.to_ary || [object]
8
+ else
9
+ [object]
10
+ end
11
+ end
12
+ end
13
+ end
@@ -1,25 +1,25 @@
1
- # require "grammy/tree"
1
+ require "grammy/tree"
2
2
 
3
3
 
4
- # # The purpose of the Transformer is to provide a simpler DSL within `transform` blocks.
5
- # # If the `transform` is for a ParseTree, you can use `name`, `children`, and `build(child)`.
6
- # # If the `transform` is for a Token, you can use `name`, `token`, and `text`.
7
- # module Grammy
8
- # class AST < Tree
9
- # class Transformer
4
+ # The purpose of the Transformer is to provide a simpler DSL within `transform` blocks.
5
+ # If the `transform` is for a ParseTree, you can use `name`, `children`, and `build(child)`.
6
+ # If the `transform` is for a Token, you can use `name`, `token`, and `text`.
7
+ module Grammy
8
+ class AST < Tree
9
+ class Transformer
10
10
 
11
- # def initialize(node, node_class)
12
- # @node = node
13
- # @node_class = node_class
14
- # end
11
+ def initialize(node, node_class)
12
+ @node = node
13
+ @node_class = node_class
14
+ end
15
15
 
16
- # def build(child) = @node_class.build(child)
16
+ def build(child) = @node_class.build(child)
17
17
 
18
- # def name = @node.name.to_sym
19
- # def children = @node.children
20
- # def token = @node
21
- # def text = token.text
18
+ def name = @node.name.to_sym
19
+ def children = @node.children
20
+ def token = @node
21
+ def text = token.text
22
22
 
23
- # end
24
- # end
25
- # end
23
+ end
24
+ end
25
+ end
data/lib/grammy/ast.rb CHANGED
@@ -9,12 +9,17 @@ module Grammy
9
9
  def transform(name, &blk) = transforms[name.to_sym] = blk
10
10
 
11
11
  # The `node` will be a ParseTree or a Token. The code can handle other trees and leaves though.
12
+ # TODO: FIXME: Change this to use `node.each`, checking each node to see if there is a transform.
13
+ # ... if there isn't, we should treat the node's children as belonging to the node's parent.
12
14
  def build(node)
13
- blk = transforms[node.name.to_sym]
15
+ node_name = node.respond_to?(:name) ? node.name.to_sym : nil
16
+ blk = transforms[node_name]
14
17
  if blk
15
- Transformer.new(node, self).instance_exec(&blk)
18
+ Grammy::AST::Transformer.new(node, self).instance_exec(&blk)
16
19
  elsif node.respond_to?(:children)
17
- node.children.flat_map(&method(:build))
20
+ # TODO: FIXME: We should pass the children, not ourself.
21
+ node.with(children: node.children.map{ build(it) })
22
+ # node
18
23
  else
19
24
  node
20
25
  end
@@ -2,6 +2,7 @@ require "grammy/combinator/primitives"
2
2
  require "grammy/errors"
3
3
  require "grammy/scanner"
4
4
  require "grammy/parse_tree"
5
+ require_relative "../extensions/array"
5
6
 
6
7
 
7
8
  module Grammy
@@ -13,9 +14,9 @@ module Grammy
13
14
  # DSL for defining grammar rules.
14
15
  def start(rule_name) = @start_rule = rule_name
15
16
  def rule(name, &)
16
- rule_proc = lambda { |_|
17
+ rule_proc = lambda {
17
18
  results = instance_eval(&)
18
- children = Array(results).flatten.map { |result|
19
+ children = Array.wrap(results).flatten.map { |result|
19
20
  result.is_a?(Grammy::Matcher) ? result.match(@scanner) : result
20
21
  }
21
22
  Grammy::ParseTree.new(name.to_s, children.flatten)
@@ -59,7 +60,7 @@ module Grammy
59
60
  # Primitive combinators will need access to the scanner.
60
61
  def initialize(scanner) = @scanner = scanner
61
62
 
62
- def execute_rule(rule_name) = instance_eval(&rules[rule_name])
63
+ def execute_rule(rule_name) = public_send(rule_name)
63
64
  def rules = self.class.rules
64
65
 
65
66
  end
@@ -0,0 +1,66 @@
1
+ require "grammy/combinator/primitives"
2
+ require "grammy/errors"
3
+ require "grammy/scanner"
4
+ require "grammy/parse_tree"
5
+
6
+
7
+ module Grammy
8
+ class Grammar
9
+
10
+ include Grammy::Combinator::Primitives
11
+
12
+ class << self
13
+ # DSL for defining grammar rules.
14
+ def start(rule_name) = @start_rule = rule_name
15
+ def rule(name, &)
16
+ rule_proc = lambda {
17
+ results = instance_eval(&)
18
+ children = Array(results).flatten.map { |result|
19
+ result.is_a?(Grammy::Matcher) ? result.match(@scanner) : result
20
+ }
21
+ Grammy::ParseTree.new(name.to_s, children.flatten)
22
+ }
23
+ define_method(name, &rule_proc)
24
+ rules[name] = rule_proc
25
+ end
26
+
27
+ # Examples:
28
+ # terminal(:number, /\d+/)
29
+ # terminal(:number) { /\d+/ }
30
+ # terminal(:open_paren, "(")
31
+ # terminal(:open_paren) { "(" }
32
+ def terminal(name, pattern = nil, &block)
33
+ fail ArgumentError, "may only supply a pattern OR a block to #{__callee__}" if pattern && block
34
+ pattern ||= yield if block
35
+ if pattern.is_a?(Regexp)
36
+ terminal_proc = -> { Grammy::Matcher::Regexp.new(pattern) }
37
+ else
38
+ terminal_proc = -> { Grammy::Matcher::String.new(pattern) }
39
+ end
40
+ define_method(name, &terminal_proc)
41
+ rules[name] = terminal_proc
42
+ end
43
+ alias token terminal
44
+
45
+ # Access to the rules.
46
+ def start_rule = @start_rule || @rules.first || :start
47
+ def rules = @rules ||= {}
48
+
49
+ # Parse an input using the grammar.
50
+ def parse(input, start: start_rule)
51
+ scanner = Grammy::Scanner.new(input)
52
+ grammar = self.new(scanner)
53
+ result = grammar.execute_rule(start)
54
+ fail(Grammy::ParseError, "Parsing failed at location #{scanner.location}") if result.nil? || result.empty? && !scanner.input.empty?
55
+ result
56
+ end
57
+ end
58
+
59
+ # Primitive combinators will need access to the scanner.
60
+ def initialize(scanner) = @scanner = scanner
61
+
62
+ def execute_rule(rule_name) = send(rule_name)
63
+ def rules = self.class.rules
64
+
65
+ end
66
+ end
@@ -1,25 +1,25 @@
1
1
  module Grammy
2
- # row/column are 1-based.
3
- # index is 0-based, with respect to the full input string.
4
- class Location < Data.define(:row, :column, :index)
2
+ # line/column are 1-based.
3
+ # offset is 0-based character position in the full input string.
4
+ class Location < Data.define(:line, :column, :offset)
5
5
 
6
- def self.new(row = 1, column = 1, index = 0)
6
+ def self.new(line = 1, column = 1, offset = 0)
7
7
  super
8
8
  end
9
9
 
10
10
  def advance(text)
11
11
  newline_count = text.count("\n")
12
- new_index = index + text.length
13
- new_row = row + newline_count
12
+ new_offset = offset + text.length
13
+ new_line = line + newline_count
14
14
  if newline_count.zero?
15
15
  new_column = column + text.length
16
16
  else
17
17
  new_column = text.length - text.rindex("\n")
18
18
  end
19
- Location.new(new_row, new_column, new_index)
19
+ Location.new(new_line, new_column, new_offset)
20
20
  end
21
21
 
22
- def to_s = "(#{row},#{column})"
22
+ def to_s = "(#{line},#{column})"
23
23
  def inspect = to_s
24
24
  def pretty_print(pp) = pp.text inspect # For IRB output.
25
25
 
@@ -9,7 +9,7 @@ module Grammy
9
9
  def initialize = super(nil)
10
10
 
11
11
  def match(scanner)
12
- scanner.location.index.zero? ? Match.new(nil, scanner.location, scanner.location) : nil
12
+ scanner.location.offset.zero? ? Match.new(nil, scanner.location, scanner.location) : nil
13
13
  end
14
14
 
15
15
  end
@@ -20,7 +20,7 @@ module Grammy
20
20
  # Returns `nil` if pattern does not match.
21
21
  # Otherwise, updates @location and returns a Match object.
22
22
  def match_string(pattern)
23
- return nil if @location.index >= @input.size
23
+ return nil if @location.offset >= @input.size
24
24
  return nil unless remaining_input.start_with?(pattern)
25
25
  match_text(pattern)
26
26
  end
@@ -29,7 +29,7 @@ module Grammy
29
29
  # Returns `nil` if pattern does not match.
30
30
  # Otherwise, updates @location and returns a Match object.
31
31
  def match_regexp(pattern)
32
- return nil if @location.index >= @input.size
32
+ return nil if @location.offset >= @input.size
33
33
  anchored_regex = Regexp.new("\\A(?:#{pattern.source})", pattern.options)
34
34
  match = remaining_input.match(anchored_regex)
35
35
  return nil unless match
@@ -54,11 +54,11 @@ module Grammy
54
54
  end
55
55
 
56
56
  def finished?
57
- @location.index == @input.size
57
+ @location.offset == @input.size
58
58
  end
59
59
 
60
60
  private def remaining_input
61
- @input[@location.index..]
61
+ @input[@location.offset..]
62
62
  end
63
63
 
64
64
  # Returns matched text and its location (or nil).
data/lib/grammy/token.rb CHANGED
@@ -13,6 +13,7 @@ module Grammy
13
13
  end
14
14
 
15
15
  # TODO: Extract this into a Data helper. Better yet, create a Value class for value objects.
16
+ # UHH: I think Data **already** has `with`!
16
17
  def with(value:) = self.class.new(name, match, value)
17
18
 
18
19
  def text = match.text
data/lib/grammy/tree.rb CHANGED
@@ -1,26 +1,48 @@
1
1
  module Grammy
2
- class Tree < Data.define(:name, :children)
2
+ class Tree
3
+
3
4
  INDENTATION = 4
4
5
 
6
+ include Enumerable
7
+
8
+ attr_reader :name
9
+ attr_reader :children
10
+
5
11
  # Make it easier to create nested trees.
6
- def self.new(name, children = [], &block)
12
+ def initialize(name, children = [], &block)
7
13
  children = yield if block
8
- super(name:, children: Array(children))
14
+ @name = name
15
+ @children = Array(children)
9
16
  end
10
17
 
11
- def empty? = children.flatten.compact.empty?
18
+ def empty? = leaves.empty?
12
19
  def leaves = children.flat_map { it.is_a?(self.class) ? it.leaves : it }.compact
13
20
 
14
21
  def to_s(level = 0) = ([to_s_base(level)] + children.map{ to_s_child(it, level) }).join("\n")
15
22
 
16
- def inspect(level = 0) = ([inspect_base(level)] + children.map{ to_s_child(it, level) }).join("\n")
23
+ # Walk the tree in pre-order, yielding every node (including self).
24
+ def each(&block)
25
+ return enum_for(:each) unless block
26
+ yield self
27
+ children.each do |child|
28
+ if child.respond_to?(:children) && child.respond_to?(:each)
29
+ child.each(&block)
30
+ else
31
+ yield child
32
+ end
33
+ end
34
+ end
35
+
36
+ def inspect(level = 0) = ([inspect_base(level)] + Array(children&.map{ inspect_child(it, level) })).join("\n")
17
37
  def to_h = {name:, children: children.map(&:to_h)}
18
38
  def pretty_print(pp) = pp.text inspect # For IRB output.
19
39
 
20
40
  private def to_s_base(level) = "#{indent(level)}#{name}"
21
- private def inspect_base(level) = "#{indent(level)}#<#{class_name} #{name.inspect}>"
22
41
  private def to_s_child(child, level) = child.is_a?(self.class) ? child.to_s(level + 1) : to_s_leaf(child, level + 1)
23
42
  private def to_s_leaf(leaf, level) = "#{indent(level)}#{leaf}"
43
+ private def inspect_base(level) = "#{indent(level)}#<#{class_name} #{name.inspect}>"
44
+ private def inspect_child(child, level) = child.is_a?(self.class) ? child.inspect(level + 1) : inspect_leaf(child, level + 1)
45
+ private def inspect_leaf(leaf, level) = "#{indent(level)}#{leaf.inspect}"
24
46
  private def indent(level) = " " * (level * INDENTATION)
25
47
  private def class_name = self.class.name.split("::").last
26
48
 
data/lib/grammy.rb CHANGED
@@ -4,6 +4,6 @@ require "grammy/grammar"
4
4
 
5
5
  module Grammy
6
6
 
7
- VERSION = File.read(File.join("..", "VERSION"))
7
+ VERSION = "0.10"
8
8
 
9
9
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: grammy
3
3
  version: !ruby/object:Gem::Version
4
- version: '0.10'
4
+ version: '0.11'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Craig Buchek
@@ -24,6 +24,8 @@ files:
24
24
  - docs/CHANGELOG.md
25
25
  - docs/LICENSE.md
26
26
  - docs/TODO.md
27
+ - lib/disable_ssl_verify.rb
28
+ - lib/extensions/array.rb
27
29
  - lib/extensions/debugging.rb
28
30
  - lib/grammy.rb
29
31
  - lib/grammy/ast.rb
@@ -31,6 +33,7 @@ files:
31
33
  - lib/grammy/combinator/primitives.rb
32
34
  - lib/grammy/errors.rb
33
35
  - lib/grammy/grammar.rb
36
+ - lib/grammy/grammar.rb.bak
34
37
  - lib/grammy/location.rb
35
38
  - lib/grammy/match.rb
36
39
  - lib/grammy/matcher.rb
@@ -68,7 +71,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
68
71
  - !ruby/object:Gem::Version
69
72
  version: '0'
70
73
  requirements: []
71
- rubygems_version: 3.6.7
74
+ rubygems_version: 3.6.9
72
75
  specification_version: 4
73
76
  summary: A PEG parsing library with a simple DSL
74
77
  test_files: []