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
# 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; their friendly
  # derived forms (`@lt`, `@gt`, `@truthy`, …) are stdlib files that build on
  # `@OP.*`, so they follow a per-directory `@OP` override. 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))
  define.call("toObject", method(:to_object))

  # 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("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)),
    "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.



323
324
325
326
327
328
329
330
331
332
333
334
# File 'lib/fusion/interpreter/builtins.rb', line 323

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) ---



377
378
379
380
381
382
# File 'lib/fusion/interpreter/builtins.rb', line 377

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



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

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



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

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



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

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.



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

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



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

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



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

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).



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

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.



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

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.negative? ? 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).



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

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

  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).



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

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).



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

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



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

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).



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

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



292
293
294
295
296
297
# File 'lib/fusion/interpreter/builtins.rb', line 292

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).



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

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.



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

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_invert(v) ⇒ Object

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



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

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_modulo(v) ⇒ Object



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

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



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

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



306
307
308
309
310
# File 'lib/fusion/interpreter/builtins.rb', line 306

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

  !@interp.truthy?(v)
end

#op_or(v) ⇒ Object



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

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



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

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.



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

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. The friendly derived forms (@lt, @gt, @truthy, …) are stdlib files built on these.



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

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



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

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 ---



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

def size(v)
  return v if v.is_a?(ErrorVal)
  return argument_error("size", v, ["_ ? @String", "_ ? @Array", "_ ? @Object"]) 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.



340
341
342
343
344
345
346
347
348
349
# File 'lib/fusion/interpreter/builtins.rb', line 340

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_object(v) ⇒ Object



391
392
393
394
395
396
397
398
399
400
# File 'lib/fusion/interpreter/builtins.rb', line 391

def to_object(v)
  return v if v.is_a?(ErrorVal)
  # Each entry must be a [string, _] pair.
  expected = ['_ ? (xs => {"c": xs, "f": ([_ ? @String, _] => true)} | @all)']
  unless v.is_a?(Array) && v.all? { |entry| pair?(entry) && entry[0].is_a?(String) }
    return argument_error("toObject", v, expected)
  end

  v.to_h
end

#to_string(v) ⇒ Object



351
352
353
354
355
356
357
358
359
360
361
362
# File 'lib/fusion/interpreter/builtins.rb', line 351

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



384
385
386
387
388
389
# File 'lib/fusion/interpreter/builtins.rb', line 384

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

  v.values
end