fusion-lang 0.0.1.alpha2 → 0.0.1

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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +9 -8
  3. data/docs/lang/design.md +240 -51
  4. data/docs/lang/implementation.md +238 -0
  5. data/docs/lang/roadmap.md +20 -36
  6. data/docs/user/explanation.md +5 -10
  7. data/docs/user/how-to-guides.md +60 -15
  8. data/docs/user/reference.md +356 -142
  9. data/docs/user/tutorial.md +21 -19
  10. data/examples/double.fsn +1 -1
  11. data/examples/factorial.fsn +2 -2
  12. data/examples/fizzbuzz.fsn +1 -4
  13. data/examples/json_test.fsn +4 -0
  14. data/examples/palindrome.fsn +1 -1
  15. data/exe/fusion +10 -10
  16. data/lib/fusion/ast.rb +2 -1
  17. data/lib/fusion/cli/decoder.rb +10 -5
  18. data/lib/fusion/cli/options.rb +130 -60
  19. data/lib/fusion/cli/parser.rb +3 -3
  20. data/lib/fusion/cli/repl.rb +30 -25
  21. data/lib/fusion/cli/serializer.rb +5 -4
  22. data/lib/fusion/cli.rb +119 -48
  23. data/lib/fusion/interpreter/builtins.rb +260 -151
  24. data/lib/fusion/interpreter/env.rb +42 -12
  25. data/lib/fusion/interpreter/error_val.rb +42 -20
  26. data/lib/fusion/interpreter/thunk.rb +53 -0
  27. data/lib/fusion/interpreter.rb +239 -82
  28. data/lib/fusion/lexer.rb +69 -3
  29. data/lib/fusion/parser.rb +189 -51
  30. data/lib/fusion/version.rb +1 -1
  31. data/stdlib/all.fsn +13 -0
  32. data/stdlib/any.fsn +12 -0
  33. data/stdlib/chars.fsn +5 -0
  34. data/stdlib/compact.fsn +6 -0
  35. data/stdlib/concat.fsn +5 -0
  36. data/stdlib/falsey.fsn +6 -0
  37. data/stdlib/filter.fsn +12 -0
  38. data/stdlib/flatten.fsn +7 -0
  39. data/stdlib/gt.fsn +9 -0
  40. data/stdlib/gte.fsn +9 -0
  41. data/stdlib/lt.fsn +9 -0
  42. data/stdlib/lte.fsn +9 -0
  43. data/stdlib/map.fsn +6 -4
  44. data/stdlib/range.fsn +2 -2
  45. data/stdlib/reduce.fsn +8 -0
  46. data/stdlib/sanitize.fsn +2 -2
  47. data/stdlib/truthy.fsn +7 -0
  48. metadata +18 -4
  49. data/lib/fusion/interpreter/file_thunk.rb +0 -39
  50. data/stdlib/mapValues.fsn +0 -5
  51. data/stdlib/math/square.fsn +0 -4
@@ -11,29 +11,18 @@ module Fusion
11
11
  @interp = interp
12
12
  define = ->(name, fn) { table[name] = NativeFunc.new(name, fn) }
13
13
 
14
- # operations on a pair [a, b] (or a single value)
15
- define.call("add", method(:add))
16
- define.call("subtract", method(:subtract))
17
- define.call("multiply", method(:multiply))
18
- define.call("divide", method(:divide))
19
- define.call("mod", method(:mod))
20
- define.call("negate", method(:negate))
21
- define.call("floor", method(:floor))
22
- define.call("equals", method(:equals))
23
- define.call("lessThan", method(:less_than))
24
- define.call("and", method(:and_))
25
- define.call("or", method(:or_))
26
- define.call("not", method(:not_))
27
- define.call("length", method(:length))
28
- define.call("concat", method(:concat))
29
- define.call("chars", method(:chars))
14
+ # Irreducible primitives kept as built-ins. The sugar-target operators
15
+ # (arithmetic/comparison/boolean) live in `@OP` below; their friendly
16
+ # derived forms (`@lt`, `@gt`, `@truthy`, …) are stdlib files that build on
17
+ # `@OP.*`, so they follow a per-directory `@OP` override. Numeric functions
18
+ # (`round`, `divide`, `sin`, …) and constants (`pi`, `e`) live in `@math`.
19
+ define.call("size", method(:size))
30
20
  define.call("join", method(:join))
