Module: Fusion::Interpreter::Builtins
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
- #install(table, interp) ⇒ Object
-
#join(v) ⇒ Object
[items, separator]: join an array of strings into one string. -
#keys(v) ⇒ Object
--- object key enumeration (Tier 0: patterns can't enumerate unknown keys) ---.
- #math_abs(v) ⇒ Object
- #math_ceil(v) ⇒ Object
- #math_cos(v) ⇒ Object
-
#math_divide(v) ⇒ Object
dividealways yields a float. - #math_exp(v) ⇒ Object
- #math_floor(v) ⇒ Object
-
#math_log(v) ⇒ Object
Natural logarithm; the domain is positive numbers (
Math.lograises on a negative and gives-Infinityat 0). -
#math_pow(v) ⇒ Object
base ** exponent: integer when the base and a non-negative integer exponent are integers, a float otherwise. -
#math_rand(v) ⇒ Object
null→ a float in[0.0, 1.0); a positive integern→ an integer in[0, n). -
#math_round(v) ⇒ Object
round/floor/ceilreturn an integer; a non-finite input is a math_error (their Ruby forms raiseFloatDomainErroron it). -
#math_sign(v) ⇒ Object
-1 / 0 / 1 (via
</>, so NaN → 0, never Ruby'snil). - #math_sin(v) ⇒ Object
-
#math_sqrt(v) ⇒ Object
Square root; the domain is non-negative numbers (a negative would be complex).
- #op_and(v) ⇒ Object
-
#op_compare(v) ⇒ Object
Order two numbers or two strings: -1, 0, or 1 (no deep equality).
-
#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).
- #op_gt(v) ⇒ Object
- #op_gte(v) ⇒ Object
-
#op_invert(v) ⇒ Object
The unary reciprocal 1/x, always a float; 0 is a math_error.
-
#op_lt(v) ⇒ Object
The comparison readers interpret an
@OP.compareresult. - #op_lte(v) ⇒ Object
- #op_modulo(v) ⇒ Object
- #op_negate(v) ⇒ Object
- #op_not(v) ⇒ Object
- #op_or(v) ⇒ Object
- #op_product(v) ⇒ Object
-
#op_quotient(v) ⇒ Object
Integer division and its remainder — integers only (
@dividehandles the float case). -
#op_sum(v) ⇒ Object
--- OP: the sugar-target operators (reached as
@OP.sum,@OP.and, …) ---. - #parse_number(v) ⇒ Object
-
#size(v) ⇒ Object
--- strings and structure bridges ---.
-
#split(v) ⇒ Object
[string, separator]: the inverse of@join. - #to_string(v) ⇒ Object
- #values(v) ⇒ Object
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 |