Module: Fusion::Interpreter::Builtins

Extended by:
Builtins
Included in:
Builtins
Defined in:
lib/fusion/interpreter/builtins.rb

Constant Summary collapse

NUMBER_PAIR =

--- math (reached as @math.round, @math.divide, @math.pi, …) ---

["[_ ? @Number, _ ? @Number]"].freeze
NUMBER_ARRAY =
['_ ? (xs => {"c": xs, "f": @Number} | @all)'].freeze
NUMBER =
["_ ? @Number"].freeze
INTEGER_PAIR =
["[_ ? @Integer, _ ? @Integer]"].freeze
COMPARE_PAIR =
["[_ ? @Number, _ ? @Number]", "[_ ? @String, _ ? @String]"].freeze

Instance Method Summary collapse

Instance Method Details

#install(table, interp) ⇒ Object



10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/fusion/interpreter/builtins.rb', line 10

def install(table, interp)
  @interp = interp
  define = ->(name, fn) { table[name] = NativeFunc.new(name, fn) }

  # Irreducible primitives kept as built-ins. The sugar-target operators
  # (arithmetic/comparison/boolean) live in `@OP` below. Numeric functions
  # (`round`, `divide`, `sin`, …) and constants (`pi`, `e`) live in `@math`.
  define.call("size", method(:size))
  define.call("join", method(:join))
  define.call("split", method(:split))
  define.call("toString", method(:to_string))
  define.call("parseNumber", method(:parse_number))
  define.call("keys", method(:keys))
  define.call("values", method(:values))

  # type predicates: return false on any non-matching value, never an error
  define.call("Integer", method(:integer?))
  define.call("Float", method(:float?))
  define.call("Number", method(:numeric?))
  define.call("String", method(:string?))
  define.call("Boolean", method(:boolean?))
  define.call("Array", method(:array?))
  define.call("Object", method(:object?))
  define.call("Collection", method(:collection?))
  define.call("Null", method(:null?))
  define.call("Function", method(:function?))
  define.call("NonFinite", method(:non_finite?))

  # `OP` bundles the sugar-target operators as a shadowable builtin object,
  # reached as `@OP.sum`, `@OP.and`, … A directory can swap the operators by
  # placing an `OP.fsn` sibling that overrides members (reaching the
  # originals with `@@`); infix sugar and the derived stdlib helpers resolve
  # `@OP` per directory, so they follow the override.
  table["OP"] = {
    "sum"      => NativeFunc.new("OP.sum", method(:op_sum)),
    "product"  => NativeFunc.new("OP.product", method(:op_product)),
    "negate"   => NativeFunc.new("OP.negate", method(:op_negate)),
    "invert"   => NativeFunc.new("OP.invert", method(:op_invert)),
    "quotient" => NativeFunc.new("OP.quotient", method(:op_quotient)),
    "modulo"   => NativeFunc.new("OP.modulo", method(:op_modulo)),
    "equal"    => NativeFunc.new("OP.equal", method(:op_equal)),
    "compare"  => NativeFunc.new("OP.compare", method(:op_compare)),
    "lt"       => NativeFunc.new("OP.lt", method(:op_lt)),
    "gt"       => NativeFunc.new("OP.gt", method(:op_gt)),
    "lte"      => NativeFunc.new("OP.lte", method(:op_lte)),
    "gte"      => NativeFunc.new("OP.gte", method(:op_gte)),
    "and"      => NativeFunc.new("OP.and", method(:op_and)),
    "or"       => NativeFunc.new("OP.or", method(:op_or)),
    "not"      => NativeFunc.new("OP.not", method(:op_not)),
  }

  # `math` bundles numeric functions and constants, reached as `@math.round`,
  # `@math.pi`, … Like `@OP`, it is a shadowable builtin object. `pi`/`e` are
  # plain values; the rest are one-argument functions.
  table["math"] = {
    "round"  => NativeFunc.new("math.round", method(:math_round)),
    "floor"  => NativeFunc.new("math.floor", method(:math_floor)),
    "ceil"   => NativeFunc.new("math.ceil", method(:math_ceil)),
    "divide" => NativeFunc.new("math.divide", method(:math_divide)),
    "sign"   => NativeFunc.new("math.sign", method(:math_sign)),
    "abs"    => NativeFunc.new("math.abs", method(:math_abs)),
    "rand"   => NativeFunc.new("math.rand", method(:math_rand)),
    "sin"    => NativeFunc.new("math.sin", method(:math_sin)),
    "cos"    => NativeFunc.new("math.cos", method(:math_cos)),
    "exp"    => NativeFunc.new("math.exp", method(:math_exp)),
    "log"    => NativeFunc.new("math.log", method(:math_log)),
    "pow"    => NativeFunc.new("math.pow", method(:math_pow)),
    "sqrt"   => NativeFunc.new("math.sqrt", method(:math_sqrt)),
    "pi"     => Math::PI,
    "e"      => Math::E,
  }
