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 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
@@ -0,0 +1,19 @@
1
+ module JSE
2
+ module Ast
3
+ class AstNode
4
+ def initialize(env)
5
+ @env = env
6
+ end
7
+
8
+ attr_reader :env
9
+
10
+ def apply(call_env)
11
+ raise NotImplementedError
12
+ end
13
+
14
+ def to_json
15
+ raise NotImplementedError
16
+ end
17
+ end
18
+ end
19
+ end
@@ -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
@@ -0,0 +1,3 @@
1
+ module JSE
2
+ VERSION = "0.2.4"
3
+ end
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: []