21
+ define.call("split", method(:split))
31
22
  define.call("toString", method(:to_string))
32
23
  define.call("parseNumber", method(:parse_number))
33
24
  define.call("keys", method(:keys))
34
25
  define.call("values", method(:values))
35
- define.call("get", method(:get))
36
- define.call("set", method(:set))
37
26
  define.call("toObject", method(:to_object))
38
27
 
39
28
  # type predicates: return false on any non-matching value, never an error
@@ -47,161 +36,318 @@ module Fusion
47
36
  define.call("Null", method(:null?))
48
37
  define.call("Function", method(:function?))
49
38
  define.call("NonFinite", method(:non_finite?))
50
- end
51
39
 
52
- # --- arithmetic ---
40
+ # `OP` bundles the sugar-target operators as a shadowable builtin object,
41
+ # reached as `@OP.sum`, `@OP.and`, … A directory can swap the operators by
42
+ # placing an `OP.fsn` sibling that overrides members (reaching the
43
+ # originals with `@@`); infix sugar and the derived stdlib helpers resolve
44
+ # `@OP` per directory, so they follow the override.
45
+ table["OP"] = {
46
+ "sum" => NativeFunc.new("OP.sum", method(:op_sum)),
47
+ "product" => NativeFunc.new("OP.product", method(:op_product)),
48
+ "negate" => NativeFunc.new("OP.negate", method(:op_negate)),
49
+ "invert" => NativeFunc.new("OP.invert", method(:op_invert)),
50
+ "quotient" => NativeFunc.new("OP.quotient", method(:op_quotient)),
51
+ "modulo" => NativeFunc.new("OP.modulo", method(:op_modulo)),
52
+ "equal" => NativeFunc.new("OP.equal", method(:op_equal)),
53
+ "compare" => NativeFunc.new("OP.compare", method(:op_compare)),
54
+ "and" => NativeFunc.new("OP.and", method(:op_and)),
55
+ "or" => NativeFunc.new("OP.or", method(:op_or)),
56
+ "not" => NativeFunc.new("OP.not", method(:op_not)),
57
+ }
58
+
59
+ # `math` bundles numeric functions and constants, reached as `@math.round`,
60
+ # `@math.pi`, … Like `@OP`, it is a shadowable builtin object. `pi`/`e` are
61
+ # plain values; the rest are one-argument functions.
62
+ table["math"] = {
63
+ "round" => NativeFunc.new("math.round", method(:math_round)),
64
+ "floor" => NativeFunc.new("math.floor", method(:math_floor)),
65
+ "ceil" => NativeFunc.new("math.ceil", method(:math_ceil)),
66
+ "divide" => NativeFunc.new("math.divide", method(:math_divide)),
67
+ "sign" => NativeFunc.new("math.sign", method(:math_sign)),
68
+ "abs" => NativeFunc.new("math.abs", method(:math_abs)),
69
+ "rand" => NativeFunc.new("math.rand", method(:math_rand)),
70
+ "sin" => NativeFunc.new("math.sin", method(:math_sin)),
71
+ "cos" => NativeFunc.new("math.cos", method(:math_cos)),
72
+ "exp" => NativeFunc.new("math.exp", method(:math_exp)),
73
+ "log" => NativeFunc.new("math.log", method(:math_log)),
74
+ "pow" => NativeFunc.new("math.pow", method(:math_pow)),
75
+ "sqrt" => NativeFunc.new("math.sqrt", method(:math_sqrt)),
76
+ "pi" => Math::PI,
77
+ "e" => Math::E,
78
+ }
79
+ end
80
+
81
+ # --- math (reached as `@math.round`, `@math.divide`, `@math.pi`, …) ---
82
+
83
+ NUMBER_PAIR = ["[_ ? @Number, _ ? @Number]"].freeze
84
+ NUMBER_ARRAY = ['_ ? (xs => {"c": xs, "f": @Number} | @all)'].freeze
85
+ NUMBER = ["_ ? @Number"].freeze
86
+
87
+ # `round`/`floor`/`ceil` return an integer; a non-finite input is a math_error
88
+ # (their Ruby forms raise `FloatDomainError` on it).
89
+ def math_round(v)
90
+ return v if v.is_a?(ErrorVal)
91
+ return argument_error("math.round", v, NUMBER) unless numeric?(v)
92
+ return error("math_error", "math.round", v, "not a finite number") if non_finite?(v)
53
93
 
