introhive_expression_language 0.6.8
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/.github/dependabot.yml +80 -0
- data/.github/pull_request_template.md +17 -0
- data/.github/workflows/ci.yml +31 -0
- data/.gitignore +15 -0
- data/.qlty/qlty.toml +10 -0
- data/.ruby-version +1 -0
- data/CLAUDE.md +169 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +87 -0
- data/README.md +55 -0
- data/Rakefile +10 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/introhive_expression_language.gemspec +54 -0
- data/lib/introhive_expression_language/iel/evaluation_context.rb +35 -0
- data/lib/introhive_expression_language/iel/evaluation_error.rb +9 -0
- data/lib/introhive_expression_language/iel/evaluator.rb +166 -0
- data/lib/introhive_expression_language/iel/node_util.rb +65 -0
- data/lib/introhive_expression_language/iel/parser.rb +30 -0
- data/lib/introhive_expression_language/iel/sexp_parser.rb +339 -0
- data/lib/introhive_expression_language/iel/std_lib.rb +43 -0
- data/lib/introhive_expression_language/iel/std_lib_assoc.rb +84 -0
- data/lib/introhive_expression_language/iel/std_lib_control.rb +81 -0
- data/lib/introhive_expression_language/iel/std_lib_enum.rb +30 -0
- data/lib/introhive_expression_language/iel/std_lib_existence.rb +19 -0
- data/lib/introhive_expression_language/iel/std_lib_json.rb +38 -0
- data/lib/introhive_expression_language/iel/std_lib_kind.rb +88 -0
- data/lib/introhive_expression_language/iel/std_lib_let.rb +22 -0
- data/lib/introhive_expression_language/iel/std_lib_list.rb +85 -0
- data/lib/introhive_expression_language/iel/std_lib_logic.rb +52 -0
- data/lib/introhive_expression_language/iel/std_lib_math.rb +75 -0
- data/lib/introhive_expression_language/iel/std_lib_number.rb +28 -0
- data/lib/introhive_expression_language/iel/std_lib_regexp.rb +30 -0
- data/lib/introhive_expression_language/iel/std_lib_string.rb +79 -0
- data/lib/introhive_expression_language/iel/symbol_detail.rb +16 -0
- data/lib/introhive_expression_language/iel.rb +2 -0
- data/lib/introhive_expression_language/version.rb +3 -0
- data/lib/introhive_expression_language.rb +9 -0
- metadata +305 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
require_relative 'evaluation_context'
|
|
2
|
+
require_relative 'evaluation_error'
|
|
3
|
+
require_relative 'sexp_parser'
|
|
4
|
+
require_relative 'std_lib'
|
|
5
|
+
require_relative 'symbol_detail'
|
|
6
|
+
require_relative 'parser'
|
|
7
|
+
require_relative 'node_util'
|
|
8
|
+
|
|
9
|
+
module Platform
|
|
10
|
+
module IEL
|
|
11
|
+
class Evaluator
|
|
12
|
+
def self.eval_expr(expr, ctx)
|
|
13
|
+
ast = Parser.parse(expr)
|
|
14
|
+
eval_node(ast, ctx)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.eval_block_expr(expr, ctx)
|
|
18
|
+
ast = Parser.parse(expr)
|
|
19
|
+
eval_block(ast, ctx)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.eval_node(node, ctx)
|
|
23
|
+
case node.kind
|
|
24
|
+
when :list
|
|
25
|
+
eval_list_node(node, ctx)
|
|
26
|
+
when :symbol
|
|
27
|
+
deref_symbol(node, ctx)
|
|
28
|
+
else
|
|
29
|
+
node
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.eval_list_node(node, ctx)
|
|
34
|
+
# First special form - function application
|
|
35
|
+
if function_application?(node, ctx)
|
|
36
|
+
apply_fn(node, ctx)
|
|
37
|
+
else
|
|
38
|
+
SexpParser::Node.list(
|
|
39
|
+
node.value.map {|child_node| eval_node(child_node, ctx)},
|
|
40
|
+
node.source)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.function_application?(node, ctx)
|
|
45
|
+
node.kind == :list && node.value.first && node.value.first.kind == :symbol && ctx[node.value.first.value] && ctx[node.value.first.value].kind == :native_function
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.deref_symbol(symbol_node, ctx)
|
|
49
|
+
target = ctx[symbol_node.value]
|
|
50
|
+
raise EvaluationError.new("symbol '#{symbol_node.value}' at #{symbol_node.source} not found", target) if target.nil?
|
|
51
|
+
target
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def self.apply_fn(node, ctx)
|
|
55
|
+
# Decode the function name.
|
|
56
|
+
name_node = node.value.first
|
|
57
|
+
raise EvaluationError.new('symbol expected', name_node) if name_node.nil?
|
|
58
|
+
raise EvaluationError.new('function application requires symbol', name_node) if name_node.kind != :symbol
|
|
59
|
+
symbol_name = name_node.value
|
|
60
|
+
|
|
61
|
+
# Get the detail of this symbol from the provided context.
|
|
62
|
+
detail_node = ctx[symbol_name]
|
|
63
|
+
raise EvaluationError.new("function '#{symbol_name}' is not found", detail_node) if detail_node.nil?
|
|
64
|
+
fn_proc = detail_node.value[:proc]
|
|
65
|
+
arg_list = detail_node.value[:arg_list]
|
|
66
|
+
|
|
67
|
+
# Create a new context overriding the provided context.
|
|
68
|
+
this_ctx = EvaluationContext.new(ctx)
|
|
69
|
+
|
|
70
|
+
# Decode the parameters and bind each to the new context.
|
|
71
|
+
if arg_list.size == 1 && arg_list[0].start_with?('va_')
|
|
72
|
+
# Varargs - build a list and bind as single arg
|
|
73
|
+
this_ctx[arg_list[0]] = SexpParser::Node.list(node.value.drop(1).map do |arg_node|
|
|
74
|
+
if arg_list[0].end_with?('__')
|
|
75
|
+
arg_node
|
|
76
|
+
else
|
|
77
|
+
eval_node(arg_node, ctx)
|
|
78
|
+
end
|
|
79
|
+
end, node.source)
|
|
80
|
+
else
|
|
81
|
+
# Args are bound by placement and the number must match.
|
|
82
|
+
raise EvaluationError.new("function '#{symbol_name}' requires #{arg_list.size} arguments but #{node.value.size - 1} are provided.", node) if arg_list.size != node.value.size - 1
|
|
83
|
+
node.value.drop(1).each_with_index do |arg_node, index|
|
|
84
|
+
arg_name = arg_list[index]
|
|
85
|
+
this_ctx[arg_name] = if arg_name.end_with?('__')
|
|
86
|
+
arg_node
|
|
87
|
+
else
|
|
88
|
+
eval_node(arg_node, ctx)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Invoke the function.
|
|
94
|
+
rv = fn_proc.call(this_ctx)
|
|
95
|
+
raise EvaluationError.new("function '#{symbol_name}' does not return a node", rv) unless rv.is_a?(SexpParser::Node)
|
|
96
|
+
rv
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Evaluate each item inside this "block" of code (e.g. a list) and return the last evaluated result.
|
|
100
|
+
# e.g.
|
|
101
|
+
# [
|
|
102
|
+
# [puts "hello"]
|
|
103
|
+
# [puts "world"]
|
|
104
|
+
# 123
|
|
105
|
+
# ]
|
|
106
|
+
# hello
|
|
107
|
+
# world
|
|
108
|
+
# => 123
|
|
109
|
+
def self.eval_block(node, ctx)
|
|
110
|
+
rv = nil
|
|
111
|
+
node.value.each do |child_node|
|
|
112
|
+
rv = Evaluator.eval_node(child_node, ctx)
|
|
113
|
+
end
|
|
114
|
+
rv
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Wraps a native function so that it can be invoked in an expression.
|
|
118
|
+
# The native function MUST accept the evaluation context as its first param.
|
|
119
|
+
# The remaining params are mapped as iel mandatory args.
|
|
120
|
+
def self.wrap_native_function(&fn)
|
|
121
|
+
arg_list = fn.parameters.drop(1).map {|_kind, name| name.to_s}
|
|
122
|
+
|
|
123
|
+
wrapped_fn = lambda do |ctx|
|
|
124
|
+
args = [ctx] + arg_list.map {|name| ctx[name]}
|
|
125
|
+
fn.call(*args)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
SexpParser::Node.new(
|
|
129
|
+
:native_function,
|
|
130
|
+
0,
|
|
131
|
+
{
|
|
132
|
+
proc: wrapped_fn,
|
|
133
|
+
arg_list: arg_list
|
|
134
|
+
}
|
|
135
|
+
)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Returns the numeric value of this node. It does not evaluate the node first, just returns its numeric value if
|
|
139
|
+
# possible.
|
|
140
|
+
def self.numeric_value_of(node)
|
|
141
|
+
if node.kind == :numeric_literal
|
|
142
|
+
node.value
|
|
143
|
+
elsif node.kind == :string_literal
|
|
144
|
+
node.value.to_f
|
|
145
|
+
else
|
|
146
|
+
0
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Returns the string value of this node. It does not evaluate the node first, just returns its string value if
|
|
151
|
+
# possible.
|
|
152
|
+
def self.string_value_of(node)
|
|
153
|
+
if node.kind == :numeric_literal
|
|
154
|
+
node.value.to_s
|
|
155
|
+
elsif node.kind == :string_literal
|
|
156
|
+
node.value
|
|
157
|
+
elsif node.kind == :list
|
|
158
|
+
node.value.map {|item| string_value_of(item)}.join
|
|
159
|
+
else
|
|
160
|
+
''
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
end
|
|
166
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
module Platform
|
|
2
|
+
module IEL
|
|
3
|
+
# Converts data from and to standard node graph formats.
|
|
4
|
+
class NodeUtil
|
|
5
|
+
# Recursively convert the given native value to a node using standard representations.
|
|
6
|
+
# The following types are converted:
|
|
7
|
+
# Hash => nested list e.g. [[a 1] [b 2]]
|
|
8
|
+
# String, Time (etc) => string_literal
|
|
9
|
+
# Fixnum, Float => numeric_literal, nan
|
|
10
|
+
# Array => list
|
|
11
|
+
# nil => nil
|
|
12
|
+
# Boolean => boolean
|
|
13
|
+
#
|
|
14
|
+
# e.g.
|
|
15
|
+
# [{:z => 123, 5 => 6}, [9, 8, 7.1, "hi", false, [nil]]]
|
|
16
|
+
# is transformed to:
|
|
17
|
+
# [[["z" 123] [5 6]] [9 8 7.1 "hi" false [nil]]]
|
|
18
|
+
def self.from_native(value)
|
|
19
|
+
if value.is_a? Hash
|
|
20
|
+
Platform::IEL::SexpParser::Node.list([SexpParser::Node.symbol('assoc')] + value.map { |k, v| SexpParser::Node.list([from_native(k), from_native(v)]) })
|
|
21
|
+
elsif value.is_a? Array
|
|
22
|
+
Platform::IEL::SexpParser::Node.list(value.map(&method(:from_native)))
|
|
23
|
+
elsif value.is_a? Numeric
|
|
24
|
+
Platform::IEL::SexpParser::Node.numeric_literal(value)
|
|
25
|
+
elsif value.is_a? NilClass
|
|
26
|
+
Platform::IEL::SexpParser::Node.nil
|
|
27
|
+
elsif value.is_a? TrueClass
|
|
28
|
+
Platform::IEL::SexpParser::Node.boolean(true)
|
|
29
|
+
elsif value.is_a? FalseClass
|
|
30
|
+
Platform::IEL::SexpParser::Node.boolean(false)
|
|
31
|
+
elsif value.is_a? Symbol
|
|
32
|
+
Platform::IEL::SexpParser::Node.symbol(value.to_s)
|
|
33
|
+
else
|
|
34
|
+
Platform::IEL::SexpParser::Node.string_literal(value.to_s)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Recursively
|
|
39
|
+
def self.to_native(node)
|
|
40
|
+
case node.kind
|
|
41
|
+
when :string_literal, :numeric_literal, :boolean
|
|
42
|
+
node.value
|
|
43
|
+
when :symbol
|
|
44
|
+
node.value.to_sym
|
|
45
|
+
when :nil
|
|
46
|
+
nil
|
|
47
|
+
when :nan
|
|
48
|
+
Float::NAN
|
|
49
|
+
when :list
|
|
50
|
+
if node.value.size > 0 && node.value[0].kind == :symbol && node.value[0].value == 'assoc'
|
|
51
|
+
Hash[node.value.drop(1).map do |kv_pair|
|
|
52
|
+
if kv_pair.kind == :list && kv_pair.value.size >= 2
|
|
53
|
+
[to_native(kv_pair.value[0]), to_native(kv_pair.value[1])]
|
|
54
|
+
end
|
|
55
|
+
end.compact]
|
|
56
|
+
else
|
|
57
|
+
node.value.map { |n| to_native(n) }
|
|
58
|
+
end
|
|
59
|
+
else
|
|
60
|
+
raise ArgumentError, "Unable to convert kind #{node.kind} to native value"
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module Platform
|
|
2
|
+
module IEL
|
|
3
|
+
class Parser
|
|
4
|
+
def self.parse(expr)
|
|
5
|
+
ast = SexpParser.parse(expr)
|
|
6
|
+
apply_transforms(ast)
|
|
7
|
+
ast
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def self.apply_transforms(node)
|
|
11
|
+
if node.kind == :list
|
|
12
|
+
# recursively apply first
|
|
13
|
+
raise EvaluationError.new("Node can't end with trailing quote", node) if node.value[-1].to_s == "'" #trailing quote
|
|
14
|
+
node.value.each { |child| apply_transforms(child) }
|
|
15
|
+
# look for the quote pattern in the nodes
|
|
16
|
+
loop do
|
|
17
|
+
match, index = node.value.each_with_index.find { |child, index| child.kind == :symbol && child.value == "'" }
|
|
18
|
+
break if match.nil?
|
|
19
|
+
if index + 1 < node.value.size # There's a node following the quote
|
|
20
|
+
# Replace the two nodes with a single one
|
|
21
|
+
after_match = node.value.delete_at(index + 1)
|
|
22
|
+
node.value[index] = SexpParser::Node.list([SexpParser::Node.symbol('quote', match.source), after_match], match.source)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
module Platform
|
|
2
|
+
module IEL
|
|
3
|
+
class SexpParser
|
|
4
|
+
class ParseError < StandardError
|
|
5
|
+
def initialize(message, source = nil)
|
|
6
|
+
@source = source
|
|
7
|
+
super("#{message}#{source.nil? ? '' : " at #{source}"}")
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
class UnexpectedTokenError < ParseError
|
|
12
|
+
def initialize(token)
|
|
13
|
+
super("#{token.kind} unexpected", token.source)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Parse the given expression and return its AST.
|
|
18
|
+
# Raises SexpParser::ParseError if a syntax error is encountered.
|
|
19
|
+
# @expr string in correct format.
|
|
20
|
+
def self.parse(expr)
|
|
21
|
+
parse_context = ParseContext.new(expr)
|
|
22
|
+
node_stack = [Node.new(:list, 0, [], parse_context)]
|
|
23
|
+
Lexer.tokenize(expr.chars, 0, parse_context) do |token|
|
|
24
|
+
case token.kind
|
|
25
|
+
when :string_literal, :numeric_literal, :symbol, :boolean, :nan, :nil
|
|
26
|
+
node_stack.last.value << token
|
|
27
|
+
when :list_end
|
|
28
|
+
raise UnexpectedTokenError, token if node_stack.size == 1
|
|
29
|
+
value = node_stack.pop
|
|
30
|
+
raise UnexpectedTokenError, token if value.nil?
|
|
31
|
+
node_stack.last.value << value
|
|
32
|
+
when :list_start
|
|
33
|
+
node_stack.push(Node.new(:list, token.source, [], parse_context))
|
|
34
|
+
when :comment
|
|
35
|
+
# Do nothing
|
|
36
|
+
else
|
|
37
|
+
raise UnexpectedTokenError, token
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
if node_stack.size != 1
|
|
41
|
+
raise ParseError, 'Unexpected end of input'
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Remove containing list if there's only a single item results.
|
|
45
|
+
if node_stack.first.value.size == 1
|
|
46
|
+
node_stack = node_stack.first.value
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
node_stack.first
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
class ParseContext
|
|
53
|
+
attr_reader :code
|
|
54
|
+
|
|
55
|
+
def initialize(code)
|
|
56
|
+
@code = code
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def code_line(offset)
|
|
60
|
+
idx = 0
|
|
61
|
+
line = 1
|
|
62
|
+
pos = 0
|
|
63
|
+
last_line_start = 0
|
|
64
|
+
line_location = 1
|
|
65
|
+
@code.chars.each do |c|
|
|
66
|
+
if c == "\n"
|
|
67
|
+
line += 1
|
|
68
|
+
pos = 0
|
|
69
|
+
last_line_start = idx + 1
|
|
70
|
+
else
|
|
71
|
+
pos += 1
|
|
72
|
+
end
|
|
73
|
+
if idx == offset
|
|
74
|
+
line_location = pos
|
|
75
|
+
break
|
|
76
|
+
end
|
|
77
|
+
idx += 1
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
code_line = ''
|
|
81
|
+
@code[last_line_start..-1].chars.each do |c|
|
|
82
|
+
if c == "\n"
|
|
83
|
+
break
|
|
84
|
+
else
|
|
85
|
+
code_line += c
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
" at Line #{line}, Pos #{line_location}\n#{code_line}\n#{'-' * (line_location-1)}^"
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
class Node
|
|
94
|
+
include Comparable
|
|
95
|
+
attr_reader :kind, :value, :source, :context
|
|
96
|
+
|
|
97
|
+
def initialize(kind, source, value=nil, context=nil)
|
|
98
|
+
@kind = kind
|
|
99
|
+
@source = source
|
|
100
|
+
@value = value
|
|
101
|
+
@context = context
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def source_desc
|
|
105
|
+
if context
|
|
106
|
+
context.code_line(@source)
|
|
107
|
+
else
|
|
108
|
+
" Offset #{@source}"
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def <=>(other)
|
|
113
|
+
[kind, value] <=> [other.kind, other.value]
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def inspect
|
|
117
|
+
if value.is_a? Array
|
|
118
|
+
"[#{value.map { |item| item.inspect}.join(', ')}]"
|
|
119
|
+
elsif kind == :symbol
|
|
120
|
+
":#{value}"
|
|
121
|
+
elsif kind == :nil
|
|
122
|
+
'nil'
|
|
123
|
+
elsif kind == :nan
|
|
124
|
+
'nan'
|
|
125
|
+
else
|
|
126
|
+
value.inspect
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def self.symbol(symbol_name, source = 0, context = nil)
|
|
131
|
+
new(:symbol, source, symbol_name)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def self.string_literal(str, source = 0, context = nil)
|
|
135
|
+
new(:string_literal, source, str)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def self.numeric_literal(n, source = 0, context = nil)
|
|
139
|
+
new(:numeric_literal, source, n)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def self.nan(source = 0, context = nil)
|
|
143
|
+
new(:nan, source)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def self.list(contents, source = 0, context = nil)
|
|
147
|
+
new(:list, source, contents)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def self.nil(source = 0, context = nil)
|
|
151
|
+
new(:nil, source)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def self.boolean(boolean_value, source = 0, context = nil)
|
|
155
|
+
new(:boolean, source, boolean_value)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def to_s
|
|
159
|
+
value
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
class Lexer
|
|
164
|
+
SYMBOL_LEADING_CHAR_SE = %q{[\w@+*\/=!<>-]}
|
|
165
|
+
SYMBOL_LEADING_CHAR = Regexp.new(SYMBOL_LEADING_CHAR_SE)
|
|
166
|
+
SYMBOL_TRAILING_CHAR_SE = '[\w:@=.()#+*\/!<>?-]'
|
|
167
|
+
SYMBOL_TRAILING_CHAR = Regexp.new(SYMBOL_TRAILING_CHAR_SE)
|
|
168
|
+
SYMBOL = Regexp.new("#{SYMBOL_LEADING_CHAR_SE}#{SYMBOL_TRAILING_CHAR_SE}*")
|
|
169
|
+
|
|
170
|
+
def self.tokenize(chars, pos, context, &block)
|
|
171
|
+
self.new.tokenize(chars, pos, context, &block)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def tokenize(chars, pos, context, &block)
|
|
175
|
+
@chars = chars
|
|
176
|
+
@pos = pos
|
|
177
|
+
@context = context
|
|
178
|
+
@block = block
|
|
179
|
+
while next_token
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def next_token
|
|
184
|
+
case cpeek
|
|
185
|
+
when nil
|
|
186
|
+
return false
|
|
187
|
+
when /\s/
|
|
188
|
+
cnext
|
|
189
|
+
when '['
|
|
190
|
+
@block.call(Node.new(:list_start, @pos, nil, @context))
|
|
191
|
+
cnext
|
|
192
|
+
when ']'
|
|
193
|
+
@block.call(Node.new(:list_end, @pos, nil, @context))
|
|
194
|
+
cnext
|
|
195
|
+
when '"'
|
|
196
|
+
string_literal
|
|
197
|
+
when "'"
|
|
198
|
+
quote_symbol
|
|
199
|
+
when '-'
|
|
200
|
+
numeric_literal_or_symbol
|
|
201
|
+
when /[-\d]/
|
|
202
|
+
numeric_literal
|
|
203
|
+
when SYMBOL_LEADING_CHAR
|
|
204
|
+
symbol_or_comment
|
|
205
|
+
else
|
|
206
|
+
raise ParseError, "'#{cpeek}' unexpected at #{@pos}"
|
|
207
|
+
end
|
|
208
|
+
true
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def numeric_literal_or_symbol
|
|
212
|
+
cnext # consume the minus
|
|
213
|
+
if cpeek =~ /[\d]/
|
|
214
|
+
numeric_literal('-')
|
|
215
|
+
else
|
|
216
|
+
symbol('-')
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def string_literal
|
|
221
|
+
pos = @pos
|
|
222
|
+
expect('"')
|
|
223
|
+
s = ''
|
|
224
|
+
while true
|
|
225
|
+
case cpeek
|
|
226
|
+
when nil
|
|
227
|
+
raise ParseError, "Unexpected end of string literal at #{@pos}"
|
|
228
|
+
when '"'
|
|
229
|
+
cnext
|
|
230
|
+
if cpeek == '"'
|
|
231
|
+
s += cnext
|
|
232
|
+
else
|
|
233
|
+
@block.call(Node.new(:string_literal, pos, s, @context))
|
|
234
|
+
break
|
|
235
|
+
end
|
|
236
|
+
else
|
|
237
|
+
s += cnext
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def numeric_literal(s = '')
|
|
243
|
+
pos = @pos - s.length
|
|
244
|
+
dot_count = 0
|
|
245
|
+
while true
|
|
246
|
+
case cpeek
|
|
247
|
+
when /[\d]/
|
|
248
|
+
s += cnext
|
|
249
|
+
when /[.]/
|
|
250
|
+
dot_count += 1
|
|
251
|
+
raise ParseError, "Unexpected '.' in numeric literal at #{@pos}" if dot_count > 1
|
|
252
|
+
s += cnext
|
|
253
|
+
else
|
|
254
|
+
@block.call(Node.new(:numeric_literal, pos, s.include?('.') ? s.to_f : s.to_i, @context))
|
|
255
|
+
break
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def symbol_or_comment(s = '')
|
|
261
|
+
s += cnext
|
|
262
|
+
if cpeek == '*'
|
|
263
|
+
s += cnext
|
|
264
|
+
comment(s)
|
|
265
|
+
else
|
|
266
|
+
symbol(s)
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def comment(s = '')
|
|
271
|
+
pos = @pos
|
|
272
|
+
maybe_ending = false
|
|
273
|
+
while true
|
|
274
|
+
case cpeek
|
|
275
|
+
when '*'
|
|
276
|
+
maybe_ending = true
|
|
277
|
+
s += cnext
|
|
278
|
+
when '/'
|
|
279
|
+
s += cnext
|
|
280
|
+
if maybe_ending
|
|
281
|
+
@block.call(Node.new(:comment, pos, s, @context))
|
|
282
|
+
break
|
|
283
|
+
end
|
|
284
|
+
else
|
|
285
|
+
maybe_ending = false
|
|
286
|
+
next_c = cnext
|
|
287
|
+
raise ParseError, "Unexpected EOF in comment at #{@pos}" if next_c.nil?
|
|
288
|
+
s += next_c
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def symbol(s = '')
|
|
294
|
+
pos = @pos - s.length
|
|
295
|
+
while true
|
|
296
|
+
case cpeek
|
|
297
|
+
when SYMBOL_TRAILING_CHAR
|
|
298
|
+
s += cnext
|
|
299
|
+
else
|
|
300
|
+
case s
|
|
301
|
+
when 'true'
|
|
302
|
+
@block.call(Node.new(:boolean, pos, true, @context))
|
|
303
|
+
when 'false'
|
|
304
|
+
@block.call(Node.new(:boolean, pos, false, @context))
|
|
305
|
+
when 'nan'
|
|
306
|
+
@block.call(Node.new(:nan, pos, nil, @context))
|
|
307
|
+
when 'nil'
|
|
308
|
+
@block.call(Node.new(:nil, pos, nil, @context))
|
|
309
|
+
else
|
|
310
|
+
@block.call(Node.new(:symbol, pos, s, @context))
|
|
311
|
+
end
|
|
312
|
+
break
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def quote_symbol
|
|
318
|
+
cnext
|
|
319
|
+
@block.call(Node.new(:symbol, @pos, "'", @context))
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def expect(c)
|
|
323
|
+
raise ParseError, "'#{c}' expected at #{@pos}" if cpeek != c
|
|
324
|
+
cnext
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def cpeek
|
|
328
|
+
@chars[@pos]
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def cnext
|
|
332
|
+
c = @chars[@pos]
|
|
333
|
+
@pos += 1
|
|
334
|
+
c
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
require_relative 'std_lib_let'
|
|
2
|
+
require_relative 'std_lib_control'
|
|
3
|
+
require_relative 'std_lib_enum'
|
|
4
|
+
require_relative 'std_lib_math'
|
|
5
|
+
require_relative 'std_lib_string'
|
|
6
|
+
require_relative 'std_lib_assoc'
|
|
7
|
+
require_relative 'std_lib_list'
|
|
8
|
+
require_relative 'std_lib_kind'
|
|
9
|
+
require_relative 'std_lib_json'
|
|
10
|
+
require_relative 'std_lib_number'
|
|
11
|
+
require_relative 'std_lib_logic'
|
|
12
|
+
require_relative 'std_lib_existence'
|
|
13
|
+
require_relative 'std_lib_regexp'
|
|
14
|
+
|
|
15
|
+
module Platform
|
|
16
|
+
module IEL
|
|
17
|
+
class StdLib
|
|
18
|
+
|
|
19
|
+
# Declare the standard lib functions in this eval context.
|
|
20
|
+
def self.declare(defining_context)
|
|
21
|
+
StdLibLet.declare(defining_context)
|
|
22
|
+
StdLibControl.declare(defining_context)
|
|
23
|
+
StdLibEnum.declare(defining_context)
|
|
24
|
+
StdLibMath.declare(defining_context)
|
|
25
|
+
StdLibString.declare(defining_context)
|
|
26
|
+
StdLibAssoc.declare(defining_context)
|
|
27
|
+
StdLibKind.declare(defining_context)
|
|
28
|
+
StdLibJson.declare(defining_context)
|
|
29
|
+
StdLibNumber.declare(defining_context)
|
|
30
|
+
StdLibLogic.declare(defining_context)
|
|
31
|
+
StdLibExistence.declare(defining_context)
|
|
32
|
+
StdLibRegexp.declare(defining_context)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Helper to create a new context with stdlib already defined.
|
|
36
|
+
def self.new_context
|
|
37
|
+
ec = EvaluationContext.new
|
|
38
|
+
declare(ec)
|
|
39
|
+
ec
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|