jse4r 0.2.4
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/lib/jse/ast/base.rb +19 -0
- data/lib/jse/ast/nodes.rb +203 -0
- data/lib/jse/ast/parser.rb +97 -0
- data/lib/jse/engine.rb +19 -0
- data/lib/jse/env.rb +60 -0
- data/lib/jse/functors/builtin.rb +67 -0
- data/lib/jse/functors/lisp.rb +72 -0
- data/lib/jse/functors/sql.rb +455 -0
- data/lib/jse/functors/utils.rb +123 -0
- data/lib/jse/version.rb +3 -0
- data/lib/jse.rb +13 -0
- metadata +55 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 9218575b22bb92b6850c899c9bd83a6025e867699a38b9960ee306a032691c05
|
|
4
|
+
data.tar.gz: 8224a8dd3d968ca693811f5e8a83b8a47a32d2a11ba215e912d4839ef61a7a84
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 9709bef4773fc21221ac5883081bc77715a69a80431f3ff8f823b321c161618cff6e11385dbea5052895521cb413a7b0ad725484c009317163dc74194ba92480
|
|
7
|
+
data.tar.gz: ffccc8545db69a0a7e547655821f20fd5e07edee8bbf31cbd6cb341086616d4eac97021d5d22e351ca55bb66a7339ae48810a1e7a7d213b5d4604567e4b84464
|
data/lib/jse/ast/base.rb
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
module JSE
|
|
2
|
+
module Ast
|
|
3
|
+
class LiteralNode < AstNode
|
|
4
|
+
def initialize(value, env)
|
|
5
|
+
super(env)
|
|
6
|
+
@value = value
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
attr_reader :value
|
|
10
|
+
|
|
11
|
+
def apply(_call_env)
|
|
12
|
+
@value
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def to_json
|
|
16
|
+
@value
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class SymbolNode < AstNode
|
|
21
|
+
def initialize(name, env)
|
|
22
|
+
super(env)
|
|
23
|
+
@name = name
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
attr_reader :name
|
|
27
|
+
|
|
28
|
+
def apply(call_env)
|
|
29
|
+
val = call_env.resolve(@name)
|
|
30
|
+
raise NameError, "Symbol '#{@name}' not found" if val.nil?
|
|
31
|
+
val
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def to_json
|
|
35
|
+
@name
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
class ArrayNode < AstNode
|
|
40
|
+
def initialize(elements, env)
|
|
41
|
+
super(env)
|
|
42
|
+
@elements = elements
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
attr_reader :elements
|
|
46
|
+
|
|
47
|
+
def apply(call_env)
|
|
48
|
+
return [] if @elements.empty?
|
|
49
|
+
|
|
50
|
+
first = @elements.first
|
|
51
|
+
if first.is_a?(SymbolNode) && JSE::Parser.symbol?(first.name)
|
|
52
|
+
apply_function_call(call_env, first.name)
|
|
53
|
+
else
|
|
54
|
+
@elements.map { |e| call_env.eval(e) }
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def to_json
|
|
59
|
+
@elements.map(&:to_json)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def apply_function_call(call_env, symbol)
|
|
65
|
+
rest = @elements[1..]
|
|
66
|
+
if symbol == "$quote"
|
|
67
|
+
# Pass unevaluated
|
|
68
|
+
args = rest.map(&:to_json)
|
|
69
|
+
return apply_functor(call_env, symbol, args)
|
|
70
|
+
end
|
|
71
|
+
evaluated = rest.map { |e| call_env.eval(e) }
|
|
72
|
+
apply_functor(call_env, symbol, evaluated)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def apply_functor(call_env, symbol, args)
|
|
76
|
+
functor = call_env.resolve(symbol)
|
|
77
|
+
raise NameError, "Unknown operator: #{symbol}" if functor.nil?
|
|
78
|
+
functor.call(call_env, *args)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
class ObjectNode < AstNode
|
|
83
|
+
def initialize(dict, env)
|
|
84
|
+
super(env)
|
|
85
|
+
@dict = dict
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def apply(call_env)
|
|
89
|
+
@dict.transform_values { |v| call_env.eval(v) }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def to_json
|
|
93
|
+
@dict.transform_values(&:to_json)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
class ExpressionNode < AstNode
|
|
98
|
+
def initialize(operator, value, metadata, env)
|
|
99
|
+
super(env)
|
|
100
|
+
@operator = operator
|
|
101
|
+
@value = value
|
|
102
|
+
@metadata = metadata
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
attr_reader :operator, :value, :metadata
|
|
106
|
+
|
|
107
|
+
def apply(call_env)
|
|
108
|
+
functor = call_env.resolve(@operator)
|
|
109
|
+
raise NameError, "Unknown operator: #{@operator}" if functor.nil?
|
|
110
|
+
|
|
111
|
+
evaluated_meta = @metadata.transform_values { |v| deep_eval(call_env, v) }
|
|
112
|
+
call_env.set_meta(evaluated_meta)
|
|
113
|
+
begin
|
|
114
|
+
args = case @operator
|
|
115
|
+
when "$quote"
|
|
116
|
+
[@value]
|
|
117
|
+
when "$expr"
|
|
118
|
+
[call_env.eval(@value)]
|
|
119
|
+
when "$sql"
|
|
120
|
+
# $sql receives raw JSON stored in LiteralNode by parser
|
|
121
|
+
if @value.is_a?(LiteralNode)
|
|
122
|
+
[@value.value]
|
|
123
|
+
else
|
|
124
|
+
[@value]
|
|
125
|
+
end
|
|
126
|
+
else
|
|
127
|
+
if @value.is_a?(ArrayNode)
|
|
128
|
+
@value.elements.map { |e| deep_eval(call_env, e) }
|
|
129
|
+
else
|
|
130
|
+
[call_env.eval(@value)]
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
functor.call(call_env, *args)
|
|
135
|
+
ensure
|
|
136
|
+
call_env.clear_meta
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def to_json
|
|
141
|
+
result = { @operator => @value.to_json }
|
|
142
|
+
@metadata.each { |k, v| result[k] = v }
|
|
143
|
+
result
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
private
|
|
147
|
+
|
|
148
|
+
def deep_eval(env, value)
|
|
149
|
+
evaluated = env.eval(value)
|
|
150
|
+
case evaluated
|
|
151
|
+
when Array
|
|
152
|
+
evaluated.map { |item| deep_eval(env, item) }
|
|
153
|
+
when Hash
|
|
154
|
+
evaluated.transform_values { |v| deep_eval(env, v) }
|
|
155
|
+
else
|
|
156
|
+
evaluated
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
class QuoteNode < AstNode
|
|
162
|
+
def initialize(value, env)
|
|
163
|
+
super(env)
|
|
164
|
+
@value = value
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def apply(_call_env)
|
|
168
|
+
@value
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def to_json
|
|
172
|
+
if @value.respond_to?(:to_json)
|
|
173
|
+
["$quote", @value.to_json]
|
|
174
|
+
else
|
|
175
|
+
["$quote", @value]
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
class LambdaNode < AstNode
|
|
181
|
+
def initialize(params, body, closure_env)
|
|
182
|
+
super(closure_env)
|
|
183
|
+
@params = params
|
|
184
|
+
@body = body
|
|
185
|
+
@closure_env = closure_env
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def apply(call_env, *args)
|
|
189
|
+
if args.length != @params.length
|
|
190
|
+
raise ArgumentError, "Lambda expects #{@params.length} args, got #{args.length}"
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
call_env = JSE::Env.new(parent: @closure_env)
|
|
194
|
+
@params.zip(args).each { |param, arg| call_env.set(param, arg) }
|
|
195
|
+
call_env.eval(@body)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def to_json
|
|
199
|
+
"<lambda>"
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
module JSE
|
|
2
|
+
class Parser
|
|
3
|
+
def self.symbol?(s)
|
|
4
|
+
return false if s == "$*"
|
|
5
|
+
s.is_a?(String) && s.start_with?("$") && !s.start_with?("$$")
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def self.unescape(s)
|
|
9
|
+
s.is_a?(String) && s.start_with?("$$") ? s[1..] : s
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def initialize(env)
|
|
13
|
+
@env = env
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def parse(expr)
|
|
17
|
+
case expr
|
|
18
|
+
when nil, true, false, Integer, Float
|
|
19
|
+
Ast::LiteralNode.new(expr, @env)
|
|
20
|
+
when String
|
|
21
|
+
if self.class.symbol?(expr)
|
|
22
|
+
Ast::SymbolNode.new(expr, @env)
|
|
23
|
+
else
|
|
24
|
+
Ast::LiteralNode.new(self.class.unescape(expr), @env)
|
|
25
|
+
end
|
|
26
|
+
when Array
|
|
27
|
+
parse_list(expr)
|
|
28
|
+
when Hash
|
|
29
|
+
parse_dict(expr)
|
|
30
|
+
else
|
|
31
|
+
Ast::LiteralNode.new(expr, @env)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def parse_list(lst)
|
|
38
|
+
return Ast::ArrayNode.new([], @env) if lst.empty?
|
|
39
|
+
|
|
40
|
+
first = lst.first
|
|
41
|
+
if first == "$quote"
|
|
42
|
+
value = lst.length > 1 ? lst[1] : nil
|
|
43
|
+
return Ast::QuoteNode.new(value, @env)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
if first.is_a?(String) && self.class.symbol?(first)
|
|
47
|
+
elements = lst[1..].map { |e| parse(e) }
|
|
48
|
+
return Ast::ExpressionNode.new(first, Ast::ArrayNode.new(elements, @env), {}, @env)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
elements = lst.map { |e| parse(e) }
|
|
52
|
+
Ast::ArrayNode.new(elements, @env)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def parse_dict(dict)
|
|
56
|
+
symbol_keys = dict.keys.select { |k| self.class.symbol?(k) }
|
|
57
|
+
|
|
58
|
+
if symbol_keys.empty?
|
|
59
|
+
result = {}
|
|
60
|
+
dict.each do |k, v|
|
|
61
|
+
parsed_key = self.class.unescape(k)
|
|
62
|
+
result[parsed_key] = parse(v)
|
|
63
|
+
end
|
|
64
|
+
return Ast::ObjectNode.new(result, @env)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
if symbol_keys.length == 1
|
|
68
|
+
operator = symbol_keys.first
|
|
69
|
+
|
|
70
|
+
if operator == "$quote"
|
|
71
|
+
return Ast::QuoteNode.new(dict[operator], @env)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# $sql: pass raw JSON directly (bypass JSE parsing)
|
|
75
|
+
if operator == "$sql"
|
|
76
|
+
raw_value = dict[operator]
|
|
77
|
+
metadata = {}
|
|
78
|
+
dict.each do |k, v|
|
|
79
|
+
next if k == operator
|
|
80
|
+
metadata[self.class.unescape(k)] = v
|
|
81
|
+
end
|
|
82
|
+
return Ast::ExpressionNode.new(operator, Ast::LiteralNode.new(raw_value, @env), metadata, @env)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
parsed_value = parse(dict[operator])
|
|
86
|
+
metadata = {}
|
|
87
|
+
dict.each do |k, v|
|
|
88
|
+
next if k == operator
|
|
89
|
+
metadata[self.class.unescape(k)] = parse(v)
|
|
90
|
+
end
|
|
91
|
+
return Ast::ExpressionNode.new(operator, parsed_value, metadata, @env)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
raise "JSE structure error: object cannot have multiple operator keys"
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
data/lib/jse/engine.rb
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module JSE
|
|
2
|
+
class Engine
|
|
3
|
+
def initialize(env)
|
|
4
|
+
@env = env
|
|
5
|
+
@parser = Parser.new(env)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
attr_reader :env
|
|
9
|
+
|
|
10
|
+
def execute(expr)
|
|
11
|
+
ast = @parser.parse(expr)
|
|
12
|
+
@env.eval(ast)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.with_env
|
|
16
|
+
new(Env.new)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
data/lib/jse/env.rb
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
module JSE
|
|
2
|
+
class Env
|
|
3
|
+
def initialize(parent: nil)
|
|
4
|
+
@parent = parent
|
|
5
|
+
@bindings = {}
|
|
6
|
+
@current_meta = {}
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
attr_reader :parent
|
|
10
|
+
|
|
11
|
+
def resolve(name)
|
|
12
|
+
if @bindings.key?(name)
|
|
13
|
+
@bindings[name]
|
|
14
|
+
elsif @parent
|
|
15
|
+
@parent.resolve(name)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def register(name, value)
|
|
20
|
+
if @bindings.key?(name)
|
|
21
|
+
raise "Symbol '#{name}' already exists in current scope"
|
|
22
|
+
end
|
|
23
|
+
@bindings[name] = value
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def set(name, value)
|
|
27
|
+
@bindings[name] = value
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def exists?(name)
|
|
31
|
+
return true if @bindings.key?(name)
|
|
32
|
+
return @parent.exists?(name) if @parent
|
|
33
|
+
false
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def load(mod)
|
|
37
|
+
mod.each { |name, functor| register(name, functor) }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def eval(node)
|
|
41
|
+
if node.respond_to?(:apply)
|
|
42
|
+
node.apply(self)
|
|
43
|
+
else
|
|
44
|
+
node
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def set_meta(dict)
|
|
49
|
+
@current_meta = dict
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def get_meta
|
|
53
|
+
@current_meta
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def clear_meta
|
|
57
|
+
@current_meta = {}
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
module JSE
|
|
2
|
+
module Functors
|
|
3
|
+
module Builtin
|
|
4
|
+
def self.quote(env, *args)
|
|
5
|
+
args[0]
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def self.eq(env, *args)
|
|
9
|
+
a = args[0].respond_to?(:apply) ? env.eval(args[0]) : args[0]
|
|
10
|
+
b = args[1].respond_to?(:apply) ? env.eval(args[1]) : args[1]
|
|
11
|
+
a == b
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.cond(env, *args)
|
|
15
|
+
i = 0
|
|
16
|
+
while i < args.length - 1
|
|
17
|
+
test = args[i].respond_to?(:apply) ? env.eval(args[i]) : args[i]
|
|
18
|
+
if test
|
|
19
|
+
return args[i + 1].respond_to?(:apply) ? env.eval(args[i + 1]) : args[i + 1]
|
|
20
|
+
end
|
|
21
|
+
i += 2
|
|
22
|
+
end
|
|
23
|
+
# Odd number of args: last is default
|
|
24
|
+
if args.length.odd?
|
|
25
|
+
env.eval(args.last)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.head(env, *args)
|
|
30
|
+
lst = args[0].respond_to?(:apply) ? env.eval(args[0]) : args[0]
|
|
31
|
+
raise "$head requires a list" unless lst.is_a?(Array)
|
|
32
|
+
raise "$head: list is empty" if lst.empty?
|
|
33
|
+
lst.first
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.tail(env, *args)
|
|
37
|
+
lst = args[0].respond_to?(:apply) ? env.eval(args[0]) : args[0]
|
|
38
|
+
raise "$tail requires a list" unless lst.is_a?(Array)
|
|
39
|
+
raise "$tail: list is empty" if lst.empty?
|
|
40
|
+
lst[1..]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.atomp(env, *args)
|
|
44
|
+
val = args[0].respond_to?(:apply) ? env.eval(args[0]) : args[0]
|
|
45
|
+
val.nil? || val.is_a?(Integer) || val.is_a?(Float) ||
|
|
46
|
+
val == true || val == false || val.is_a?(String)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def self.cons(env, *args)
|
|
50
|
+
elem = args[0].respond_to?(:apply) ? env.eval(args[0]) : args[0]
|
|
51
|
+
lst = args[1].respond_to?(:apply) ? env.eval(args[1]) : args[1]
|
|
52
|
+
raise "$cons second argument must be a list" unless lst.is_a?(Array)
|
|
53
|
+
[elem] + lst
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
BUILTIN_FUNCTORS = {
|
|
57
|
+
"$quote" => method(:quote),
|
|
58
|
+
"$eq" => method(:eq),
|
|
59
|
+
"$cond" => method(:cond),
|
|
60
|
+
"$head" => method(:head),
|
|
61
|
+
"$tail" => method(:tail),
|
|
62
|
+
"$atom?" => method(:atomp),
|
|
63
|
+
"$cons" => method(:cons),
|
|
64
|
+
}.freeze
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
module JSE
|
|
2
|
+
module Functors
|
|
3
|
+
module Lisp
|
|
4
|
+
def self.eval_fn(env, *args)
|
|
5
|
+
raise "$eval requires 1 argument" if args.empty?
|
|
6
|
+
env.eval(args[0])
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def self.apply_fn(env, *args)
|
|
10
|
+
raise "$apply requires (functor, arglist)" if args.length < 2
|
|
11
|
+
functor = args[0].respond_to?(:apply) ? env.eval(args[0]) : args[0]
|
|
12
|
+
arglist = args[1].respond_to?(:apply) ? env.eval(args[1]) : args[1]
|
|
13
|
+
raise "$apply second argument must be a list" unless arglist.is_a?(Array)
|
|
14
|
+
functor.call(env, *arglist)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.lambda_fn(env, *args)
|
|
18
|
+
raise "$lambda requires (params, body)" if args.length < 2
|
|
19
|
+
params_raw = args[0]
|
|
20
|
+
body = args[1]
|
|
21
|
+
|
|
22
|
+
params = if params_raw.is_a?(Ast::ArrayNode)
|
|
23
|
+
params_raw.elements.map { |e| e.is_a?(Ast::SymbolNode) ? e.name : e }
|
|
24
|
+
elsif params_raw.is_a?(Ast::SymbolNode)
|
|
25
|
+
[params_raw.name]
|
|
26
|
+
elsif params_raw.is_a?(Array)
|
|
27
|
+
params_raw
|
|
28
|
+
else
|
|
29
|
+
[params_raw.to_s]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
Ast::LambdaNode.new(params, body, env)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.def_fn(env, *args)
|
|
36
|
+
raise "$def requires (name, value)" if args.length < 2
|
|
37
|
+
name_node = args[0]
|
|
38
|
+
name = if name_node.is_a?(Ast::SymbolNode)
|
|
39
|
+
name_node.name
|
|
40
|
+
elsif name_node.is_a?(String)
|
|
41
|
+
name_node
|
|
42
|
+
else
|
|
43
|
+
name_node.to_s
|
|
44
|
+
end
|
|
45
|
+
value = args[1].respond_to?(:apply) ? env.eval(args[1]) : args[1]
|
|
46
|
+
env.register(name, value)
|
|
47
|
+
value
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.defn(env, *args)
|
|
51
|
+
raise "$defn requires (name, params, body)" if args.length < 3
|
|
52
|
+
name_node = args[0]
|
|
53
|
+
name = if name_node.is_a?(Ast::SymbolNode)
|
|
54
|
+
name_node.name
|
|
55
|
+
else
|
|
56
|
+
name_node.to_s
|
|
57
|
+
end
|
|
58
|
+
lambda_result = lambda_fn(env, args[1], args[2])
|
|
59
|
+
env.register(name, lambda_result)
|
|
60
|
+
lambda_result
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
LISP_FUNCTORS = {
|
|
64
|
+
"$eval" => method(:eval_fn),
|
|
65
|
+
"$apply" => method(:apply_fn),
|
|
66
|
+
"$lambda" => method(:lambda_fn),
|
|
67
|
+
"$def" => method(:def_fn),
|
|
68
|
+
"$defn" => method(:defn),
|
|
69
|
+
}.freeze
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
module JSE
|
|
4
|
+
module Functors
|
|
5
|
+
module SQL
|
|
6
|
+
QUERY_FIELDS = "subject, predicate, object, meta"
|
|
7
|
+
|
|
8
|
+
# ============================================================
|
|
9
|
+
# Subquery detection
|
|
10
|
+
# ============================================================
|
|
11
|
+
|
|
12
|
+
def self.subquery?(value)
|
|
13
|
+
return false unless value.is_a?(Array) && !value.empty?
|
|
14
|
+
value.all? { |item| item.is_a?(Array) && !item.empty? && item[0].is_a?(String) && item[0].start_with?("$") }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# ============================================================
|
|
18
|
+
# String utilities
|
|
19
|
+
# ============================================================
|
|
20
|
+
|
|
21
|
+
def self.symbol_str?(s)
|
|
22
|
+
return false if s == "$*"
|
|
23
|
+
s.is_a?(String) && s.start_with?("$") && !s.start_with?("$$")
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.escaped_str?(s)
|
|
27
|
+
s.is_a?(String) && s.start_with?("$$")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.sql_quote(s)
|
|
31
|
+
"'#{s.gsub("'", "''")}'"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.parenthesized?(s)
|
|
35
|
+
return false unless s.start_with?("(")
|
|
36
|
+
depth = 0
|
|
37
|
+
s.each_char.with_index do |c, i|
|
|
38
|
+
depth += 1 if c == "("
|
|
39
|
+
depth -= 1 if c == ")"
|
|
40
|
+
return i == s.length - 1 if depth == 0
|
|
41
|
+
end
|
|
42
|
+
false
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# ============================================================
|
|
46
|
+
# Expression renderer
|
|
47
|
+
# ============================================================
|
|
48
|
+
|
|
49
|
+
def self.render_expr(value)
|
|
50
|
+
case value
|
|
51
|
+
when nil then "null"
|
|
52
|
+
when true then "true"
|
|
53
|
+
when false then "false"
|
|
54
|
+
when Integer, Float then value.to_s
|
|
55
|
+
when String
|
|
56
|
+
if escaped_str?(value)
|
|
57
|
+
sql_quote("$#{value[2..]}")
|
|
58
|
+
elsif symbol_str?(value)
|
|
59
|
+
value[1..]
|
|
60
|
+
else
|
|
61
|
+
sql_quote(value)
|
|
62
|
+
end
|
|
63
|
+
when Array
|
|
64
|
+
return "" if value.empty?
|
|
65
|
+
if subquery?(value)
|
|
66
|
+
render_subquery(value)
|
|
67
|
+
elsif value[0].is_a?(String) && value[0].start_with?("$") && !value[0].start_with?("$$")
|
|
68
|
+
render_list_expr(value)
|
|
69
|
+
else
|
|
70
|
+
value.map { |v| render_expr(v) }.join(", ")
|
|
71
|
+
end
|
|
72
|
+
when Hash
|
|
73
|
+
render_dict(value)
|
|
74
|
+
else
|
|
75
|
+
value.to_s
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def self.render_dict(dict)
|
|
80
|
+
return "" if dict.empty?
|
|
81
|
+
op_keys = dict.keys.select { |k| symbol_str?(k) }
|
|
82
|
+
if op_keys.empty? || op_keys.length > 1
|
|
83
|
+
return dict.map { |k, v| "#{render_key(k)} = #{render_expr(v)}" }.join(", ")
|
|
84
|
+
end
|
|
85
|
+
op = op_keys.first
|
|
86
|
+
return %("#{dict[op]}") if op == "$symbol"
|
|
87
|
+
render_expr(dict[op])
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def self.render_key(key)
|
|
91
|
+
if escaped_str?(key)
|
|
92
|
+
"$#{key[2..]}"
|
|
93
|
+
elsif symbol_str?(key)
|
|
94
|
+
key[1..]
|
|
95
|
+
else
|
|
96
|
+
key.to_s
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def self.render_list_expr(lst)
|
|
101
|
+
return "" if lst.empty?
|
|
102
|
+
op = lst[0]
|
|
103
|
+
return lst.map { |v| render_expr(v) }.join(", ") unless op.is_a?(String)
|
|
104
|
+
return lst.map { |v| render_expr(v) }.join(", ") if op.start_with?("$$")
|
|
105
|
+
render_func(op, lst[1..])
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# ============================================================
|
|
109
|
+
# Function dispatch
|
|
110
|
+
# ============================================================
|
|
111
|
+
|
|
112
|
+
def self.render_func(op, args)
|
|
113
|
+
case op
|
|
114
|
+
when "$as" then render_as(args)
|
|
115
|
+
when "$count" then render_count(args)
|
|
116
|
+
when "$sum" then render_agg("sum", args)
|
|
117
|
+
when "$avg" then render_agg("avg", args)
|
|
118
|
+
when "$max" then render_agg("max", args)
|
|
119
|
+
when "$min" then render_agg("min", args)
|
|
120
|
+
when "$eq" then render_binary("=", args)
|
|
121
|
+
when "$ne" then render_binary("!=", args)
|
|
122
|
+
when "$gt" then render_binary(">", args)
|
|
123
|
+
when "$gte" then render_binary(">=", args)
|
|
124
|
+
when "$lt" then render_binary("<", args)
|
|
125
|
+
when "$lte" then render_binary("<=", args)
|
|
126
|
+
when "$like" then render_binary("like", args)
|
|
127
|
+
when "$is" then render_is(args)
|
|
128
|
+
when "$is-not" then render_is_not(args)
|
|
129
|
+
when "$and" then render_logical("and", args)
|
|
130
|
+
when "$or" then render_logical("or", args)
|
|
131
|
+
when "$in" then render_in(args)
|
|
132
|
+
when "$case" then render_case(args)
|
|
133
|
+
when "$excluded" then render_excluded(args)
|
|
134
|
+
else
|
|
135
|
+
"#{op[1..]}(#{args.map { |a| render_expr(a) }.join(", ")})"
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def self.render_as(args)
|
|
140
|
+
return "" if args.length < 2
|
|
141
|
+
expr = render_expr(args[0])
|
|
142
|
+
als = render_expr(args[1])
|
|
143
|
+
if args[0].is_a?(Array)
|
|
144
|
+
"(#{expr}) as #{als}"
|
|
145
|
+
elsif args[0].is_a?(String) && symbol_str?(args[0])
|
|
146
|
+
"#{args[0][1..]} as #{als}"
|
|
147
|
+
else
|
|
148
|
+
"#{expr} as #{als}"
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def self.render_count(args)
|
|
153
|
+
return "count(*)" if args.empty?
|
|
154
|
+
return "count(*)" if args[0].is_a?(String) && args[0] == "$*"
|
|
155
|
+
"count(#{render_expr(args[0])})"
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def self.render_agg(fn, args)
|
|
159
|
+
return "#{fn}(*)" if args.empty?
|
|
160
|
+
"#{fn}(#{render_expr(args[0])})"
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def self.render_binary(op, args)
|
|
164
|
+
return "" if args.length < 2
|
|
165
|
+
"#{render_expr(args[0])} #{op} #{render_expr(args[1])}"
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def self.render_is(args)
|
|
169
|
+
return "" if args.length < 2
|
|
170
|
+
left = render_expr(args[0])
|
|
171
|
+
return "#{left} is null" if args[1].nil?
|
|
172
|
+
"#{left} is #{render_expr(args[1])}"
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def self.render_is_not(args)
|
|
176
|
+
return "" if args.length < 2
|
|
177
|
+
left = render_expr(args[0])
|
|
178
|
+
return "#{left} is not null" if args[1].nil?
|
|
179
|
+
"#{left} is not #{render_expr(args[1])}"
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def self.render_logical(op, args)
|
|
183
|
+
return op == "and" ? "true" : "false" if args.empty?
|
|
184
|
+
parts = args.map do |a|
|
|
185
|
+
r = render_expr(a)
|
|
186
|
+
parenthesized?(r) ? r : "(#{r})"
|
|
187
|
+
end
|
|
188
|
+
"(#{parts.join(" #{op} ")})"
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def self.render_in(args)
|
|
192
|
+
return "" if args.length < 2
|
|
193
|
+
col = render_expr(args[0])
|
|
194
|
+
val = args[1]
|
|
195
|
+
if val.is_a?(Array)
|
|
196
|
+
if subquery?(val)
|
|
197
|
+
"#{col} in (#{render_subquery(val)})"
|
|
198
|
+
else
|
|
199
|
+
"#{col} in (#{val.map { |v| render_expr(v) }.join(", ")})"
|
|
200
|
+
end
|
|
201
|
+
else
|
|
202
|
+
"#{col} in (#{render_expr(val)})"
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def self.render_case(args)
|
|
207
|
+
parts = ["case"]
|
|
208
|
+
else_val = nil
|
|
209
|
+
args.each do |arg|
|
|
210
|
+
if arg.is_a?(Array) && !arg.empty? && arg[0].is_a?(String)
|
|
211
|
+
if arg[0] == "$when" && arg.length >= 3
|
|
212
|
+
parts << "when #{render_expr(arg[1])} then #{render_expr(arg[2])}"
|
|
213
|
+
elsif arg[0] == "$else" && arg.length >= 2
|
|
214
|
+
else_val = render_expr(arg[1])
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
parts << "else #{else_val}" if else_val
|
|
219
|
+
parts << "end"
|
|
220
|
+
parts.join(" ")
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def self.render_excluded(args)
|
|
224
|
+
return "excluded" if args.empty?
|
|
225
|
+
"excluded.#{render_expr(args[0])}"
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# ============================================================
|
|
229
|
+
# Subquery rendering
|
|
230
|
+
# ============================================================
|
|
231
|
+
|
|
232
|
+
def self.render_subquery(clauses)
|
|
233
|
+
clauses.select { |c| c.is_a?(Array) }
|
|
234
|
+
.map { |c| render_clause(c) }
|
|
235
|
+
.reject(&:empty?)
|
|
236
|
+
.join(" ")
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# ============================================================
|
|
240
|
+
# Clause renderer
|
|
241
|
+
# ============================================================
|
|
242
|
+
|
|
243
|
+
def self.render_clause(clause)
|
|
244
|
+
return "" if clause.empty?
|
|
245
|
+
kw = clause[0]
|
|
246
|
+
return "" unless kw.is_a?(String) && kw.start_with?("$")
|
|
247
|
+
args = clause[1..]
|
|
248
|
+
|
|
249
|
+
case kw
|
|
250
|
+
when "$select"
|
|
251
|
+
"select #{args.map { |a| render_expr(a) }.join(", ")}"
|
|
252
|
+
when "$from"
|
|
253
|
+
"from #{render_table(args)}"
|
|
254
|
+
when "$join"
|
|
255
|
+
"join #{render_join(args)}"
|
|
256
|
+
when "$left-join"
|
|
257
|
+
"left join #{render_join(args)}"
|
|
258
|
+
when "$right-join"
|
|
259
|
+
"right join #{render_join(args)}"
|
|
260
|
+
when "$full-join"
|
|
261
|
+
"full join #{render_join(args)}"
|
|
262
|
+
when "$cross-join"
|
|
263
|
+
"cross join #{render_table(args)}"
|
|
264
|
+
when "$where"
|
|
265
|
+
args.empty? ? "" : "where #{render_expr(args[0])}"
|
|
266
|
+
when "$group-by"
|
|
267
|
+
args.empty? ? "" : "group by #{render_expr(args[0])}"
|
|
268
|
+
when "$having"
|
|
269
|
+
args.empty? ? "" : "having #{render_expr(args[0])}"
|
|
270
|
+
when "$order-by"
|
|
271
|
+
render_order_by(args)
|
|
272
|
+
when "$limit"
|
|
273
|
+
args.empty? ? "" : "limit #{render_expr(args[0])}"
|
|
274
|
+
when "$offset"
|
|
275
|
+
args.empty? ? "" : "offset #{render_expr(args[0])}"
|
|
276
|
+
when "$with"
|
|
277
|
+
render_with(args)
|
|
278
|
+
when "$insert-into"
|
|
279
|
+
render_insert_into(args)
|
|
280
|
+
when "$values"
|
|
281
|
+
"values (#{args.map { |a| render_expr(a) }.join(", ")})"
|
|
282
|
+
when "$update"
|
|
283
|
+
"update #{render_table(args)}"
|
|
284
|
+
when "$set"
|
|
285
|
+
render_set(args)
|
|
286
|
+
when "$delete-from"
|
|
287
|
+
"delete from #{render_table(args)}"
|
|
288
|
+
when "$on-conflict"
|
|
289
|
+
args.empty? ? "" : "on conflict (#{render_expr(args[0])})"
|
|
290
|
+
when "$do-update"
|
|
291
|
+
render_do_update(args)
|
|
292
|
+
else
|
|
293
|
+
kw[1..]
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def self.render_table(args)
|
|
298
|
+
return "" if args.empty?
|
|
299
|
+
first = args[0]
|
|
300
|
+
if first.is_a?(Array) && !first.empty? && first[0] == "$as"
|
|
301
|
+
render_list_expr(first)
|
|
302
|
+
else
|
|
303
|
+
render_expr(first)
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def self.render_join(args)
|
|
308
|
+
return "" if args.empty?
|
|
309
|
+
table = render_expr(args[0])
|
|
310
|
+
return "#{table} on #{render_expr(args[1])}" if args.length >= 2
|
|
311
|
+
table
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def self.render_order_by(args)
|
|
315
|
+
return "" if args.empty?
|
|
316
|
+
col = render_expr(args[0])
|
|
317
|
+
if args.length >= 2 && args[1].is_a?(String) && symbol_str?(args[1])
|
|
318
|
+
dir = args[1][1..]
|
|
319
|
+
return "order by #{col} #{dir}" if %w[desc asc].include?(dir)
|
|
320
|
+
end
|
|
321
|
+
return "order by #{col} #{render_expr(args[1])}" if args.length >= 2
|
|
322
|
+
"order by #{col}"
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def self.render_with(args)
|
|
326
|
+
return "" if args.empty? || !args[0].is_a?(Hash)
|
|
327
|
+
cte_defs = args[0]
|
|
328
|
+
cte_parts = cte_defs.map do |cte_name_key, cte_clauses|
|
|
329
|
+
cte_name = symbol_str?(cte_name_key) ? cte_name_key[1..] : cte_name_key
|
|
330
|
+
next unless cte_clauses.is_a?(Array) && !cte_clauses.empty?
|
|
331
|
+
inner = cte_clauses.map { |c| c.is_a?(Array) ? render_clause(c) : "" }.reject(&:empty?).join(" ")
|
|
332
|
+
"#{cte_name} as (#{inner})"
|
|
333
|
+
end.compact
|
|
334
|
+
return "" if cte_parts.empty?
|
|
335
|
+
"with #{cte_parts.join(",\n")}"
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def self.render_insert_into(args)
|
|
339
|
+
return "insert into" if args.empty?
|
|
340
|
+
table = render_expr(args[0])
|
|
341
|
+
if args.length > 1
|
|
342
|
+
cols = args[1..].map { |a| render_expr(a) }.join(", ")
|
|
343
|
+
"insert into #{table} (#{cols})"
|
|
344
|
+
else
|
|
345
|
+
"insert into #{table}"
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def self.render_set(args)
|
|
350
|
+
return "" if args.empty? || !args[0].is_a?(Hash)
|
|
351
|
+
parts = args[0].map { |col, val| "#{render_key(col)} = #{render_expr(val)}" }
|
|
352
|
+
"set #{parts.join(", ")}"
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
def self.render_do_update(args)
|
|
356
|
+
return "" if args.empty? || !args[0].is_a?(Hash)
|
|
357
|
+
parts = args[0].map { |col, val| "#{render_key(col)} = #{render_expr(val)}" }
|
|
358
|
+
"do update set #{parts.join(", ")}"
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
# ============================================================
|
|
362
|
+
# $sql functor
|
|
363
|
+
# ============================================================
|
|
364
|
+
|
|
365
|
+
def self.sql_fn(env, *args)
|
|
366
|
+
return "" if args.empty?
|
|
367
|
+
data = args[0]
|
|
368
|
+
return "" unless data.is_a?(Array) && !data.empty?
|
|
369
|
+
|
|
370
|
+
parts = []
|
|
371
|
+
pending_values = []
|
|
372
|
+
|
|
373
|
+
flush_values = -> {
|
|
374
|
+
unless pending_values.empty?
|
|
375
|
+
rows = pending_values.map { |vlist|
|
|
376
|
+
"(#{vlist.map { |v| render_expr(v) }.join(", ")})"
|
|
377
|
+
}
|
|
378
|
+
parts << "values #{rows.join(", ")}"
|
|
379
|
+
pending_values.clear
|
|
380
|
+
end
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
data.each do |item|
|
|
384
|
+
next unless item.is_a?(Array) && !item.empty?
|
|
385
|
+
if item[0] == "$values"
|
|
386
|
+
pending_values << item[1..]
|
|
387
|
+
next
|
|
388
|
+
end
|
|
389
|
+
flush_values.call
|
|
390
|
+
rendered = render_clause(item)
|
|
391
|
+
parts << rendered unless rendered.empty?
|
|
392
|
+
end
|
|
393
|
+
flush_values.call
|
|
394
|
+
|
|
395
|
+
parts.join("\n")
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
# ============================================================
|
|
399
|
+
# Legacy $query / $pattern (backward compatibility)
|
|
400
|
+
# ============================================================
|
|
401
|
+
|
|
402
|
+
def self.pattern_to_triple(subject, predicate, object)
|
|
403
|
+
if subject == "$*" && object == "$*"
|
|
404
|
+
[predicate]
|
|
405
|
+
else
|
|
406
|
+
[subject, predicate, object]
|
|
407
|
+
end
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
def self.triple_to_sql_condition(triple)
|
|
411
|
+
inner = triple.map { |s| %("#{s}") }.join(",")
|
|
412
|
+
json = %({"triple":[#{inner}]})
|
|
413
|
+
escaped = json.gsub("'", "''")
|
|
414
|
+
"meta @> '#{escaped}'"
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
def self.pattern_fn(env, *args)
|
|
418
|
+
raise "$pattern requires (subject, predicate, object)" if args.length < 3
|
|
419
|
+
subj, pred, obj = args[0..2].map { |a| env.eval(a) }
|
|
420
|
+
triple = pattern_to_triple(subj, pred, obj)
|
|
421
|
+
triple_to_sql_condition(triple)
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
def self.legacy_and(env, *args)
|
|
425
|
+
args.map { |a| env.eval(a).to_s }.join(" and ")
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
def self.wildcard_fn(_env, *_args)
|
|
429
|
+
"*"
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
LOCAL_SQL_FUNCTORS = {
|
|
433
|
+
"$pattern" => method(:pattern_fn),
|
|
434
|
+
"$and" => method(:legacy_and),
|
|
435
|
+
"$*" => method(:wildcard_fn),
|
|
436
|
+
}.freeze
|
|
437
|
+
|
|
438
|
+
def self.query_fn(env, *args)
|
|
439
|
+
raise "$query expects a condition expression" if args.empty?
|
|
440
|
+
local = Env.new(parent: env)
|
|
441
|
+
local.load(LOCAL_SQL_FUNCTORS)
|
|
442
|
+
parser = Parser.new(local)
|
|
443
|
+
condition = parser.parse(args)
|
|
444
|
+
where = condition.apply(local)
|
|
445
|
+
where = where.is_a?(Array) ? where.first.to_s : where.to_s
|
|
446
|
+
"select #{QUERY_FIELDS} \nfrom statement \nwhere \n #{where} \noffset 0\nlimit 100 \n"
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
SQL_FUNCTORS = {
|
|
450
|
+
"$sql" => method(:sql_fn),
|
|
451
|
+
"$query" => method(:query_fn),
|
|
452
|
+
}.freeze
|
|
453
|
+
end
|
|
454
|
+
end
|
|
455
|
+
end
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
module JSE
|
|
2
|
+
module Functors
|
|
3
|
+
module Utils
|
|
4
|
+
def self.eval_arg(env, arg)
|
|
5
|
+
arg.respond_to?(:apply) ? env.eval(arg) : arg
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def self.not_fn(env, *args)
|
|
9
|
+
return true if args.empty?
|
|
10
|
+
!eval_arg(env, args[0])
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.and_fn(env, *args)
|
|
14
|
+
return true if args.empty?
|
|
15
|
+
args.each do |arg|
|
|
16
|
+
return false unless eval_arg(env, arg)
|
|
17
|
+
end
|
|
18
|
+
true
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.or_fn(env, *args)
|
|
22
|
+
return false if args.empty?
|
|
23
|
+
args.each do |arg|
|
|
24
|
+
val = eval_arg(env, arg)
|
|
25
|
+
return val if val
|
|
26
|
+
end
|
|
27
|
+
false
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.listp(env, *args)
|
|
31
|
+
return false if args.empty?
|
|
32
|
+
eval_arg(env, args[0]).is_a?(Array)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.mapp(env, *args)
|
|
36
|
+
return false if args.empty?
|
|
37
|
+
eval_arg(env, args[0]).is_a?(Hash)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.nullp(env, *args)
|
|
41
|
+
return true if args.empty?
|
|
42
|
+
eval_arg(env, args[0]).nil?
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def self.get_fn(env, *args)
|
|
46
|
+
raise "$get requires (collection, key)" if args.length < 2
|
|
47
|
+
coll = eval_arg(env, args[0])
|
|
48
|
+
key = eval_arg(env, args[1])
|
|
49
|
+
if coll.is_a?(Hash)
|
|
50
|
+
coll[key]
|
|
51
|
+
elsif coll.is_a?(Array)
|
|
52
|
+
raise "$get on list requires integer index" unless key.is_a?(Integer)
|
|
53
|
+
raise IndexError, "Index #{key} out of range" if key < 0 || key >= coll.length
|
|
54
|
+
coll[key]
|
|
55
|
+
else
|
|
56
|
+
raise "$get first argument must be map or list"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def self.set_fn(env, *args)
|
|
61
|
+
raise "$set requires (collection, key, value)" if args.length < 3
|
|
62
|
+
coll = eval_arg(env, args[0])
|
|
63
|
+
key = eval_arg(env, args[1])
|
|
64
|
+
val = eval_arg(env, args[2])
|
|
65
|
+
if coll.is_a?(Hash)
|
|
66
|
+
coll[key] = val
|
|
67
|
+
elsif coll.is_a?(Array)
|
|
68
|
+
raise "$set on list requires integer index" unless key.is_a?(Integer)
|
|
69
|
+
coll[key] = val
|
|
70
|
+
else
|
|
71
|
+
raise "$set first argument must be map or list"
|
|
72
|
+
end
|
|
73
|
+
coll
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def self.del_fn(env, *args)
|
|
77
|
+
raise "$del requires (collection, key)" if args.length < 2
|
|
78
|
+
coll = eval_arg(env, args[0])
|
|
79
|
+
key = eval_arg(env, args[1])
|
|
80
|
+
if coll.is_a?(Hash)
|
|
81
|
+
coll.delete(key) { raise KeyError, "Key '#{key}' not found" }
|
|
82
|
+
elsif coll.is_a?(Array)
|
|
83
|
+
raise "$del on list requires integer index" unless key.is_a?(Integer)
|
|
84
|
+
coll.delete_at(key)
|
|
85
|
+
else
|
|
86
|
+
raise "$del first argument must be map or list"
|
|
87
|
+
end
|
|
88
|
+
coll
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def self.conj(env, *args)
|
|
92
|
+
raise "$conj requires exactly 2 arguments" unless args.length == 2
|
|
93
|
+
elem = eval_arg(env, args[0])
|
|
94
|
+
lst = eval_arg(env, args[1])
|
|
95
|
+
raise "$conj second argument must be a list" unless lst.is_a?(Array)
|
|
96
|
+
lst + [elem]
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def self.eq2(env, *args)
|
|
100
|
+
evaluated = args.map { |a| eval_arg(env, a) }
|
|
101
|
+
case evaluated.length
|
|
102
|
+
when 1 then true
|
|
103
|
+
when 2 then evaluated[0] == evaluated[1]
|
|
104
|
+
else
|
|
105
|
+
evaluated.each_cons(2).all? { |a, b| a == b }
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
UTILS_FUNCTORS = {
|
|
110
|
+
"$not" => method(:not_fn),
|
|
111
|
+
"$list?" => method(:listp),
|
|
112
|
+
"$map?" => method(:mapp),
|
|
113
|
+
"$null?" => method(:nullp),
|
|
114
|
+
"$get" => method(:get_fn),
|
|
115
|
+
"$set" => method(:set_fn),
|
|
116
|
+
"$del" => method(:del_fn),
|
|
117
|
+
"$conj" => method(:conj),
|
|
118
|
+
"$and" => method(:and_fn),
|
|
119
|
+
"$or" => method(:or_fn),
|
|
120
|
+
}.freeze
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
data/lib/jse/version.rb
ADDED
data/lib/jse.rb
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
require_relative "jse/version"
|
|
2
|
+
require_relative "jse/env"
|
|
3
|
+
require_relative "jse/ast/base"
|
|
4
|
+
require_relative "jse/ast/nodes"
|
|
5
|
+
require_relative "jse/ast/parser"
|
|
6
|
+
require_relative "jse/engine"
|
|
7
|
+
require_relative "jse/functors/builtin"
|
|
8
|
+
require_relative "jse/functors/utils"
|
|
9
|
+
require_relative "jse/functors/lisp"
|
|
10
|
+
require_relative "jse/functors/sql"
|
|
11
|
+
|
|
12
|
+
module JSE
|
|
13
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: jse4r
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.2.4
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Mars Liu
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-04-30 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: JSE is a JSON-based structural expression specification. It extends JSON
|
|
14
|
+
from a data carrier into a medium that can express structured intent and computational
|
|
15
|
+
logic.
|
|
16
|
+
email: mars.liu@outlook.com
|
|
17
|
+
executables: []
|
|
18
|
+
extensions: []
|
|
19
|
+
extra_rdoc_files: []
|
|
20
|
+
files:
|
|
21
|
+
- lib/jse.rb
|
|
22
|
+
- lib/jse/ast/base.rb
|
|
23
|
+
- lib/jse/ast/nodes.rb
|
|
24
|
+
- lib/jse/ast/parser.rb
|
|
25
|
+
- lib/jse/engine.rb
|
|
26
|
+
- lib/jse/env.rb
|
|
27
|
+
- lib/jse/functors/builtin.rb
|
|
28
|
+
- lib/jse/functors/lisp.rb
|
|
29
|
+
- lib/jse/functors/sql.rb
|
|
30
|
+
- lib/jse/functors/utils.rb
|
|
31
|
+
- lib/jse/version.rb
|
|
32
|
+
homepage: https://github.com/MarchLiu/jse
|
|
33
|
+
licenses:
|
|
34
|
+
- MIT
|
|
35
|
+
metadata: {}
|
|
36
|
+
post_install_message:
|
|
37
|
+
rdoc_options: []
|
|
38
|
+
require_paths:
|
|
39
|
+
- lib
|
|
40
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
41
|
+
requirements:
|
|
42
|
+
- - ">="
|
|
43
|
+
- !ruby/object:Gem::Version
|
|
44
|
+
version: '3.0'
|
|
45
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
46
|
+
requirements:
|
|
47
|
+
- - ">="
|
|
48
|
+
- !ruby/object:Gem::Version
|
|
49
|
+
version: '0'
|
|
50
|
+
requirements: []
|
|
51
|
+
rubygems_version: 3.3.26
|
|
52
|
+
signing_key:
|
|
53
|
+
specification_version: 4
|
|
54
|
+
summary: JSON Structural Expression (JSE) runtime for Ruby
|
|
55
|
+
test_files: []
|