54
- def add(v)
94
+ v.round
95
+ end
96
+
97
+ def math_floor(v)
55
98
  return v if v.is_a?(ErrorVal)
56
- return error("argument_error", "add", v, "expected [_, _]") unless pair?(v)
57
- return error("type_error", "add", v, "expected numbers") unless numeric?(v[0])
58
- return error("type_error", "add", v, "expected numbers") unless numeric?(v[1])
99
+ return argument_error("math.floor", v, NUMBER) unless numeric?(v)
100
+ return error("math_error", "math.floor", v, "not a finite number") if non_finite?(v)
59
101
 
60
- v[0] + v[1]
102
+ v.floor
61
103
  end
62
104
 
63
- def subtract(v)
105
+ def math_ceil(v)
64
106
  return v if v.is_a?(ErrorVal)
65
- return error("argument_error", "subtract", v, "expected [_, _]") unless pair?(v)
66
- return error("type_error", "subtract", v, "expected numbers") unless numeric?(v[0])
67
- return error("type_error", "subtract", v, "expected numbers") unless numeric?(v[1])
107
+ return argument_error("math.ceil", v, NUMBER) unless numeric?(v)
108
+ return error("math_error", "math.ceil", v, "not a finite number") if non_finite?(v)
68
109
 
69
- v[0] - v[1]
110
+ v.ceil
70
111
  end
71
112
 
72
- def multiply(v)
113
+ # `divide` always yields a float. Integer division is `@OP.quotient`, the
114
+ # remainder `@OP.modulo`.
115
+ def math_divide(v)
73
116
  return v if v.is_a?(ErrorVal)
74
- return error("argument_error", "multiply", v, "expected [_, _]") unless pair?(v)
75
- return error("type_error", "multiply", v, "expected numbers") unless numeric?(v[0])
76
- return error("type_error", "multiply", v, "expected numbers") unless numeric?(v[1])
117
+ return argument_error("math.divide", v, NUMBER_PAIR) unless pair?(v) && numeric?(v[0]) && numeric?(v[1])
118
+ return error("math_error", "math.divide", v, "division by zero") if v[1] == 0
77
119
 
78
- v[0] * v[1]
120
+ v[0].to_f / v[1]
79
121
  end
80
122
 
81
- def divide(v)
123
+ # -1 / 0 / 1 (via `<`/`>`, so NaN → 0, never Ruby's `nil`).
124
+ def math_sign(v)
82
125
  return v if v.is_a?(ErrorVal)
83
- return error("argument_error", "divide", v, "expected [_, _]") unless pair?(v)
84
- return error("type_error", "divide", v, "expected numbers") unless numeric?(v[0])
85
- return error("type_error", "divide", v, "expected numbers") unless numeric?(v[1])
86
- return error("math_error", "divide", v, "division by zero") if v[1] == 0
126
+ return argument_error("math.sign", v, NUMBER) unless numeric?(v)
87
127
 
88
- a, b = v
89
- if a.is_a?(Integer) && b.is_a?(Integer) && (a % b).zero?
90
- a / b
91
- else
92
- a.to_f / b
128
+ if v < 0 then -1
129
+ elsif v > 0 then 1
130
+ else 0
93
131
  end
94
132
  end
95
133
 
96
- def mod(v)
134
+ def math_abs(v)
97
135
  return v if v.is_a?(ErrorVal)
