l43_peg 0.0.2 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module L43
4
+ module OpenObject
5
+ def attributes(*atts, **defaults)
6
+ attr_reader(*atts)
7
+ attr_reader(*defaults.keys)
8
+
9
+ define_method :== do |other|
10
+ other&.to_h == to_h
11
+ end
12
+
13
+ define_method :initialize do |**kwds|
14
+ missing = atts - kwds.keys
15
+ raise ArgumentError, "missing required keyword parameters: #{missing.inspect}" unless missing.empty?
16
+ spurious = kwds.keys - atts - defaults.keys
17
+ raise ArgumentError, "spurious keyword parameters: #{spurious.inspect}" unless spurious.empty?
18
+
19
+ values = defaults.merge(kwds)
20
+ values.each do |key, value|
21
+ instance_variable_set("@#{key}", value)
22
+ end
23
+ _init if respond_to?(:_init)
24
+ end
25
+
26
+ define_method :to_h do |*|
27
+ first = atts.inject Hash.new do |h, attribute|
28
+ h.update(attribute => send(attribute))
29
+ end
30
+ defaults.keys.inject first do |h, attribute|
31
+ h.update(attribute => send(attribute))
32
+ end
33
+ end
34
+ alias_method :deconstruct_keys, :to_h
35
+ end
36
+ end
37
+ end
38
+ # SPDX-License-Identifier: AGPL-3.0-or-later
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Object
4
+ def require_subdir(path=nil, &blk)
5
+ Dir.glob(File.join([File.dirname(blk.source_location.first), path, '/**/*.rb'].compact)).each { require _1}
6
+ end
7
+ end
8
+ # SPDX-License-Identifier: AGPL-3.0-or-later
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module L43Peg
4
+ class Cache
5
+ extend L43::OpenObject
6
+ attributes cache: {}
7
+ end
8
+ end
9
+ # SPDX-License-Identifier: Apache-2.0
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../helper"
4
+ module L43Peg
5
+ module Combinators
6
+ module Many extend self
7
+
8
+ include L43Peg::Helper
9
+ def many(input:, cache:, name:, parser:, min:, max:)
10
+ curr_input = input
11
+ curr_cache = cache
12
+ count = 0
13
+ ast = []
14
+
15
+ loop do
16
+ if max && count == max
17
+ return succeed_parser(ast, curr_input, cache: curr_cache)
18
+ end
19
+ case parser.(curr_input, cache: curr_cache)
20
+ in L43Peg::Success => success
21
+ ast.push(success.ast)
22
+ curr_input = success.rest
23
+ curr_cache = success.cache
24
+ count += 1
25
+ in L43Peg::Failure
26
+ if count < min
27
+ return fail_parser("many #{name} should match at least #{min} times but did only #{count} times")
28
+ end
29
+ return succeed_parser(ast, curr_input, cache: curr_cache)
30
+ end
31
+ end
32
+ end
33
+
34
+ end
35
+ end
36
+ end
37
+
38
+ # SPDX-License-Identifier: AGPL-3.0-or-later
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../helper"
4
+ module L43Peg
5
+ module Combinators
6
+ module Seq extend self
7
+
8
+ include L43Peg::Helper
9
+ def seq(input:, cache:, name:, parsers:)
10
+ curr_input = input
11
+ curr_cache = cache
12
+ ast = []
13
+ parsers.each do |parser|
14
+ case parser.(curr_input, cache: curr_cache)
15
+ in L43Peg::Failure => failure
16
+ return L43Peg::Failure.new(reason: "#{failure.reason} in (#{name})", position: failure.position, input:)
17
+ in L43Peg::Success => success
18
+ ast.push(success.ast)
19
+ curr_input = success.rest
20
+ curr_cache = success.cache
21
+ end
22
+ end
23
+ succeed_parser(ast, curr_input, cache: curr_cache)
24
+ end
25
+
26
+ end
27
+ end
28
+ end
29
+ # SPDX-License-Identifier: AGPL-3.0-or-later
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'helper'
4
+ require_relative 'mappers'
5
+ require_relative 'parser'
6
+ require_relative 'parsers'
7
+ require_subdir('combinators') {}
8
+
9
+ module L43Peg
10
+ module Combinators extend self
11
+
12
+ include L43Peg::Helper
13
+ include L43Peg::Mappers
14
+ include L43Peg::Parsers
15
+
16
+ def args_parser(definitions, name: nil, &block)
17
+ parser = tokens_parser(definitions, &block)
18
+ name = name || "args_parser(#{definitions.inspect})"
19
+ inner = many(parser)
20
+ map(inner, name:, fn: join_maps)
21
+ end
22
+
23
+ def many(parser, name: nil, min: 0, max: nil)
24
+ Parser.new(name || "many(#{parser.name})") {|input, cache, name1=nil| Many.many(input:, cache:, name: name1 || name, parser:, min:, max:)}
25
+ end
26
+
27
+ def map(parser, name: nil, fn: nil, &mapper)
28
+ raise ArgumentError, "must not provide keyword parameyer fn and a block" if fn && mapper
29
+ mapper = fn || mapper
30
+ raise ArgumentError, "must provide keyword parameyer fn or a block" unless mapper
31
+ Parser.new(name || "map(#{parser.name})") {|input, cache, name=nil| _map(input:, cache:, name:, parser:, mapper:)}
32
+ end
33
+
34
+ def seq(*parsers, name: nil)
35
+ name ||= "seq(#{parsers.map(&:name).join(", ")})"
36
+ Parser.new(name) {|input, cache, _name=nil| Seq.seq(input:, cache:, name:, parsers:)}
37
+ end
38
+
39
+ private
40
+
41
+ def _map(input:, cache:, name:, parser:, mapper:)
42
+ case parser.(input, cache:)
43
+ in L43Peg::Failure => failure
44
+ failure
45
+ in L43Peg::Success => success
46
+ success.map(&mapper)
47
+ end
48
+ end
49
+ end
50
+ end
51
+ # SPDX-License-Identifier: AGPL-3.0-or-later
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module L43Peg
4
+ class Failure
5
+ extend L43::OpenObject
6
+ attributes cache: nil, input: nil, parsed_by: nil, position: [1, 1], reason: ""
7
+ end
8
+ end
9
+ # SPDX-License-Identifier: Apache-2.0
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module L43Peg
4
+ module Helper
5
+ def fail_parser(reason)
6
+ L43Peg::Failure.new(reason:)
7
+ end
8
+ def succeed_parser(ast, input, cache: nil)
9
+ L43Peg::Success.new(ast:, rest: input, cache: cache || input.cache)
10
+ end
11
+ end
12
+ end
13
+ # SPDX-License-Identifier: AGPL-3.0-or-later
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module L43Peg
4
+ class Input
5
+ extend L43::OpenObject
6
+
7
+ attributes col: 1, input: "", lnb: 1, context: {}
8
+
9
+ def drop(by=nil)
10
+ case by
11
+ when nil
12
+ _drop_by_n(1)
13
+ when String
14
+ _drop_by_n(by.length)
15
+ else
16
+ _drop_by_n(by)
17
+ end
18
+ end
19
+
20
+ def empty? = input.empty?
21
+
22
+ def position = [@col, @lnb]
23
+
24
+ private
25
+
26
+ def _drop_by_n(n)
27
+ return self if input.empty?
28
+ _split(n) => col, lnb, head, tail
29
+ self.class.new(input: tail, context:, col:, lnb:)
30
+ end
31
+
32
+ # Very inefficent but sufficent for my use cases so far
33
+ # and convenient for regex parsers
34
+ def _split(n)
35
+ head = input.slice(0...n)
36
+ lines = head.count("\n")
37
+ tail = input.slice(n..-1) || ''
38
+ new_col =
39
+ if lines.zero?
40
+ col + n
41
+ else
42
+ 1 + head.sub(%r{\A.*\n}m, "").length
43
+ end
44
+ [new_col, lnb + lines, head, tail]
45
+ end
46
+ end
47
+ end
48
+ # SPDX-License-Identifier: Apache-2.0
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module L43Peg
4
+ module Mappers
5
+
6
+ def join_and_to_i
7
+ -> list do
8
+ list.join.to_i
9
+ end
10
+ end
11
+
12
+ def join_maps
13
+ -> maps do
14
+ maps.reduce Hash.new do |map, entry|
15
+ map.merge(entry) { |*args| args.drop(1).flatten }
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ # SPDX-License-Identifier: Apache-2.0
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module L43Peg
4
+ class Parser
5
+
6
+ attr_reader :fn, :name
7
+
8
+ def call(input, cache: L43Peg::Cache.new) = fn.(input, cache, name)
9
+
10
+ private
11
+
12
+ def initialize(name, &fn)
13
+ @name = name
14
+ @fn = fn
15
+ end
16
+
17
+ end
18
+ end
19
+ # SPDX-License-Identifier: Apache-2.0
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module L43Peg
4
+ module Parsers
5
+ class CharParser < L43Peg::Parser
6
+
7
+ private
8
+
9
+ def initialize(charset=nil)
10
+ charset = _mk_set(charset)
11
+ super("char_parser(#{charset})") do |input, cache, name|
12
+ if input.input.empty?
13
+ L43Peg::Failure.new(cache:, input:, parsed_by: self, reason: "cannot parse at end of input #{name}")
14
+ elsif charset.nil? || charset.member?(input.input[0])
15
+ L43Peg::Success.new(ast: input.input[0], cache:, rest: input.drop, position: input.position)
16
+ else
17
+ L43Peg::Failure.new(cache:, input:, parsed_by: self, reason: "char #{input.input[0].inspect}")
18
+ end
19
+ end
20
+ end
21
+
22
+ def _mk_set(charset)
23
+ return unless charset
24
+ case charset
25
+ when String
26
+ Set.new(charset.split(""))
27
+ when Set
28
+ charset
29
+ else
30
+ Set.new(charset)
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ # SPDX-License-Identifier: AGPL-3.0-or-later
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../parsers'
4
+
5
+ module L43Peg
6
+ module Parsers
7
+ class EndParser < L43Peg::Parser
8
+
9
+ def self.instance
10
+ @__instance__ ||= new
11
+ end
12
+
13
+ private
14
+
15
+ def initialize
16
+ super('end_parser') do |input, cache, _name=nil|
17
+ if input.input.empty?
18
+ L43Peg::Success.new(cache:, rest: input, position: input.position)
19
+ else
20
+ L43Peg::Failure.new(cache:, input:, parsed_by: self, reason: "not at end of input")
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ # SPDX-License-Identifier: Apache-2.0
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module L43Peg
4
+ module Parsers
5
+ class FailureParser < L43Peg::Parser
6
+
7
+ def self.instance
8
+ @__instance__ ||= new
9
+ end
10
+
11
+ private
12
+
13
+ def initialize
14
+ super('failure_parser') do |input, cache, _name=nil|
15
+ L43Peg::Failure.new(cache:, input:, parsed_by: self, reason: "this parser always fails")
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ # SPDX-License-Identifier: AGPL-3.0-or-later
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../parsers'
4
+
5
+ module L43Peg
6
+ module Parsers
7
+ class IntParser < L43Peg::Parser
8
+ extend L43Peg::Combinators
9
+ extend L43Peg::Mappers
10
+
11
+ class << self
12
+ def instance
13
+ @__instance__ ||= _mk_int_parser
14
+ end
15
+
16
+ private
17
+
18
+ def _mk_int_parser
19
+ map(seq(many(Parsers.char_parser("+-"), max: 1), many(Parsers.char_parser("0".."9"), min: 1), name: "int_parser"), &join_and_to_i)
20
+ end
21
+ end
22
+
23
+ end
24
+ end
25
+ end
26
+ # SPDX-License-Identifier: Apache-2.0
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module L43Peg
4
+ module Parsers
5
+ class RgxParser < L43Peg::Parser
6
+
7
+ private
8
+
9
+ def initialize(rgx, name: nil, **options)
10
+ name = name || "rgx_parser(#{rgx.inspect})"
11
+ rgx = _mk_rgx(rgx)
12
+ super(name) do |input, cache, _name|
13
+ case rgx.match(input.input)
14
+ in MatchData => md
15
+ ast = _from_match(md, options)
16
+ L43Peg::Success.new(ast:, cache:, rest: input.drop(md[0]), position: input.position)
17
+ else
18
+ L43Peg::Failure.new(cache:, input:, parsed_by: self, reason: "input does not match #{rgx.inspect} (in #{name})")
19
+ end
20
+ end
21
+ end
22
+
23
+ def _from_match(md, options)
24
+ case options[:capture]
25
+ in NilClass
26
+ md[1] || md[0]
27
+ in Integer => n
28
+ md[n]
29
+ in :all
30
+ md.to_a
31
+ end
32
+ end
33
+
34
+ def _mk_rgx(rgx)
35
+ case rgx
36
+ when String
37
+ Regexp.compile("\\A#{rgx}")
38
+ else
39
+ rgx
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ # SPDX-License-Identifier: AGPL-3.0-or-later
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module L43Peg
4
+ module Parsers
5
+ class TokenParser < L43Peg::Parser
6
+
7
+ private
8
+
9
+ def initialize(token_spec, name: nil, **options)
10
+ _check_arg!(token_spec)
11
+ name = name || "token_parser(#{token_spec.inspect})"
12
+ super(name) do |tokens, cache, _name|
13
+ if result = tokens.match(token_spec, options[:capture])
14
+ L43Peg::Success.new(ast: result, cache:, rest: tokens.drop(1), position: tokens.position)
15
+ else
16
+ reason = "token #{tokens.head.inspect} does not match #{token_spec.inspect} (in #{name})"
17
+ L43Peg::Failure.new(cache:, input: tokens, parsed_by: self, reason:)
18
+ end
19
+ end
20
+ end
21
+
22
+ def _check_arg!(token_spec)
23
+ case token_spec
24
+ when Regexp, String
25
+ else
26
+ raise ArgumentError, "#{token_spec.inspect} must be a regular expression or a string"
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ # SPDX-License-Identifier: AGPL-3.0-or-later
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module L43Peg
4
+ module Parsers
5
+ class TokensParser < L43Peg::Parser
6
+
7
+ private
8
+
9
+ def initialize(map, name: nil, &blk)
10
+ map = _check_arg!(map)
11
+ name = name || "tokens_parser(#{map.inspect})"
12
+ super(name) do |tokens, cache, _name|
13
+ case _find_matching(map, tokens)
14
+ in [token, success]
15
+ _succeed(tokens:, token:, success:, &blk)
16
+ else
17
+ reason = "no token matches (in #{name})"
18
+ L43Peg::Failure.new(cache:, input: tokens, parsed_by: self, reason:)
19
+ end
20
+ end
21
+
22
+ end
23
+
24
+ def _check_arg!(map)
25
+ case map
26
+ when Hash
27
+ _mk_tokens(map)
28
+ else
29
+ raise ArgumentError, "#{map.inspect} must be a regular expression or a string"
30
+ end
31
+ end
32
+
33
+ def _find_matching(map, tokens)
34
+ map.each do |token, token_parser|
35
+ case token_parser.(tokens)
36
+ in L43Peg::Success => success
37
+ return [token, success]
38
+ else
39
+ end
40
+ end
41
+ nil
42
+ end
43
+
44
+ def _mk_tokens(map)
45
+ map.map do |token, spec|
46
+ [token, TokenParser.new(Regexp.compile(spec), capture: 1)]
47
+ end.to_h
48
+ end
49
+
50
+ def _succeed(success:, token:, tokens:, &blk)
51
+ result = if blk
52
+ blk.(success.ast)
53
+ else
54
+ success.ast
55
+ end
56
+ L43Peg::Success.new(ast: {token => result}, cache: success.cache, rest: success.rest, position: tokens.position)
57
+ end
58
+ end
59
+ end
60
+ end
61
+ # SPDX-License-Identifier: AGPL-3.0-or-later
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module L43Peg
4
+ module Parsers
5
+ class VerbParser < L43Peg::Parser
6
+
7
+ private
8
+
9
+ def initialize(verb, name: nil)
10
+ name = name || "verb_parser(#{verb.inspect})"
11
+ super(name) do |input, cache, _name|
12
+ if input.input.start_with?(verb)
13
+ L43Peg::Success.new(ast: verb, cache:, rest: input.drop(verb), position: input.position)
14
+ else
15
+ L43Peg::Failure.new(cache:, input:, parsed_by: self, reason: "input does not start with #{verb.inspect} (in #{name})")
16
+ end
17
+ end
18
+ end
19
+
20
+ end
21
+ end
22
+ end
23
+ # SPDX-License-Identifier: AGPL-3.0-or-later
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'combinators'
4
+ require_subdir {}
5
+
6
+ module L43Peg
7
+ module Parsers extend self
8
+ def args_parser(args, name: nil) = L43Peg::Parsers::ArgsParser.new(args, name:)
9
+ def char_parser(charset = nil) = L43Peg::Parsers::CharParser.new(charset)
10
+ def end_parser = L43Peg::Parsers::EndParser.instance
11
+ def failure_parser = L43Peg::Parsers::FailureParser.instance
12
+ def int_parser = L43Peg::Parsers::IntParser.instance
13
+ def rgx_parser(rgx, name: nil, **o) = L43Peg::Parsers::RgxParser.new(rgx, name:, **o)
14
+ def token_parser(spc, name: nil, **o) = L43Peg::Parsers::TokenParser.new(spc, name:, **o)
15
+ def tokens_parser(map, name: nil, &b) = L43Peg::Parsers::TokensParser.new(map, name:, &b)
16
+ def verb_parser(verb, name: nil) = L43Peg::Parsers::VerbParser.new(verb, name:)
17
+ end
18
+ end
19
+ # SPDX-License-Identifier: Apache-2.0
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module L43Peg
4
+ class Success
5
+ def _init
6
+ return if @position
7
+ @position = _position
8
+ end
9
+ extend L43::OpenObject
10
+
11
+ attributes ast: nil, cache: nil, position: nil, rest: nil
12
+
13
+ def map(&mapper)
14
+ self.class.new(ast: mapper.(ast), cache:, position:, rest:)
15
+ end
16
+
17
+ private
18
+
19
+
20
+ def _position
21
+ case @rest
22
+ when Tokens
23
+ 1
24
+ else
25
+ [1, 1]
26
+ end
27
+ end
28
+ end
29
+ end
30
+ # SPDX-License-Identifier: Apache-2.0
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module L43Peg
4
+ class Tokens
5
+ extend L43::OpenObject
6
+
7
+ attributes tnb: 1, tokens: [], context: {}
8
+
9
+ def drop(by=nil)
10
+ by = by || 1
11
+ return self if empty?
12
+ self.class.new(tokens: input.drop(by), context:, tnb: tnb + by)
13
+ end
14
+
15
+ def empty? = tokens.empty?
16
+ def head = tokens.first
17
+ def input = tokens
18
+
19
+ def match(str_or_rgx, option)
20
+ return if empty?
21
+
22
+ case str_or_rgx
23
+ when String
24
+ head == str_or_rgx && head
25
+ when Regexp
26
+ _match_rgx(str_or_rgx, option || 0)
27
+ end
28
+ end
29
+
30
+ def position = tnb
31
+
32
+ private
33
+
34
+ def _match_rgx(rgx, option)
35
+ md = rgx.match(head)
36
+ return unless md
37
+
38
+ if option == :all
39
+ md.to_a
40
+ else
41
+ md[option]
42
+ end
43
+ end
44
+ end
45
+ end
46
+ # SPDX-License-Identifier: Apache-2.0
data/lib/l43_peg.rb CHANGED
@@ -1,7 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'l43/require_helper'
4
+ require_relative 'l43/open_object'
5
+ require_subdir {}
6
+
3
7
  module L43Peg
4
- VERSION = "0.0.2"
5
-
8
+ VERSION = "0.1.1"
6
9
  end
7
- # SPDX-License-Identifier: Apache-2.0
10
+ # SPDX-License-Identifier: AGPL-3.0-or-later