end

#join(v) ⇒ Object

[items, separator]: join an array of strings into one string. @concat is the stdlib pair-case built on this.



344
345
346
347
348
349
350
351
352
353
354
355
356
# File 'lib/fusion/interpreter/builtins.rb', line 344

def join(v)
  return v if v.is_a?(ErrorVal)

  expected = ['[_ ? (xs => {"c": xs, "f": @String} | @all), _ ? @String]']
  return argument_error("join", v, expected) unless pair?(v)

  array, separator = v
  unless array.is_a?(Array) && separator.is_a?(String) && array.all? { |item| item.is_a?(String) }
    return argument_error("join", v, expected)
  end

  array.join(separator)
end

#keys(v) ⇒ Object

--- object key enumeration (Tier 0: patterns can't enumerate unknown keys) ---



400
401
402
403
404
405
# File 'lib/fusion/interpreter/builtins.rb', line 400

def keys(v)
  return v if v.is_a?(ErrorVal)
  return argument_error("keys", v, ["_ ? @Object"]) unless v.is_a?(Hash)

  v.keys
end

#math_abs(v) ⇒ Object



136
137
138
139
140
141
# File 'lib/fusion/interpreter/builtins.rb', line 136

def math_abs(v)
  return v if v.is_a?(ErrorVal)
  return argument_error("math.abs", v, NUMBER) unless numeric?(v)

  v.abs
end

#math_ceil(v) ⇒ Object



107
108
109
110
111
112
113
# File 'lib/fusion/interpreter/builtins.rb', line 107

def math_ceil(v)
  return v if v.is_a?(ErrorVal)
  return argument_error("math.ceil", v, NUMBER) unless numeric?(v)
  return error("math_error", "math.ceil", v, "not a finite number") if non_finite?(v)

  v.ceil
end

#math_cos(v) ⇒ Object



160
161
162
163
164
165
# File 'lib/fusion/interpreter/builtins.rb', line 160

def math_cos(v)
  return v if v.is_a?(ErrorVal)
  return argument_error("math.cos", v, NUMBER) unless numeric?(v)

  Math.cos(v)
end

#math_divide(v) ⇒ Object

divide always yields a float. Integer division is @OP.quotient, the remainder @OP.modulo.



117
118
119
120
121
122
123
# File 'lib/fusion/interpreter/builtins.rb', line 117

def math_divide(v)
  return v if v.is_a?(ErrorVal)
  return argument_error("math.divide", v, NUMBER_PAIR) unless pair?(v) && numeric?(v[0]) && numeric?(v[1])
  return error("math_error", "math.divide", v, "division by zero") if v[1] == 0

  v[0].to_f / v[1]
end

#math_exp(v) ⇒ Object



167
168
169
170
171
172
# File 'lib/fusion/interpreter/builtins.rb', line 167

def math_exp(v)
  return v if v.is_a?(ErrorVal)
  return argument_error("math.exp", v, NUMBER) unless numeric?(v)

  Math.exp(v)
end

#math_floor(v) ⇒ Object



99
100
101
102
103
104
105
# File 'lib/fusion/interpreter/builtins.rb', line 99

def math_floor(v)
  return v if v.is_a?(ErrorVal)
  return argument_error("math.floor", v, NUMBER) unless numeric?(v)
  return error("math_error", "math.floor", v, "not a finite number") if non_finite?(v)

  v.floor
end

#math_log(v) ⇒ Object

Natural logarithm; the domain is positive numbers (Math.log raises on a negative and gives -Infinity at 0).



186
187
188
189
190
191
192
# File 'lib/fusion/interpreter/builtins.rb', line 186

def math_log(v)
  return v if v.is_a?(ErrorVal)
  return argument_error("math.log", v, NUMBER) unless numeric?(v)
  return error("math_error", "math.log", v, "log of a non-positive number") if v <= 0

  Math.log(v)
end

#math_pow(v) ⇒ Object

base ** exponent: integer when the base and a non-negative integer exponent are integers, a float otherwise. A negative base with a fractional exponent (a complex result) is a math_error.



197
198
199
200
201
202
203
204
205
206
# File 'lib/fusion/interpreter/builtins.rb', line 197

def math_pow(v)
  return v if v.is_a?(ErrorVal)
  return argument_error("math.pow", v, NUMBER_PAIR) unless pair?(v) && numeric?(v[0]) && numeric?(v[1])

  a, b = v
  result = a.is_a?(Integer) && b.is_a?(Integer) && b >= 0 ? a**b : a.to_f**b
  return error("math_error", "math.pow", v, "not in domain (complex result)") if result.is_a?(Complex)

  result
end

#math_rand(v) ⇒ Object

null → a float in [0.0, 1.0); a positive integer n → an integer in [0, n).



145
146
147
148
149
150
151
# File 'lib/fusion/interpreter/builtins.rb', line 145

def math_rand(v)
  return v if v.is_a?(ErrorVal)
  return rand if v == NULL
  return rand(v) if integer?(v) && v > 0

  argument_error("math.rand", v, ["_ ? @Null", '_ ? (n ? @Integer => [0, n] | @OP.compare | (-1 => true))'])
end

#math_round(v) ⇒ Object

round/floor/ceil return an integer; a non-finite input is a math_error (their Ruby forms raise FloatDomainError on it).



91
92
93
94
95
96
97
# File 'lib/fusion/interpreter/builtins.rb', line 91

def math_round(v)
  return v if v.is_a?(ErrorVal)
  return argument_error("math.round", v, NUMBER) unless numeric?(v)
  return error("math_error", "math.round", v, "not a finite number") if non_finite?(v)

  v.round
end

#math_sign(v) ⇒ Object

-1 / 0 / 1 (via </>, so NaN → 0, never Ruby's nil).



126
127
128
129
130
131
132
133
134
# File 'lib/fusion/interpreter/builtins.rb', line 126

def math_sign(v)
  return v if v.is_a?(ErrorVal)
  return argument_error("math.sign", v, NUMBER) unless numeric?(v)

  if v < 0 then -1
  elsif v > 0 then 1
  else 0
  end
end

#math_sin(v) ⇒ Object



153
154
155
156
157
158
# File 'lib/fusion/interpreter/builtins.rb', line 153

def math_sin(v)
  return v if v.is_a?(ErrorVal)
  return argument_error("math.sin", v, NUMBER) unless numeric?(v)

  Math.sin(v)
end

#math_sqrt(v) ⇒ Object

Square root; the domain is non-negative numbers (a negative would be complex).



176
177
178
179
180
181
182
# File 'lib/fusion/interpreter/builtins.rb', line 176

def math_sqrt(v)
  return v if v.is_a?(ErrorVal)
  return argument_error("math.sqrt", v, NUMBER) unless numeric?(v)
  return error("math_error", "math.sqrt", v, "square root of a negative number") if v < 0

  Math.sqrt(v)
end

#op_and(v) ⇒ Object



313
314
315
316
317
318
# File 'lib/fusion/interpreter/builtins.rb', line 313

def op_and(v)
  return v if v.is_a?(ErrorVal)
  return argument_error("OP.and", v, ["_ ? @Array"]) unless v.is_a?(Array)

  v.all? { |x| @interp.truthy?(x) }
end

#op_compare(v) ⇒ Object

Order two numbers or two strings: -1, 0, or 1 (no deep equality). Built on < rather than <=> so a NaN operand yields 0 (unordered), never Ruby's nil — NaN is a reachable value (Infinity - Infinity).



278
279
280
281
282
283
284
285
286
287
288
289
290
291
# File 'lib/fusion/interpreter/builtins.rb', line 278

def op_compare(v)
  return v if v.is_a?(ErrorVal)
  return argument_error("OP.compare", v, COMPARE_PAIR) unless pair?(v)

  a, b = v
  unless (numeric?(a) && numeric?(b)) || (a.is_a?(String) && b.is_a?(String))
    return argument_error("OP.compare", v, COMPARE_PAIR)
  end

  if a < b then -1
  elsif b < a then 1
  else 0
  end
end

#op_equal(v) ⇒ Object

Deep, exact equality across the whole array: true iff every element equals the first (so 0 and 1 elements are vacuously equal). Any types.



266
267
268
269
270
271
# File 'lib/fusion/interpreter/builtins.rb', line 266

def op_equal(v)
  return v if v.is_a?(ErrorVal)
  return argument_error("OP.equal", v, ["_ ? @Array"]) unless v.is_a?(Array)

  v.all? { |x| @interp.deep_equal?(v[0], x) }
end

#op_gt(v) ⇒ Object



301
302
303
# File 'lib/fusion/interpreter/builtins.rb', line 301

def op_gt(v)
  read_compare_result("OP.gt", v, [1])
end

#op_gte(v) ⇒ Object



309
310
311
# File 'lib/fusion/interpreter/builtins.rb', line 309

def op_gte(v)
  read_compare_result("OP.gte", v, [0, 1])
end

#op_invert(v) ⇒ Object

The unary reciprocal 1/x, always a float; 0 is a math_error.



236
237
238
239
240
241
242
# File 'lib/fusion/interpreter/builtins.rb', line 236

def op_invert(v)
  return v if v.is_a?(ErrorVal)
  return argument_error("OP.invert", v, ["_ ? @Number"]) unless numeric?(v)
  return error("math_error", "OP.invert", v, "division by zero") if v == 0

  1.0 / v
end

#op_lt(v) ⇒ Object

The comparison readers interpret an @OP.compare result. The infix sugar a < b desugars to [a, b] | @OP.compare | @OP.lt (likewise <= / > / >=), so an @OP override reskins the ordering and its reading together.



297
298
299
# File 'lib/fusion/interpreter/builtins.rb', line 297

def op_lt(v)
  read_compare_result("OP.lt", v, [-1])
end

#op_lte(v) ⇒ Object



305
306
307
# File 'lib/fusion/interpreter/builtins.rb', line 305

def op_lte(v)
  read_compare_result("OP.lte", v, [-1, 0])
end

#op_modulo(v) ⇒ Object



256
257
258
259
260
261
262
# File 'lib/fusion/interpreter/builtins.rb', line 256

def op_modulo(v)
  return v if v.is_a?(ErrorVal)
  return argument_error("OP.modulo", v, INTEGER_PAIR) unless pair?(v) && integer?(v[0]) && integer?(v[1])
  return error("math_error", "OP.modulo", v, "modulo by zero") if v[1] == 0

  v[0] % v[1]
end

#op_negate(v) ⇒ Object



228
229
230
231
232
233
# File 'lib/fusion/interpreter/builtins.rb', line 228

def op_negate(v)
  return v if v.is_a?(ErrorVal)
  return argument_error("OP.negate", v, ["_ ? @Number"]) unless numeric?(v)

  -v
end

#op_not(v) ⇒ Object



327
328
329
330
331
# File 'lib/fusion/interpreter/builtins.rb', line 327

def op_not(v)
  return v if v.is_a?(ErrorVal)

  !@interp.truthy?(v)
end

#op_or(v) ⇒ Object



320
321
322
323
324
325
# File 'lib/fusion/interpreter/builtins.rb', line 320

def op_or(v)
  return v if v.is_a?(ErrorVal)
  return argument_error("OP.or", v, ["_ ? @Array"]) unless v.is_a?(Array)

  v.any? { |x| @interp.truthy?(x) }
end

#op_product(v) ⇒ Object



221
222
223
224
225
226
# File 'lib/fusion/interpreter/builtins.rb', line 221

def op_product(v)
  return v if v.is_a?(ErrorVal)
  return argument_error("OP.product", v, NUMBER_ARRAY) unless v.is_a?(Array) && v.all? { |x| numeric?(x) }

  v.reduce(1, :*)
end

#op_quotient(v) ⇒ Object

Integer division and its remainder — integers only (@divide handles the float case). Ruby's / and % agree in sign, so q*b + r == a holds.



248
249
250
251
252
253
254
# File 'lib/fusion/interpreter/builtins.rb', line 248

def op_quotient(v)
  return v if v.is_a?(ErrorVal)
  return argument_error("OP.quotient", v, INTEGER_PAIR) unless pair?(v) && integer?(v[0]) && integer?(v[1])
  return error("math_error", "OP.quotient", v, "division by zero") if v[1] == 0

  v[0] / v[1]
end

#op_sum(v) ⇒ Object

--- OP: the sugar-target operators (reached as @OP.sum, @OP.and, …) ---

The arithmetic, boolean, and equality members take an array of ANY length; the unary ones take a single value; compare returns -1 / 0 / 1, which lt / gt / lte / gte then read into a boolean.



214
215
216
217
218
219
# File 'lib/fusion/interpreter/builtins.rb', line 214

def op_sum(v)
  return v if v.is_a?(ErrorVal)
  return argument_error("OP.sum", v, NUMBER_ARRAY) unless v.is_a?(Array) && v.all? { |x| numeric?(x) }

  v.sum(0)
end

#parse_number(v) ⇒ Object



387
388
389
390
391
392
393
394
395
396
# File 'lib/fusion/interpreter/builtins.rb', line 387

def parse_number(v)
  return v if v.is_a?(ErrorVal)
  return argument_error("parseNumber", v, ["_ ? @String"]) unless v.is_a?(String)

  case v
  when /\A-?\d+\z/ then v.to_i
  when /\A-?\d+(\.\d+)?([eE][+-]?\d+)?\z/ then v.to_f
  else error("conversion_error", "parseNumber", v, "not a numeric string")
  end
end

#size(v) ⇒ Object

--- strings and structure bridges ---



335
336
337
338
339
340
# File 'lib/fusion/interpreter/builtins.rb', line 335

def size(v)
  return v if v.is_a?(ErrorVal)
  return argument_error("size", v, ["_ ? @String", "_ ? @Collection"]) unless v.is_a?(String) || v.is_a?(Array) || v.is_a?(Hash)

  v.length
end

#split(v) ⇒ Object

[string, separator]: the inverse of @join. Splits on the LITERAL separator (Ruby's " " whitespace special-case does not apply) and keeps empty fields; an empty separator splits into characters. @chars is the stdlib single-string case built on this.



362
363
364
365
366
367
368
369
370
371
372
# File 'lib/fusion/interpreter/builtins.rb', line 362

def split(v)
  return v if v.is_a?(ErrorVal)

  expected = ["[_ ? @String, _ ? @String]"]
  return argument_error("split", v, expected) unless pair?(v) && v[0].is_a?(String) && v[1].is_a?(String)

  string, separator = v
  return string.chars if separator.empty?

  string.split(Regexp.new(Regexp.escape(separator)), -1)
end

#to_string(v) ⇒ Object



374
375
376
377
378
379
380
381
382
383
384
385
# File 'lib/fusion/interpreter/builtins.rb', line 374

def to_string(v)
  return v if v.is_a?(ErrorVal)

  case v
  when String then v
  when Integer, Float then v.to_s
  when true then "true"
  when false then "false"
  when NULL then "null"
  else error("conversion_error", "toString", v, "cannot stringify this value type")
  end
end

#values(v) ⇒ Object



407
408
409
410
411
412
# File 'lib/fusion/interpreter/builtins.rb', line 407

def values(v)
  return v if v.is_a?(ErrorVal)
  return argument_error("values", v, ["_ ? @Object"]) unless v.is_a?(Hash)

  v.values
end