98
- return error("argument_error", "mod", v, "expected [_, _]") unless pair?(v)
99
- return error("type_error", "mod", v, "expected numbers") unless numeric?(v[0])
100
- return error("type_error", "mod", v, "expected numbers") unless numeric?(v[1])
101
- return error("math_error", "mod", v, "modulo by zero") if v[1] == 0
136
+ return argument_error("math.abs", v, NUMBER) unless numeric?(v)
102
137
 
103
- v[0] % v[1]
138
+ v.abs
104
139
  end
105
140
 
106
- def negate(v)
141
+ # `null` → a float in `[0.0, 1.0)`; a positive integer `n` → an integer in
142
+ # `[0, n)`.
143
+ def math_rand(v)
107
144
  return v if v.is_a?(ErrorVal)
108
- return error("type_error", "negate", v, "expected a number") unless numeric?(v)
145
+ return rand if v == NULL
146
+ return rand(v) if integer?(v) && v.positive?
109
147
 
110
- -v
148
+ argument_error("math.rand", v, ["_ ? @Null", '_ ? (n ? @Integer => [0, n] | @OP.compare | (-1 => true))'])
111
149
  end
112
150
 
113
- def floor(v)
151
+ def math_sin(v)
114
152
  return v if v.is_a?(ErrorVal)
115
- return error("type_error", "floor", v, "expected a number") unless numeric?(v)
116
- return error("math_error", "floor", v, "not a finite number") if non_finite?(v)
153
+ return argument_error("math.sin", v, NUMBER) unless numeric?(v)
117
154
 
118
- v.floor
155
+ Math.sin(v)
156
+ end
157
+
158
+ def math_cos(v)
159
+ return v if v.is_a?(ErrorVal)
160
+ return argument_error("math.cos", v, NUMBER) unless numeric?(v)
161
+
162
+ Math.cos(v)
163
+ end
164
+
165
+ def math_exp(v)
166
+ return v if v.is_a?(ErrorVal)
167
+ return argument_error("math.exp", v, NUMBER) unless numeric?(v)
168
+
169
+ Math.exp(v)
119
170
  end
120
171
 
121
- # --- comparison ---
172
+ # Square root; the domain is non-negative numbers (a negative would be
173
+ # complex).
174
+ def math_sqrt(v)
175
+ return v if v.is_a?(ErrorVal)
176
+ return argument_error("math.sqrt", v, NUMBER) unless numeric?(v)
177
+ return error("math_error", "math.sqrt", v, "square root of a negative number") if v < 0
178
+
179
+ Math.sqrt(v)
180
+ end
122
181
 
123
- def equals(v)
182
+ # Natural logarithm; the domain is positive numbers (`Math.log` raises on a
183
+ # negative and gives `-Infinity` at 0).
184
+ def math_log(v)
124
185
  return v if v.is_a?(ErrorVal)
125
- return error("argument_error", "equals", v, "expected [_, _]") unless pair?(v)
186
+ return argument_error("math.log", v, NUMBER) unless numeric?(v)
187
+ return error("math_error", "math.log", v, "log of a non-positive number") if v <= 0
126
188
 
127
- @interp.deep_equal?(v[0], v[1])
189
+ Math.log(v)
128
190
  end
129
191
 
130
- def less_than(v)
192
+ # `base ** exponent`: integer when the base and a non-negative integer
193
+ # exponent are integers, a float otherwise. A negative base with a fractional
194
+ # exponent (a complex result) is a math_error.
195
+ def math_pow(v)
131
196
  return v if v.is_a?(ErrorVal)
132
- return error("argument_error", "lessThan", v, "expected [_, _]") unless pair?(v)
197
+ return argument_error("math.pow", v, NUMBER_PAIR) unless pair?(v) && numeric?(v[0]) && numeric?(v[1])
133
198
 
134
199
  a, b = v
135
- if numeric?(a) && numeric?(b)
136
- a < b
137
- elsif a.is_a?(String) && b.is_a?(String)
138
- a < b
139
- else
140
- error("type_error", "lessThan", v, "expected two numbers or two strings")
141
- end
200
+ result = a.is_a?(Integer) && b.is_a?(Integer) && !b.negative? ? a**b : a.to_f**b
201
+ return error("math_error", "math.pow", v, "not in domain (complex result)") if result.is_a?(Complex)
202
+
203
+ result
142
204
  end
