aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authors-ol <s+removethis@s-ol.nu>2022-02-02 16:37:58 +0000
committers-ol <s+removethis@s-ol.nu>2025-03-02 14:24:49 +0000
commit8baadfc47832bbc3820e7d55adbbe5ddfaffedf3 (patch)
treee97b316c29ed3fdce52a061fd8c205edc2613b9a
parentallow ^ and % in symbols (diff)
downloadalive-8baadfc47832bbc3820e7d55adbbe5ddfaffedf3.tar.gz
alive-8baadfc47832bbc3820e7d55adbbe5ddfaffedf3.zip
lib: rename math → math-simple, add vectorized math
-rw-r--r--alv-lib/math-simple.moon215
-rw-r--r--alv-lib/math.moon225
-rw-r--r--alv/type.moon13
-rw-r--r--docs/gen/layout.moon2
-rw-r--r--docs/reference/04-2_pure-operators.md8
-rw-r--r--spec/lang/math_spec.moon267
6 files changed, 681 insertions, 49 deletions
diff --git a/alv-lib/math-simple.moon b/alv-lib/math-simple.moon
new file mode 100644
index 0000000..b79f065
--- /dev/null
+++ b/alv-lib/math-simple.moon
@@ -0,0 +1,215 @@
+import PureOp, Input, Constant, T, any from require 'alv.base'
+unpack or= table.unpack
+
+num = any.num
+
+class ReduceOp extends PureOp
+ pattern: num\rep 2, nil
+ type: T.num
+
+ tick: =>
+ args = @unwrap_all!
+ accum = args[1]
+ for val in *args[2,]
+ accum = @.fn accum, val
+ @out\set accum
+
+func_op = (func, pattern) ->
+ class extends PureOp
+ pattern: pattern
+ type: T.num
+
+ tick: => @out\set func unpack @unwrap_all!
+
+func_def = (name, args, func, summary, pattern) ->
+ Constant.meta
+ meta:
+ :name
+ :summary
+ examples: { "(#{name} #{args})" }
+ value: func_op func, pattern or num\rep 1, 1
+
+evenodd_op = (remainder) ->
+ class extends PureOp
+ pattern: num + -num
+ type: T.bool
+
+ tick: =>
+ { val, div } = @unwrap_all!
+ @out\set (val % div) == remainder
+
+add = Constant.meta
+ meta:
+ name: 'add'
+ summary: "Add values."
+ examples: { '(+ a b [c…])', '(add a b [c…])' }
+ description: "Sum all arguments."
+ value: class extends ReduceOp
+ fn: (a, b) -> a + b
+
+sub = Constant.meta
+ meta:
+ name: 'sub'
+ summary: "Subtract values."
+ examples: { '(- a b [c…])', '(- b)', '(sub a b [c…])' }
+ description: "Subtract all other arguments from `a`.
+
+If only `b` is given, `a` is assumed to be `0`."
+ value: class extends ReduceOp
+ setup: (...) =>
+ super ...
+ if #@inputs == 1
+ table.insert @inputs, 1, Input.cold Constant.num 0
+
+ pattern: num*0
+ fn: (a, b) -> a - b
+
+mul = Constant.meta
+ meta:
+ name: 'mul'
+ summary: "Multiply values."
+ examples: { '(* a b [c…])', '(mul a b [c…])' }
+ value: class extends ReduceOp
+ fn: (a, b) -> a * b
+
+div = Constant.meta
+ meta:
+ name: 'div'
+ summary: "Divide values."
+ examples: { '(/ a b [c…])', '(div a b [c…])' }
+ description: "Divide `a` by all other arguments."
+ value: class extends ReduceOp
+ fn: (a, b) -> a / b
+
+pow = Constant.meta
+ meta:
+ name: 'pow'
+ summary: "Raise to a power."
+ examples: { '(^ base exp)', '(pow base exp' }
+ description: "Raise `base` to the power `exp`."
+ value: class extends ReduceOp
+ fn: (a, b) -> a ^ b
+
+mod = Constant.meta
+ meta:
+ name: 'mod'
+ summary: 'Modulo operator.'
+ examples: { '(% num div)', '(mod num div)' }
+ description: "Calculate remainder of division by `div`."
+ value: func_op ((a, b) -> a % b), num + num
+
+even = Constant.meta
+ meta:
+ name: 'even'
+ summary: 'Check whether val is even.'
+ examples: { '(even val [div])' }
+ description: "`true` if dividing `val` by `div` has remainder zero.
+`div` defaults to 2."
+ value: evenodd_op 0
+
+odd = Constant.meta
+ meta:
+ name: 'odd'
+ summary: 'Check whether val is odd.'
+ examples: { '(odd val [div])' }
+ description: "`true` if dividing `val` by `div` has remainder one.
+`div` defaults to 2."
+ value: evenodd_op 1
+
+mix = Constant.meta
+ meta:
+ name: 'mix'
+ summary: 'Linearly interpolate.'
+ examples: { '(mix a b i)' }
+ description: "Interpolate between `a` and `b` using `i` in range 0-1."
+ value: func_op ((a, b, i) -> i*b + (1-i)*a), num + num + num
+
+min = Constant.meta
+ meta:
+ name: 'min'
+ summary: "Find the minimum."
+ examples: { '(min a b [c…])' }
+ description: "Return the lowest of arguments."
+ value: func_op math.min, num*0
+
+max = Constant.meta
+ meta:
+ name: 'max'
+ summary: "Find the maximum."
+ examples: { '(max a b [c…])' }
+ description: "Return the highest of arguments."
+ value: func_op math.max, num*0
+
+clamp = Constant.meta
+ meta:
+ name: 'clamp'
+ summary: "Clamp a value to a range."
+ examples: { '(clamp min max val)' }
+ description: "Returns `min` if `val < min`; `max` if `val > max`; and `val` otherwise."
+ value: func_op ((min, max, val) -> math.min max, math.max min, val), num*3
+
+inc = func_def 'inc', 'i', ((i) -> i + 1), "Increment by 1."
+dec = func_def 'dec', 'i', ((i) -> i - 1), "Decrement by 1."
+
+cos = func_def 'cos', 'alpha', math.cos, "Cosine function (radians)."
+sin = func_def 'sin', 'alpha', math.sin, "Sine function (radians)."
+tan = func_def 'tan', 'alpha', math.tan, "Tangent function (radians)."
+acos = func_def 'acos', 'cos', math.acos, "Inverse cosine function (radians)."
+asin = func_def 'asin', 'sin', math.asin, "Inverse sine function (radians)."
+atan = func_def 'atan', 'tan', math.atan, "Inverse tangent function (radians)."
+atan2 = func_def 'atan2', 'y x', math.atan2, "Inverse tangent function (two argument version).", num + num
+cosh = func_def 'cosh', 'alpha', math.cosh, "Hyperbolic cosine function (radians)."
+sinh = func_def 'sinh', 'alpha', math.sinh, "Hyperbolic sine function (radians)."
+tanh = func_def 'tanh', 'alpha', math.tanh, "Hyperbolic tangent function (radians)."
+
+floor = func_def 'floor', 'val', math.floor, "Round towards negative infinity."
+ceil = func_def 'ceil', 'val', math.ceil, "Round towards positive infinity."
+abs = func_def 'abs', 'val', math.abs, "Get the absolute value."
+
+exp = func_def 'exp', 'exp', math.floor, "*e* number raised to a power."
+log = func_def 'log', 'val [base]', math.log, "Logarithm with optional base.", num + -num
+log10 = func_def 'log10', 'val', math.log10, "Logarithm with base 10."
+sqrt = func_def 'sqrt', 'val', math.sqrt, "Square root function."
+
+Constant.meta
+ meta:
+ name: 'math-simple'
+ summary: "Mathematical functions for scalars."
+ description: "
+All operators are PureOps.
+They accept only scalar numbers.
+For vectorized operators and matrix multiplication, use [math/][]."
+
+ value:
+ :add, '+': add
+ :sub, '-': sub
+ :mul, '*': mul
+ :div, '/': div
+ :pow, '^': pow
+ :mod, '%': mod
+
+ :even, :odd
+
+ :mix
+ :min, :max, :clamp
+
+ :inc, :dec
+
+ pi: Constant.meta
+ value: math.pi
+ meta: summary: 'The pi constant.'
+
+ tau: Constant.meta
+ value: math.pi*2
+ meta: summary: 'The tau constant.'
+
+ huge: Constant.meta
+ value: math.huge
+ meta: summary: 'Positive infinity constant.'
+
+ :sin, :cos, :tan
+ :asin, :acos, :atan, :atan2
+ :sinh, :cosh, :tanh
+
+ :floor, :ceil, :abs
+ :exp, :log, :log10, :sqrt
diff --git a/alv-lib/math.moon b/alv-lib/math.moon
index 7f76ce9..4baf4be 100644
--- a/alv-lib/math.moon
+++ b/alv-lib/math.moon
@@ -1,25 +1,133 @@
-import PureOp, Constant, T, sig, evt from require 'alv.base'
+import PureOp, RTNode, Constant, Error, T, Array, any from require 'alv.base'
unpack or= table.unpack
-num = sig.num / evt.num
+--- (recursively) wrap/repeat a scalar value to match a (nested) array type.
+--
+-- For example `expand_to (Array 3, Array 4, T.num), 2` will return
+-- `[[2 2 2 2] [2 2 2 2] [2 2 2 2]]`.
+expand_to = (want, have, val) ->
+ return val if want == have
+
+ return for key, inner in want\iter_keys!
+ expand_to inner, have, val
+
+--- apply fn componentwise
+deep_apply = (fn, type, args) ->
+ return fn args unless type.iter_keys
+
+ return for key, inner in type\iter_keys!
+ deep_apply fn, inner, [arg[key] for arg in *args]
+
+is_vec = (type) -> type.__class == Array and type.type == T.num
+is_mat = (type) -> type.__class == Array and type.type.__class == Array
+
+--- return a function that runs `expand_to` on all arguments
+--
+-- @treturn function
+deep_apply_fn = (_func, types) ->
+ func = (args) -> _func unpack args
+
+ result_type = nil
+ for type in *types
+ continue if type == T.num
+
+ result_type or= type
+ assert type == result_type
+
+ -- all scalars, don't expand
+ if not result_type
+ return T.num, func
+
+ -- at least one non-scalar
+ result_type, (args) ->
+ -- expand all arguments
+ expanded = for i, arg in ipairs args
+ if types[i] != result_type
+ expand_to result_type, types[i], arg
+ else
+ arg
+
+ deep_apply func, result_type, expanded
+
+
+multiply_mat = (ltype, rtype, lval, rval) ->
+ return for i = 1, ltype.size
+ for j = 1, rtype.type.size
+ accum = 0
+ for k = 1, rtype.size
+ accum += lval[i][k] * rval[k][j]
+ accum
+
+apply_fn_linalg = (types) ->
+ result = types[1]
+ fns = {}
+
+ for i=2, #types
+ local nextres
+ ltype = result
+ rtype = types[i]
+
+ lvec, rvec = (is_vec ltype), (is_vec rtype)
+
+ fn = if (lvec and rvec) or ltype == T.num or rtype == T.num
+ -- componentwise mult
+ nextres = if lvec or (is_mat ltype) then ltype else rtype
+ (lval, rval) ->
+ lval = expand_to nextres, ltype, lval
+ rval = expand_to nextres, rtype, rval
+ deep_apply ((args) -> args[1] * args[2]), nextres, { lval, rval }
+ else
+ -- matrix/vector multiplication or matrix/matrix multiplication
+ if lvec
+ nextres = ltype
+ ltype = Array 1, ltype
+ else if rvec
+ nextres = rtype
+ rtype = Array rtype.size, Array 1, rtype.type if rvec
+ else
+ nextres = Array ltype.size, Array rtype.type.size, T.num
+
+ assert ltype.type.size == rtype.size
+
+ (lval, rval) ->
+ lval = {lval} if lvec
+ rval = [{ v } for v in *rval] if rvec
+ res = multiply_mat ltype, rtype, lval, rval
+ if rvec
+ [v[1] for v in *res]
+ else if lvec
+ res[1]
+ else
+ res
+
+ result = nextres
+ table.insert fns, fn
+
+ result, (args) ->
+ accum = args[1]
+ for i, fn in ipairs fns
+ accum = fn accum, args[i + 1]
+ accum
-class ReduceOp extends PureOp
- pattern: num\rep 2, nil
- type: T.num
+num = any.num / any!!
- tick: =>
- args = @unwrap_all!
- accum = args[1]
- for val in *args[2,]
- accum = @.fn accum, val
- @out\set accum
+reduce_fn = (fn) ->
+ (accum, ...) ->
+ for i=1, select '#', ...
+ accum = fn accum, select i, ...
+ accum
func_op = (func, pattern) ->
class extends PureOp
pattern: pattern
- type: T.num
+ type: (inputs) =>
+ types = [input\type! for input in *inputs]
+ result, @func = deep_apply_fn func, types
+ assert (is_vec result) or (is_mat result) or (result == T.num),
+ Error 'argument', "expected matrices, vectors or numbers"
+ result
- tick: => @out\set func unpack @unwrap_all!
+ tick: => @out\set @.func @unwrap_all!
func_def = (name, args, func, summary, pattern) ->
Constant.meta
@@ -31,7 +139,7 @@ func_def = (name, args, func, summary, pattern) ->
evenodd_op = (remainder) ->
class extends PureOp
- pattern: num + -num
+ pattern: T.num + -T.num
type: T.bool
tick: =>
@@ -44,8 +152,7 @@ add = Constant.meta
summary: "Add values."
examples: { '(+ a b [c…])', '(add a b [c…])' }
description: "Sum all arguments."
- value: class extends ReduceOp
- fn: (a, b) -> a + b
+ value: func_op (reduce_fn (a, b) -> a + b), num\rep 2, nil
sub = Constant.meta
meta:
@@ -53,16 +160,38 @@ sub = Constant.meta
summary: "Subtract values."
examples: { '(- a b [c…])', '(sub a b [c…])' }
description: "Subtract all other arguments from `a`."
- value: class extends ReduceOp
- fn: (a, b) -> a - b
+ value: class extends func_op (reduce_fn (a, b) -> a - b), num*0
+ setup: (inputs, ...) =>
+ if #inputs == 1
+ table.insert inputs, 1, RTNode result: Constant.num 0
+ super inputs, ...
mul = Constant.meta
meta:
name: 'mul'
- summary: "Multiply values."
+ summary: "Multiply scalars, vectors and matrices."
examples: { '(* a b [c…])', '(mul a b [c…])' }
- value: class extends ReduceOp
- fn: (a, b) -> a * b
+ description: "Multiplies all arguments.
+
+For every pair of arguments, from left to right:
+
+- If either argument is a scalar, or both are vectors, multiply componentwise.
+- If either argument is a matrix and the other is a vector, apply the matrix transformation.
+ - `(* num[L][M] num[M]) → num[L]` (forward transform)
+ - `(* num[M] num[M][N]) → num[N]` (reverse transform)
+- If both arguments are matrices, multiply them using matrix multiplication.
+ - `(* num[L][M] num[M][N]) → num[M][N]`"
+ value: class extends PureOp
+ pattern: any!\rep 2, nil
+
+ type: (inputs) =>
+ types = [input\type! for input in *inputs]
+ result, @func = apply_fn_linalg types
+ assert (is_vec result) or (is_mat result) or (result == T.num),
+ Error 'argument', "expected matrices, vectors or numbers"
+ result
+
+ tick: => @out\set @.func @unwrap_all!
div = Constant.meta
meta:
@@ -70,8 +199,8 @@ div = Constant.meta
summary: "Divide values."
examples: { '(/ a b [c…])', '(div a b [c…])' }
description: "Divide `a` by all other arguments."
- value: class extends ReduceOp
- fn: (a, b) -> a / b
+ value: func_op (reduce_fn (a, b) -> a / b), num\rep 2, nil
+ -- @TODO: block matrix-matrix division
pow = Constant.meta
meta:
@@ -79,8 +208,7 @@ pow = Constant.meta
summary: "Raise to a power."
examples: { '(^ base exp)', '(pow base exp' }
description: "Raise `base` to the power `exp`."
- value: class extends ReduceOp
- fn: (a, b) -> a ^ b
+ value: func_op (reduce_fn (a, b) -> a ^ b), num\rep 2, nil
mod = Constant.meta
meta:
@@ -90,24 +218,6 @@ mod = Constant.meta
description: "Calculate remainder of division by `div`."
value: func_op ((a, b) -> a % b), num + num
-even = Constant.meta
- meta:
- name: 'even'
- summary: 'Check whether val is even.'
- examples: { '(even val [div])' }
- description: "`true` if dividing `val` by `div` has remainder zero.
-`div` defaults to 2."
- value: evenodd_op 0
-
-odd = Constant.meta
- meta:
- name: 'odd'
- summary: 'Check whether val is odd.'
- examples: { '(odd val [div])' }
- description: "`true` if dividing `val` by `div` has remainder one.
-`div` defaults to 2."
- value: evenodd_op 1
-
mix = Constant.meta
meta:
name: 'mix'
@@ -132,6 +242,14 @@ max = Constant.meta
description: "Return the highest of arguments."
value: func_op math.max, num*0
+clamp = Constant.meta
+ meta:
+ name: 'clamp'
+ summary: "Clamp a value to a range."
+ examples: { '(clamp min max val)' }
+ description: "Returns `min` if `val < min`; `max` if `val > max`; and `val` otherwise."
+ value: func_op ((min, max, val) -> math.min max, math.max min, val), num*3
+
inc = func_def 'inc', 'i', ((i) -> i + 1), "Increment by 1."
dec = func_def 'dec', 'i', ((i) -> i - 1), "Decrement by 1."
@@ -159,6 +277,25 @@ Constant.meta
meta:
name: 'math'
summary: "Mathematical functions."
+ description: "
+This module is exactly like [math-simple/][], except that the operators
+also work componentwise with vectors (`num[X]`) and matrices (`num[X][Y]`).
+All operators are PureOps.
+
+ (+ 1 2 3) #(<num= 6>)
+ (+ (array 1 2) (array 3 4)) #(<num[2]= [4 6]>)
+
+The arguments for an operator generally have to be of the same type.
+However it is also okay to pass in scalar numbers together with a different type.
+The scalars will be repeated as necessary to fit the shape of other arguments:
+
+ (* (array (array 1 2) (array 3 4))
+ 2)
+ #(<num[2][2]= [[2 4] [6 8]]>)
+
+The [mul][:math/mul:] (`*`) operator is the only exception to this,
+as it handles matrix-matrix and matrix-vector multiplication according to linear algebra.
+"
value:
:add, '+': add
@@ -171,7 +308,7 @@ Constant.meta
:even, :odd
:mix
- :min, :max
+ :min, :max, :clamp
:inc, :dec
diff --git a/alv/type.moon b/alv/type.moon
index 4f38e02..765cab2 100644
--- a/alv/type.moon
+++ b/alv/type.moon
@@ -119,6 +119,12 @@ class Struct extends Type
get: (key) =>
assert @types[key], Error 'index', "#{@} has no '#{key}' key"
+ --- iterate over contained keys and types
+ -- each iteration, returns a key and associated type
+ -- @treturn string key
+ -- @treturn Type associated type
+ iter_keys: => next, @types, nil
+
--- create a new struct type with a subset of keys.
project: (keys) =>
types = {}
@@ -161,6 +167,13 @@ class Array extends Type
assert key >= 0 and key < @size, Error 'index', "index '#{key}' out of range!"
@type
+ array_next = (i) => if i < @size then i + 1, @type
+ --- iterate over contained types
+ -- each iteration, returns a key and associated type
+ -- @treturn number key
+ -- @treturn Type associated type
+ iter_keys: => array_next, @, 0
+
__eq: (other) => other.__class == Array and @size == other.size and @type == other.type
__tostring: => "#{@type}[#{@size}]"
diff --git a/docs/gen/layout.moon b/docs/gen/layout.moon
index 7d3c4e1..4c75749 100644
--- a/docs/gen/layout.moon
+++ b/docs/gen/layout.moon
@@ -86,7 +86,7 @@ r = (text, ref) ->
-- substitute markdown-style reference links
autoref = (str) ->
str = str\gsub '%[([^%]]-)%]%[%]', r
- str = str\gsub '%[([^%]]-)%]%[:(.-):%]', r
+ str = str\gsub '%[([^%]]-)%]%[:([^%]]-):%]', r
str
subnav = do
diff --git a/docs/reference/04-2_pure-operators.md b/docs/reference/04-2_pure-operators.md
index 4d07920..b3bdf42 100644
--- a/docs/reference/04-2_pure-operators.md
+++ b/docs/reference/04-2_pure-operators.md
@@ -25,18 +25,18 @@ As an example, let's consider [math/+][]:
(trace (+ 1 2 (every 1 3)))
(trace (+ 1 (lfo 2) (every 1 3)))
```output
-(+ num= num= num=) -> num=
+(+ num= num= num=) → num=
trace (+ 1 2 3): <num= 6>
-(+ num= num= num~) -> num~
+(+ num= num= num~) → num~
trace (+ 1 2 (lfo 2)): <num~ 4.0>
trace (+ 1 2 (lfo 2)): <num~ 3.9882585630406>
-(+ num= num= num!) -> num!
+(+ num= num= num!) → num!
trace (+ 1 2 (every 2 3)): <num! 6>
trace (+ 1 2 (every 2 3)): <num! 6>
-(+ num= num~ num!) -> num!
+(+ num= num~ num!) → num!
trace (+ 1 (lfo 2) (every 2 3)): <num! 4.9950529446967>
trace (+ 1 (lfo 2) (every 2 3)): <num! 4.9950529446967>
```
diff --git a/spec/lang/math_spec.moon b/spec/lang/math_spec.moon
new file mode 100644
index 0000000..23a353b
--- /dev/null
+++ b/spec/lang/math_spec.moon
@@ -0,0 +1,267 @@
+import TestPilot from require 'spec.test_setup'
+import T, Array, Constant from require 'alv'
+
+describe_both = (fn) ->
+ describe "math", ->
+ test = TestPilot '', '(import* testing math)\n'
+ fn!
+
+ describe "math-simple", ->
+ test = TestPilot '', '(import* testing math-simple)\n'
+ fn!
+
+describe_both ->
+ describe "add, sub, mul, div, pow, mod", ->
+ it "are aliased as +-*/^%", ->
+ COPILOT\eval_once '
+ (expect= + add)
+ (expect= - sub)
+ (expect= * mul)
+ (expect= / div)
+ (expect= ^ pow)
+ (expect= % mod)
+ '
+
+ it "are sane", ->
+ COPILOT\eval_once '
+ (expect= 2 (+ 1 1))
+ (expect= 6 (+ 2 3 0 1))
+
+ (expect= -5 (- 2 7))
+ (expect= 0 (- 10 4 3 2 1 0))
+ (expect= -10 (- 10))
+
+ (expect= 1 (* 1 1))
+ (expect= -2 (* -1 2))
+ (expect= 14 (* 4 0.5 7))
+
+ (expect= 1 (/ 4 4))
+ (expect= -2 (/ -10 5))
+ (expect= 3 (/ -30 -10))
+
+ (expect= 1024 (^ 2 10))
+ (expect= 0.25 (^ 4 -1))
+ (expect= 1 (^ 999 0))
+
+ (expect= 4 (% 10 6))
+ (expect= 3 (% -2 5))
+ (expect= -2 (% -2 -5))
+ '
+
+ describe "trigonometric functions and constants", ->
+ COPILOT\eval_once '
+ (expect= tau (* pi 2))
+
+ #(darn you fp accuracy!
+ (expect= 0.5 (asin (sin 0.5)))
+ (expect= 0.5 (acos (cos 0.5)))
+ ...)
+ '
+
+ it "min, max, clamp, huge", ->
+ COPILOT\eval_once '
+ (expect= 0 (min 0 1 2))
+ (expect= 2 (max 0 1 2))
+
+ (expect= -2 (clamp -2 3.5 -4))
+ (expect= -1 (clamp -2 3.5 -1))
+ (expect= 0 (clamp -2 3.5 0))
+ (expect= 1 (clamp -2 3.5 1))
+ (expect= 3.5 (clamp -2 3.5 4))
+
+ (expect= (- huge) (min 0 1 -123456789 (- huge)))
+ (expect= huge (max 0 1 123456789 huge))
+ '
+
+ it "inc, dec", ->
+ COPILOT\eval_once '
+ (expect= 1 (inc 0))
+ (expect= -1 (dec 0))
+ (expect= 3 (inc (inc 1)))
+ (expect= -1 (dec (dec 1)))
+ (expect= 0 (inc (dec 0)) (dec (inc 0)))
+ '
+
+describe "math", ->
+ test = TestPilot '', '(import* testing math)\n'
+
+ describe "add, sub, mul, div, pow, mod", ->
+ it "handle scalar/vector", ->
+ COPILOT\eval_once '
+ (expect= (array 3 4 5)
+ (+ 1 (array 1 2 3) 1))
+
+ (expect= (array 0 1 2)
+ (- (array 1 2 3) 1))
+
+ (expect= (array 3 6 9)
+ (* 3 (array 1 2 3)))
+ (expect= (array 3 6 9)
+ (* (array 1 2 3) 3))
+
+ (expect= (array 12 9 4)
+ (/ 36 (array 3 4 9)))
+
+ (expect= (array 9 16 25)
+ (^ (array 3 4 5) 2))
+ (expect= (array 1 2 4 8)
+ (^ 2 (array 0 1 2 3)))
+
+ (expect= (array 3 0 1)
+ (% (array 3 4 5) 4))
+ '
+
+ it "handle vector/vector and matrix/matrix", ->
+ COPILOT\eval_once '
+ (expect= (array 5 7 9)
+ (+ (array 1 2 3)
+ (array 4 5 6)))
+
+ (expect= (array (array 11 12)
+ (array 13 14))
+ (+
+ (array (array 1 2)
+ (array 3 4))
+ 5
+ 5))
+
+ (expect= (array 2 0 -2)
+ (- (array 3 2 1)
+ (array 1 2 3)))
+
+ (expect= (array 1 -2 -3)
+ (- (array -1 2 3)))
+ '
+
+ err = assert.has.error ->
+ COPILOT\eval_once '
+ (+ (array 1 2 3)
+ (array 1 2))'
+
+ err = assert.has.error ->
+ COPILOT\eval_once '
+ (+ (array (array 1 2) (array 1 2))
+ (array 1 2))'
+
+ err = assert.has.error ->
+ COPILOT\eval_once '
+ (- (array 1 2 3)
+ (array 1 2))'
+
+ err = assert.has.error ->
+ COPILOT\eval_once '
+ (- (array (array 1 2) (array 1 2))
+ (array 1 2))'
+
+ describe "mul", ->
+ it "handles scalars and matrices", ->
+ with COPILOT\eval_once '
+ (* 3
+ (array
+ (array 1 2)
+ (array 4 5)))'
+ assert.is.true \is_const!
+ assert.is.equal '<num[2][2]= [[3 6] [12 15]]>', tostring .result
+
+ with COPILOT\eval_once '
+ (* (array
+ (array 1 2)
+ (array 4 5))
+ 3)'
+ assert.is.true \is_const!
+ assert.is.equal '<num[2][2]= [[3 6] [12 15]]>', tostring .result
+
+ it "handles vectors and matrices", ->
+ with COPILOT\eval_once '
+ (*
+ (array (array 1 0 0)
+ (array 0 1 0)
+ (array 0 0 1))
+ (array 4 5 6))'
+ assert.is.true \is_const!
+ assert.is.equal '<num[3]= [4 5 6]>', tostring .result
+
+ with COPILOT\eval_once '
+ (*
+ (array (array 1 0 0)
+ (array 0 1 0)
+ (array 3 2 1))
+ (array 4 5 1))'
+ assert.is.true \is_const!
+ assert.is.equal '<num[3]= [4 5 23]>', tostring .result
+
+ it "handles matrices", ->
+ with COPILOT\eval_once '
+ (*
+ (array (array 1 2 3)
+ (array 4 5 6))
+ (array (array 10 11)
+ (array 20 21)
+ (array 30 31)))'
+ assert.is.true \is_const!
+ assert.is.equal '<num[2][2]= [[140 146] [320 335]]>', tostring .result
+
+ it "handles everything mixed", ->
+ with COPILOT\eval_once '
+ (*
+ (array (array 1 2 3)
+ (array 4 5 6))
+ 2
+ (array (array 10 11)
+ (array 20 21)
+ (array 30 31)))'
+ assert.is.true \is_const!
+ assert.is.equal '<num[2][2]= [[280 292] [640 670]]>', tostring .result
+
+ with COPILOT\eval_once '
+ (*
+ (array (array 1 2 3)
+ (array 4 5 6))
+ 2
+ (array (array 10 11)
+ (array 20 21)
+ (array 30 31))
+ (array 4 7))'
+ assert.is.true \is_const!
+ assert.is.equal '<num[2]= [3164 7250]>', tostring .result
+
+ it "errors with wrong sizes (matrix and vector)", ->
+ err = assert.has.error -> COPILOT\eval_once '
+ (*
+ (array (array 1 2 3)
+ (array 4 5 6))
+ (array 1 2))'
+ -- assert.matches "", err
+
+ err = assert.has.error -> COPILOT\eval_once '
+ (*
+ (array 1 2 3)
+ (array (array 1 2 3)
+ (array 4 5 6)))'
+ -- assert.matches "", err
+
+ it "errors with wrong sizes (matrix)", ->
+ err = assert.has.error -> COPILOT\eval_once '
+ (*
+ (array (array 1 2 3)
+ (array 4 5 6))
+ (array (array 1 2)
+ (array 4 5)))'
+ -- assert.matches "", err
+
+ it "min, max, clamp, huge", ->
+ COPILOT\eval_once '
+ (expect= (array 3 2 1)
+ (min (array 3 4 1) (array 5 2 huge)))
+ (expect= (array 5 huge 4)
+ (max (array 3 huge 4) (array 5 999 2)))
+
+ (expect= (array -2 -1 0 1 3.5)
+ (clamp -2 3.5 (array -4 -1 0 1 4)))
+
+ (expect= 1 (inc 0))
+ (expect= -1 (dec 0))
+ (expect= 3 (inc (inc 1)))
+ (expect= -1 (dec (dec 1)))
+ (expect= 0 (inc (dec 0)) (dec (inc 0)))
+ '