Module: Ea::Ocl::Parser
- Defined in:
- lib/ea/ocl/parser.rb
Overview
Tokenizer + parser for an OCL invariant subset. Returns an AST built from Nodes::* structs.
Supported forms:
inv: <expr>
self.<attr>
"<literal>"
<collection>->exists(x | <pred>)
<collection>->forAll(x | <pred>)
<string>.matches('<regex>')
<expr> and <expr>
<expr> or <expr>
not <expr>
Class Method Summary collapse
- .collection_node(kind, collection_src, var, pred_src) ⇒ Object
-
.find_top_level(source, substring) ⇒ Object
Find substring at depth 0 (not inside parens).
-
.parse(source) ⇒ Array<Nodes::*>
AST nodes (one per top-level expression).
-
.parse_expression(source) ⇒ Object
Parse one expression.
-
.split_on_logical_op(source) ⇒ Object
Find the topmost
andororoutside of parens. - .strip_inv_prefix(source) ⇒ Object
Class Method Details
.collection_node(kind, collection_src, var, pred_src) ⇒ Object
92 93 94 95 96 97 |
# File 'lib/ea/ocl/parser.rb', line 92 def collection_node(kind, collection_src, var, pred_src) Nodes.const_get(kind == :exists ? :CollectionExists : :CollectionForAll) .new(collection: parse_expression(collection_src), var: var, predicate: parse_expression(pred_src)) end |
.find_top_level(source, substring) ⇒ Object
Find substring at depth 0 (not inside parens).
117 118 119 120 121 122 123 124 125 126 127 128 129 130 |
# File 'lib/ea/ocl/parser.rb', line 117 def find_top_level(source, substring) depth = 0 i = 0 while i < source.length c = source[i] depth += 1 if c == "(" depth -= 1 if c == ")" if depth.zero? && source[i, substring.length] == substring return i end i += 1 end nil end |
.parse(source) ⇒ Array<Nodes::*>
Returns AST nodes (one per top-level expression).
23 24 25 26 27 28 |
# File 'lib/ea/ocl/parser.rb', line 23 def parse(source) return [] if source.nil? || source.empty? body = strip_inv_prefix(source) parse_expression(body) end |
.parse_expression(source) ⇒ Object
Parse one expression. Returns a single AST node.
36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 |
# File 'lib/ea/ocl/parser.rb', line 36 def parse_expression(source) s = source.strip # Strip outer parens s = s[1..-2].strip while s.start_with?("(") && s.end_with?(")") # 'not <expr>' if s.start_with?("not ") return Nodes::UnaryOp.new(op: :not, operand: parse_expression(s[4..])) end # 'and' / 'or' at top level (find the operator not inside parens) if (split = split_on_logical_op(s)) op, left, right = split return Nodes::BinaryOp.new(op: op, left: parse_expression(left), right: parse_expression(right)) end # Collection ->exists(...) or ->forAll(...) if (m = s.match(/^(.+?)->(exists|forAll)\s*\(\s*(\w+)\s*\|\s*(.+?)\s*\)\z/)) return collection_node(m[2].to_sym, m[1], m[3], m[4]) end # String.matches('regex') if (m = s.match(/^(.+?)\.matches\(\s*['"](.+?)['"]\s*\)\z/)) return Nodes::StringMatches.new(string: parse_expression(m[1]), pattern: m[2]) end # Boolean literal — check BEFORE bare identifier return Nodes::Literal.new(value: true) if s == "true" return Nodes::Literal.new(value: false) if s == "false" # Numeric literal return Nodes::Literal.new(value: s.to_i) if s.match?(/\A-?\d+\z/) return Nodes::Literal.new(value: s.to_f) if s.match?(/\A-?\d+\.\d+\z/) # String literal return Nodes::Literal.new(value: s[1..-2]) if s.match?(/\A['"].*['"]\z/) # 'self.<attr>' or '<var>.<attr>' — both produce AttributeAccess. if (m = s.match(/\A(?:self\.|[a-z_]\w*\.)([a-z_]\w*)\z/i)) return Nodes::AttributeAccess.new(name: m[1]) end # Bare identifier — variable reference. if (m = s.match(/\A([a-z_]\w*)\z/)) return Nodes::AttributeAccess.new(name: m[1]) end raise UnsupportedError, "Unsupported OCL expression: #{source.inspect}" end |
.split_on_logical_op(source) ⇒ Object
Find the topmost and or or outside of parens.
Returns [op, left_str, right_str] or nil.
101 102 103 104 105 106 107 108 109 110 111 112 113 114 |
# File 'lib/ea/ocl/parser.rb', line 101 def split_on_logical_op(source) depth = 0 # Look for ' or ' first (lower precedence than 'and' in OCL?) # Actually OCL: 'or' < 'xor' < 'and' < 'implies'. We pick the # rightmost top-level operator (left-associative). %i[and or].each do |op| keyword = " #{op} " idx = find_top_level(source, keyword) next unless idx return [op, source[0...idx], source[(idx + keyword.length)..]] end nil end |
.strip_inv_prefix(source) ⇒ Object
30 31 32 33 |
# File 'lib/ea/ocl/parser.rb', line 30 def strip_inv_prefix(source) s = source.strip s.sub(/\Ainv\s*:\s*/, "") end |