143
205
 
144
- # --- boolean ---
206
+ # --- OP: the sugar-target operators (reached as `@OP.sum`, `@OP.and`, …) ---
207
+ #
208
+ # The arithmetic, boolean, and equality members take an array of ANY length;
209
+ # the unary ones take a single value; `compare` returns -1 / 0 / 1. The
210
+ # friendly derived forms (`@lt`, `@gt`, `@truthy`, …) are stdlib files built on
211
+ # these.
145
212
 
146
- # `and`/`or`/`not` judge truthiness (Ruby-style: `false` and `null` are
147
- # falsey, everything else truthy), not strict booleans, and always return a
148
- # boolean. They share the interpreter's `truthy?`, the same test `?` guards use.
149
- def and_(v)
213
+ def op_sum(v)
150
214
  return v if v.is_a?(ErrorVal)
151
- return error("argument_error", "and", v, "expected [_, _]") unless pair?(v)
215
+ return argument_error("OP.sum", v, NUMBER_ARRAY) unless v.is_a?(Array) && v.all? { |x| numeric?(x) }
152
216
 
153
- @interp.truthy?(v[0]) && @interp.truthy?(v[1])
217
+ v.sum(0)
154
218
  end
155
219
 
156
- def or_(v)
220
+ def op_product(v)
157
221
  return v if v.is_a?(ErrorVal)
158
- return error("argument_error", "or", v, "expected [_, _]") unless pair?(v)
222
+ return argument_error("OP.product", v, NUMBER_ARRAY) unless v.is_a?(Array) && v.all? { |x| numeric?(x) }
159
223
 
160
- @interp.truthy?(v[0]) || @interp.truthy?(v[1])
224
+ v.reduce(1, :*)
161
225
  end
162
226
 
163
- def not_(v)
227
+ def op_negate(v)
164
228
  return v if v.is_a?(ErrorVal)
229
+ return argument_error("OP.negate", v, ["_ ? @Number"]) unless numeric?(v)
165
230
 
166
- !@interp.truthy?(v)
231
+ -v
167
232
  end
168
233
 
169
- # --- strings and structure bridges ---
234
+ # The unary reciprocal 1/x, always a float; 0 is a math_error.
235
+ def op_invert(v)
236
+ return v if v.is_a?(ErrorVal)
237
+ return argument_error("OP.invert", v, ["_ ? @Number"]) unless numeric?(v)
238
+ return error("math_error", "OP.invert", v, "division by zero") if v == 0
239
+
240
+ 1.0 / v
241
+ end
170
242
 
171
- def length(v)
243
+ INTEGER_PAIR = ["[_ ? @Integer, _ ? @Integer]"].freeze
244
+
245
+ # Integer division and its remainder — integers only (`@divide` handles the
246
+ # float case). Ruby's `/` and `%` agree in sign, so `q*b + r == a` holds.
247
+ def op_quotient(v)
172
248
  return v if v.is_a?(ErrorVal)
173
- return error("type_error", "length", v, "expected a string, array, or object") unless v.is_a?(String) || v.is_a?(Array) || v.is_a?(Hash)
249
+ return argument_error("OP.quotient", v, INTEGER_PAIR) unless pair?(v) && integer?(v[0]) && integer?(v[1])
250
+ return error("math_error", "OP.quotient", v, "division by zero") if v[1] == 0
174
251
 
175
- v.length
252
+ v[0] / v[1]
253
+ end
254
+
255
+ def op_modulo(v)
256
+ return v if v.is_a?(ErrorVal)
257
+ return argument_error("OP.modulo", v, INTEGER_PAIR) unless pair?(v) && integer?(v[0]) && integer?(v[1])
258
+ return error("math_error", "OP.modulo", v, "modulo by zero") if v[1] == 0
259
+
260
+ v[0] % v[1]
261
+ end
262
+
263
+ # Deep, exact equality across the whole array: true iff every element
264
+ # equals the first (so 0 and 1 elements are vacuously equal). Any types.
265
+ def op_equal(v)
266
+ return v if v.is_a?(ErrorVal)
267
+ return argument_error("OP.equal", v, ["_ ? @Array"]) unless v.is_a?(Array)
268
+
269
+ v.all? { |x| @interp.deep_equal?(v[0], x) }
270
+ end
271
+
272
+ COMPARE_PAIR = ["[_ ? @Number, _ ? @Number]", "[_ ? @String, _ ? @String]"].freeze
273
+
274
+ # Order two numbers or two strings: -1, 0, or 1 (no deep equality). Built on
275
+ # `<` rather than `<=>` so a NaN operand yields 0 (unordered), never Ruby's
276
+ # `nil` — NaN is a reachable value (`Infinity - Infinity`).
277
+ def op_compare(v)
278
+ return v if v.is_a?(ErrorVal)
279
+ return argument_error("OP.compare", v, COMPARE_PAIR) unless pair?(v)
280
+
281
+ a, b = v
282
+ unless (numeric?(a) && numeric?(b)) || (a.is_a?(String) && b.is_a?(String))
283
+ return argument_error("OP.compare", v, COMPARE_PAIR)
284
+ end
285
+
286
+ if a < b then -1
287
+ elsif b < a then 1
288
+ else 0
289
+ end
290
+ end
291
+
292
+ def op_and(v)
293
+ return v if v.is_a?(ErrorVal)
294
+ return argument_error("OP.and", v, ["_ ? @Array"]) unless v.is_a?(Array)
295
+
296
+ v.all? { |x| @interp.truthy?(x) }
297
+ end
298
+
299
+ def op_or(v)
300
+ return v if v.is_a?(ErrorVal)
301
+ return argument_error("OP.or", v, ["_ ? @Array"]) unless v.is_a?(Array)
302
+
303
+ v.any? { |x| @interp.truthy?(x) }
176
304
  end
177
305
 
178
- def concat(v)
306
+ def op_not(v)
179
307
  return v if v.is_a?(ErrorVal)
180
- return error("argument_error", "concat", v, "expected [_, _]") unless pair?(v)
181
- return error("type_error", "concat", v, "expected strings") unless v[0].is_a?(String) && v[1].is_a?(String)
182
308
 
183
- v[0] + v[1]
309
+ !@interp.truthy?(v)
184
310
  end
185
311
 
186
- def chars(v)
312
+ # --- strings and structure bridges ---
313
+
314
+ def size(v)
187
315
  return v if v.is_a?(ErrorVal)
188
- return error("type_error", "chars", v, "expected a string") unless v.is_a?(String)
316
+ return argument_error("size", v, ["_ ? @String", "_ ? @Array", "_ ? @Object"]) unless v.is_a?(String) || v.is_a?(Array) || v.is_a?(Hash)
189
317
 
190
- v.chars
318
+ v.length
191
319
  end
192
320
 
321
+ # `[items, separator]`: join an array of strings into one string. `@concat`
322
+ # is the stdlib pair-case built on this.
193
323
  def join(v)
194
324
  return v if v.is_a?(ErrorVal)
195
- return error("argument_error", "join", v, "expected [_, _]") unless pair?(v)
325
+ expected = ['[_ ? (xs => {"c": xs, "f": @String} | @all), _ ? @String]']
326
+ return argument_error("join", v, expected) unless pair?(v)
196
327
 
197
328
  array, separator = v
198
329
  unless array.is_a?(Array) && separator.is_a?(String) && array.all? { |item| item.is_a?(String) }
199
- return error("type_error", "join", v, "expected [array-of-strings, separator-string]")
330
+ return argument_error("join", v, expected)
200
331
  end
201
332
 
202
333
  array.join(separator)
203
334
  end
204
335
 
336
+ # `[string, separator]`: the inverse of `@join`. Splits on the LITERAL
337
+ # separator (Ruby's `" "` whitespace special-case does not apply) and keeps
338
+ # empty fields; an empty separator splits into characters. `@chars` is the
339
+ # stdlib single-string case built on this.
340
+ def split(v)
341
+ return v if v.is_a?(ErrorVal)
342
+ expected = ["[_ ? @String, _ ? @String]"]
343
+ return argument_error("split", v, expected) unless pair?(v) && v[0].is_a?(String) && v[1].is_a?(String)
344
+
345
+ string, separator = v
346
+ return string.chars if separator.empty?
347
+
348
+ string.split(Regexp.new(Regexp.escape(separator)), -1)
349
+ end
350
+
205
351
  def to_string(v)
206
352
  return v if v.is_a?(ErrorVal)
207
353
 
@@ -217,7 +363,7 @@ module Fusion
217
363
 
218
364
  def parse_number(v)
219
365
  return v if v.is_a?(ErrorVal)
220
- return error("type_error", "parseNumber", v, "expected a string") unless v.is_a?(String)
366
+ return argument_error("parseNumber", v, ["_ ? @String"]) unless v.is_a?(String)
221
367
 
222
368
  case v
223
369
  when /\A-?\d+\z/ then v.to_i
@@ -230,64 +376,24 @@ module Fusion
230
376
 
231
377
  def keys(v)
232
378
  return v if v.is_a?(ErrorVal)
233
- return error("type_error", "keys", v, "expected an object") unless v.is_a?(Hash)
379
+ return argument_error("keys", v, ["_ ? @Object"]) unless v.is_a?(Hash)
234
380
 
235
381
  v.keys
236
382
  end
237
383
 
238
384
  def values(v)
239
385
  return v if v.is_a?(ErrorVal)
240
- return error("type_error", "values", v, "expected an object") unless v.is_a?(Hash)
386
+ return argument_error("values", v, ["_ ? @Object"]) unless v.is_a?(Hash)
241
387
 
242
388
  v.values
243
389
  end
244
390
 
245
- # Read from an array (integer index, negative counts from the end) or an
246
- # object (string key) — mirroring the `[]` operator (reference §8).
247
- def get(v)
248
- return v if v.is_a?(ErrorVal)
249
- return error("argument_error", "get", v, "expected [_, _]") unless pair?(v)
250
-
251
- container, key = v
252
- if container.is_a?(Array) && key.is_a?(Integer)
253
- i = key.negative? ? container.length + key : key
254
- return container[i] if i >= 0 && i < container.length
255
-
256
- error("access_error", "get", v, "index out of range")
257
- elsif container.is_a?(Hash) && key.is_a?(String)
258
- return container[key] if container.key?(key)
259
-
260
- error("access_error", "get", v, "missing key")
261
- else
262
- error("type_error", "get", v, "bad index type")
263
- end
264
- end
265
-
266
- # Return a new array/object with one entry set. An array index must already
267
- # exist (arrays are not extended); an object key may be new. Addressing
268
- # mirrors `@get` (array by integer index, object by string key).
269
- def set(v)
270
- return v if v.is_a?(ErrorVal)
271
- return error("argument_error", "set", v, "expected [_, _, _]") unless v.is_a?(Array) && v.length == 3
272
-
273
- container, key, value = v
274
- if container.is_a?(Array) && key.is_a?(Integer)
275
- i = key.negative? ? container.length + key : key
276
- return error("access_error", "set", v, "index out of range") unless i >= 0 && i < container.length
277
-
278
- container.dup.tap { |copy| copy[i] = value }
279
- elsif container.is_a?(Hash) && key.is_a?(String)
280
- container.merge(key => value)
281
- else
282
- error("type_error", "set", v, "bad index type")
283
- end
284
- end
285
-
286
391
  def to_object(v)
287
392
  return v if v.is_a?(ErrorVal)
288
- return error("type_error", "toObject", v, "expected an array") unless v.is_a?(Array)
289
- unless v.all? { |entry| pair?(entry) && entry[0].is_a?(String) }
290
- return error("type_error", "toObject", v, "expected [string, value] entries")
393
+ # Each entry must be a [string, _] pair.
394
+ expected = ['_ ? (xs => {"c": xs, "f": ([_ ? @String, _] => true)} | @all)']
395
+ unless v.is_a?(Array) && v.all? { |entry| pair?(entry) && entry[0].is_a?(String) }
396
+ return argument_error("toObject", v, expected)
291
397
  end
292
398
 
293
399
  v.to_h
@@ -341,15 +447,18 @@ module Fusion
341
447
  v.is_a?(Float) && !v.finite?
342
448
  end
343
449
 
344
- # Build a standardized interpreter error (see docs/user/reference.md §6.5).
450
+ # Build a standardized interpreter error carrying a human-readable
451
+ # `message` (see docs/user/reference.md §6.5). Use this for failures that
452
+ # aren't an input-shape mismatch (math, conversion, access). `operation`
453
+ # is the builtin's own `@`-reference (`@#{name}`).
345
454
  def error(kind, name, v, message)
346
- ErrorVal.internal(
347
- kind: kind,
348
- location: "builtin #{name}",
349
- operation: name,
350
- input: v,
351
- message: message
352
- )
455
+ ErrorVal.from_runtime(kind: kind, origin: "builtin", operation: "@#{name}", input: v, message: message)
456
+ end
457
+
458
+ # Build an `argument_error` describing the acceptable inputs as a list of
459
+ # Fusion patterns. The input was unacceptable iff it matches none of them.
460
+ def argument_error(name, v, expected)
461
+ ErrorVal.from_runtime(kind: "argument_error", origin: "builtin", operation: "@#{name}", input: v, expected: expected)
353
462
  end
354
463
  end
355
464
  end
@@ -22,21 +22,28 @@ module Fusion
22
22
  attr_reader :parent
23
23
 
24
24
  def initialize(parent = nil)
25
- @vars = {}
25
+ @vars = {} # pattern bindings, keyed by identifier
26
+ @context = {} # hidden interpreter context, keyed by symbol
26
27
  @parent = parent
27
28
  end
28
29
 
29
- # Unchecked insert, for interpreter-internal names (__dir__, built-ins, …).
30
- def define(name, value)
31
- @vars[name] = value
32
- self
30
+ def child
31
+ Env.new(self)
33
32
  end
34
33
 
35
- # Insert a pattern binding, rejecting a duplicate binder. Only this env's
36
- # own scope is checked: a binder may shadow a name from a parent env, but
37
- # must be unique within one pattern/clause.
38
- def bind(name, value)
39
- raise DuplicateBinding, name if @vars.key?(name)
34
+ # The topmost ancestor — the binding-free root a run is built on. The
35
+ # interpreter loads files relative to it, so they stay isolated.
36
+ def root
37
+ @parent ? @parent.root : self
38
+ end
39
+
40
+ # Pattern bindings:
41
+ # - Shadowing a binding from a parent Env is always allowed.
42
+ # - A duplicate identifier in the same Env is usually an error, but allowed on the REPL.
43
+ def bind(name, value, checked: true)
44
+ if checked && @vars.key?(name)
45
+ raise DuplicateBinding, name
46
+ end
40
47
 
41
48
  @vars[name] = value
42
49
  end
@@ -51,8 +58,31 @@ module Fusion
51
58
  end
52
59
  end
53
60
 
54
- def child
55
- Env.new(self)
61
+ # Hidden interpreter context:
62
+ # - `:dir`: the directory @-references resolve against (a path String).
63
+ # - `:file`: the current file's absolute path, used for error origins (a
64
+ # String; absent for inline/REPL code, which reports as origin
65
+ # "code" with file "<inline>").
66
+ # - `:self`: the current top-level unit's own Thunk, used for recursion via a bare `@`.
67
+ # - `:jail`: the run's jail root confining @-resolution (an absolute path
68
+ # String, or nil for unconfined). Set once on the root and, unlike
69
+ # the others, never overridden by a descendant.
70
+ # - `:call_site`: the innermost user-code `file` a stdlib body borrows for its
71
+ # errors (a String). Set on a stdlib function's clause env in
72
+ # Interpreter#apply; user/inline code derives its own and omits it.
73
+ def set_context(key, value)
74
+ @context[key] = value
75
+ self
76
+ end
77
+
78
+ def context(key)
79
+ if @context.key?(key)
80
+ @context[key]
81
+ elsif @parent
82
+ @parent.context(key)
83
+ else
84
+ :__unbound__
85
+ end
56
86
  end
57
87
  end
58
88
  end