From e83df1af2cdad8c2d61ba790a96875cd260eceaf Mon Sep 17 00:00:00 2001 From: s-ol Date: Sat, 21 Mar 2020 20:06:18 +0100 Subject: new meta/doc system --- copilot.moon | 10 +- core/base/action.moon | 33 ++-- core/builtin.moon | 487 ++++++++++++++++++++++++++++-------------------- core/cell.moon | 2 +- core/init.moon | 5 +- core/invoke.moon | 28 ++- core/result.moon | 2 - core/value.moon | 43 +++-- extra/docs.moon | 6 +- extra/layout.moon | 58 ++++-- lib/logic.moon | 159 +++++++++------- lib/math.moon | 218 +++++++++++++++------- lib/midi.moon | 169 +++++++++-------- lib/midi/core.moon | 70 ++++--- lib/midi/launchctl.moon | 210 +++++++++++---------- lib/osc.moon | 102 +++++----- lib/pilot.moon | 90 +++++---- lib/random.moon | 65 ++++--- lib/sc.moon | 49 ++--- lib/string.moon | 17 +- lib/time.moon | 267 ++++++++++++++------------ lib/util.moon | 188 +++++++++---------- 22 files changed, 1306 insertions(+), 972 deletions(-) diff --git a/copilot.moon b/copilot.moon index 600d2ed..8f98ab6 100644 --- a/copilot.moon +++ b/copilot.moon @@ -26,16 +26,18 @@ class Copilot if @root @registry\begin_tick! - L\try "error evaluating:", -> + ok, error = Error.try "updating", -> @root\tick_io! @root\tick! + if not ok + print error @registry\end_tick! eval: => @registry\begin_eval! - ast = L\try "error parsing:", parse, slurp @file - if not ast - L\error "error parsing" + ok, ast = Error.try "parsing '#{@file}'", parse, slurp @file + if not ok + print ast @registry\rollback_eval! return diff --git a/core/base/action.moon b/core/base/action.moon index cfca919..7ada4ba 100644 --- a/core/base/action.moon +++ b/core/base/action.moon @@ -15,9 +15,11 @@ class Action --- create a new instance. -- + -- @tparam Cell cell the Cell to evaluate -- @tparam Value head the (`AST:eval`d) `head` of the Cell to evaluate - -- @tparam Tag tag the Tag of the expression to evaluate - new: (@head, @tag) => + new: (@cell, @head) => + @tag = @cell.tag + @tag\replace @ --- perform the actual evaluation. -- @@ -43,12 +45,13 @@ class Action -- @tparam ?Action prev the previous Action instance setup: (prev) => - --- the head of the `Cell` this Action was created for. - -- + --- the `Cell` this Action was created for. + -- @tfield Cell cell + + --- the evaluated head of `cell`. -- @tfield AST head - --- the identity of the `Cell` this Action was created for. - -- + --- the identity of `cell`. -- @tfield Tag tag --- static functions @@ -61,31 +64,29 @@ class Action -- it pass it to `setup`. Register the `Action` with `tag`, evaluate it -- and return the `Result`. -- + -- @tparam Cell cell the `Cell` being evaluated -- @tparam Scope scope the active scope - -- @tparam Tag tag the tag of the `Cell` being evaluated -- @tparam Value head the (`AST:eval`d) head of the `Cell` being evaluated - -- @tparam {AST,...} tail the raw AST parameters to the `Cell` being evaluated -- @treturn Result the result of evaluation - @eval_cell: (scope, tag, head, tail) => - last = tag\last! + @eval_cell: (cell, scope, head) => + last = cell.tag\last! compatible = last and (last.__class == @) and last.head == head L\trace if compatible - "reusing #{last} for #{tag} <#{@__name} #{head}>" + "reusing #{last} for #{cell.tag} <#{@__name} #{head}>" else if last - "replacing #{last} with new #{tag} <#{@__name} #{head}>" + "replacing #{last} with new #{cell.tag} <#{@__name} #{head}>" else - "initializing #{tag} <#{@__name} #{head}>" + "initializing #{cell.tag} <#{@__name} #{head}>" - action = @ head, tag + action = @ cell, head if compatible action\setup last else last\destroy! if last action\setup nil - tag\replace action - action\eval scope, tail + action\eval scope, cell\tail! __tostring: => "<#{@@__name} #{@head}>" __inherited: (cls) => cls.__base.__tostring = @__tostring diff --git a/core/builtin.moon b/core/builtin.moon index 49de80f..bd3183f 100644 --- a/core/builtin.moon +++ b/core/builtin.moon @@ -6,216 +6,282 @@ -- -- @module builtin import Action, Op, FnDef, Input, match from require 'core.base' -import Value from require 'core.value' +import Value, LiteralValue from require 'core.value' import Result from require 'core.result' import Cell from require 'core.cell' import Scope from require 'core.scope' import Tag from require 'core.tag' import op_invoke from require 'core.invoke' -class doc extends Action - @doc: "(doc sym) - print documentation in console - -prints the docstring for sym in the console" - - eval: (scope, tail) => - assert #tail == 1, "'doc' takes exactly one parameter" - - result = L\push tail[1]\eval, scope - with Result children: { def } - value = result\const! - L\print "(doc #{tail[1]}):\n#{value.doc}\n" - -class def extends Action - @doc: "(def sym1 val-expr1 - [sym2 val-expr2]...) - declare symbols in parent scope - -defines the symbols sym1, sym2, ... to resolve to the values of val-expr1, val-expr2, ... -updates all val-exprs." - - eval: (scope, tail) => - L\trace "evaling #{@}" - assert #tail > 1, "'def' requires at least 2 arguments" - assert #tail % 2 == 0, "'def' requires an even number of arguments" - - children = L\push -> - return for i=1,#tail,2 - name, val_expr = tail[i], tail[i+1] - name = (name\quote scope)\unwrap 'sym' - - with val_expr\eval scope - scope\set name, \make_ref! - - Result :children - -class use extends Action - @doc: "(use scope1 [scope2]...) - merge scopes into parent scope - -adds all symbols from scope1, scope2, ... to the parent scope. -all scopes have to be eval-time constants." - - eval: (scope, tail) => - L\trace "evaling #{@}" - for child in *tail - result = L\push child\eval, scope - value = result\const! - scope\use value\unwrap 'scope', "'use' only works on scopes" - - Result! - -class require_ extends Action - @doc: "(require name-str) - require a module - -returns the module's scope -name-str has to be an eval-time constant." - - eval: (scope, tail) => - L\trace "evaling #{@}" - assert #tail == 1, "'require' takes exactly one parameter" - - result = L\push tail[1]\eval, scope - name = result\const! - - L\trace @, "loading module #{name}" - scope = Value.wrap require "lib.#{name\unwrap 'str'}" - Result :value - -class import_ extends Action - @doc: "(import sym1 [sym2]...) - require and define modules - -requires modules sym1, sym2, ... and defines them as sym1, sym2, ... in the current scope" - - eval: (scope, tail) => - L\trace "evaling #{@}" - assert #tail > 0, "'import' requires at least one arguments" - - for child in *tail - name = (child\quote scope)\unwrap 'sym' - value = Value.wrap require "lib.#{name}" - scope\set name, Result :value -- (require "lib.#{name})\unwrap 'scope' - Result! - -class import_star extends Action - @doc: "(import* sym1 [sym2]...) - require and use modules - -requires modules sym1, sym2, ... and merges them into the current scope" - - eval: (scope, tail) => - L\trace "evaling #{@}" - assert #tail > 0, "'import' requires at least one arguments" - - - for child in *tail - name = (child\quote scope)\unwrap 'sym' - value = Value.wrap require "lib.#{name}" - scope\use value\unwrap 'scope' -- (require "lib.#{name}")\unwrap 'scope' - - Result! - -class fn extends Action - @doc: "(fn (p1 [p2]...) body-expr) - declare a (lambda) function - -the symbols p1, p2, ... will resolve to the arguments passed to the function." - - eval: (scope, tail) => - L\trace "evaling #{@}" - assert #tail == 2, "'fn' takes exactly two arguments" - { params, body } = tail - - assert params.__class == Cell, "'fn's first argument has to be an expression" - param_symbols = for param in *params.children - assert param.type == 'sym', "function parameter declaration has to be a symbol" - param\quote scope - - body = body\quote scope - Result value: Value.wrap FnDef param_symbols, body, scope - -class defn extends Action - @doc: "(defn name-sym (p1 [p2]...) body-expr) - define a function - -declares a lambda (see (doc fn)) and defines it in the current scope" - - eval: (scope, tail) => - L\trace "evaling #{@}" - assert #tail == 3, "'defn' takes exactly three arguments" - { name, params, body } = tail - - name = (name\quote scope)\unwrap 'sym' - assert params.__class == Cell, "'defn's second argument has to be an expression" - param_symbols = for param in *params.children - assert param.type == 'sym', "function parameter declaration has to be a symbol" - param\quote scope - - body = body\quote scope - fn = FnDef param_symbols, body, scope - - scope\set name, Result value: Value.wrap fn - Result! - -class do_expr extends Action - @doc: "(do expr1 [expr2]...) - update multiple expressions - -evaluates and continously updates expr1, expr2, ... -the last expression's value is returned." - - eval: (scope, tail) => - scope = Scope scope - Result children: [expr\eval scope for expr in *tail] - -class if_ extends Action - @doc: "(if bool then-expr [else-xpr]) - make an eval-time const choice - -bool has to be an eval-time constant. If it is truthy, this expression is equivalent -to then-expr, otherwise it is equivalent to else-xpr if given, or nil otherwise." - - eval: (scope, tail) => - L\trace "evaling #{@}" - assert #tail >= 2, "'if' needs at least two parameters" - assert #tail <= 3, "'if' needs at most three parameters" - - { xif, xthen, xelse } = tail - - xif = L\push xif\eval, scope - xif = xif\const!\unwrap! - - if xif - xthen\eval scope - elseif xelse - xelse\eval scope - -class trace_ extends Action - @doc: "(trace! expr) - print an eval-time constant to the console" - - eval: (scope, tail) => - L\trace "evaling #{@}" - assert #tail == 1, "'trace!' takes exactly one parameter" - - with result = L\push tail[1]\eval, scope - L\print "trace! #{tail[1]\stringify!}: #{result.value}" - -class trace extends Action - @doc: "(trace expr) - print values to the console - -prints expr's value whenever it changes." - - class traceOp extends Op - setup: (inputs) => - { prefix, value } = match 'str any', inputs - super - prefix: Input.cold prefix - value: Input.value value - - tick: => - L\print "trace #{@inputs.prefix!}: #{@inputs.value.stream}" - - eval: (scope, tail) => - L\trace "evaling #{@}" - assert #tail == 1, "'trace!' takes exactly one parameter" - - op = Value 'opdef', traceOp - tag = @tag\clone Tag.parse '-1' - prefix = Value.str tostring tail[1] - op_invoke\eval_cell scope, tag, op, { prefix, tail[1] } +doc = Value.meta + meta: + name: 'doc' + summary: "Print documentation in console." + examples: { '(doc sym)' } + description: "Print the documentation for `sym` to the console" + + value: class extends Action + format_meta = => + str = @summary + if @examples + for example in *@examples + str ..= '\n' .. example + if @description + str ..= '\n' .. @description\match '^\n*(.+)\n*$' + str + + eval: (scope, tail) => + assert #tail == 1, "'doc' takes exactly one parameter" + + result = L\push tail[1]\eval, scope + with Result children: { def } + meta = result.value.meta + L\print "(doc #{tail[1]}):\n#{format_meta meta}\n" + +def = Value.meta + meta: + name: 'def' + summary: "Declare symbols in current scope." + examples: { '(def sym1 val-expr1 [sym2 val-expr2…])' } + description: " +Define the symbols `sym1`, `sym2`, … to resolve to the values of `val-expr1`, +`val-expr2`, …." + + value: class extends Action + eval: (scope, tail) => + L\trace "evaling #{@}" + assert #tail > 1, "'def' requires at least 2 arguments" + assert #tail % 2 == 0, "'def' requires an even number of arguments" + + children = L\push -> + return for i=1,#tail,2 + name, val_expr = tail[i], tail[i+1] + name = (name\quote scope)\unwrap 'sym' + + with val_expr\eval scope + scope\set name, \make_ref! + + Result :children + +use = Value.meta + meta: + name: 'use' + summary: "Merge scopes into current scope." + examples: { '(use scope1 [scope2…])' } + description: " +Copy all symbol definitions from `scope1`, `scope2`, … to the current scope. +All arguments have to be evaltime constant." + + value: class extends Action + eval: (scope, tail) => + L\trace "evaling #{@}" + for child in *tail + result = L\push child\eval, scope + value = result\const! + scope\use value\unwrap 'scope', "'use' only works on scopes" + + Result! + +require_ = Value.meta + meta: + name: 'require' + summary: "Load a module." + examples: { '(require name)' } + description: "Load a module and return its scope." + + value: class extends Action + eval: (scope, tail) => + L\trace "evaling #{@}" + assert #tail == 1, "'require' takes exactly one parameter" + + result = L\push tail[1]\eval, scope + name = result\const! + + L\trace @, "loading module #{name}" + scope = Value.wrap require "lib.#{name\unwrap 'str'}" + Result :value + +import_ = Value.meta + meta: + name: 'import' + summary: "Require and define modules." + examples: { '(import sym1 [sym2…])' } + description: " +Requires modules `sym1`, `sym2`, … and define them as `sym1`, `sym2`, … in the +current scope." + + value: class extends Action + eval: (scope, tail) => + L\trace "evaling #{@}" + assert #tail > 0, "'import' requires at least one arguments" + + for child in *tail + name = (child\quote scope)\unwrap 'sym' + value = Value.wrap require "lib.#{name}" + scope\set name, Result :value -- (require "lib.#{name})\unwrap 'scope' + Result! + +import_star = Value.meta + meta: + name: 'import*' + summary: "Require and use modules." + examples: { '(import* sym1 [sym2…])' } + description: " +Requires modules `sym1`, `sym2`, … and merges them into the current scope." + + value: class extends Action + eval: (scope, tail) => + L\trace "evaling #{@}" + assert #tail > 0, "'import' requires at least one arguments" + + + for child in *tail + name = (child\quote scope)\unwrap 'sym' + value = Value.wrap require "lib.#{name}" + scope\use value\unwrap 'scope' -- (require "lib.#{name}")\unwrap 'scope' + + Result! + +fn = Value.meta + meta: + name: 'fn' + summary: "Declare a function." + examples: { '(fn (p1 [p2…]) body-expr)' } + description: " +The symbols `p1`, `p2`, ... will resolve to the arguments passed when the +function is invoked." + + value: class extends Action + eval: (scope, tail) => + L\trace "evaling #{@}" + assert #tail == 2, "'fn' takes exactly two arguments" + { params, body } = tail + + assert params.__class == Cell, "'fn's first argument has to be an expression" + param_symbols = for param in *params.children + assert param.type == 'sym', "function parameter declaration has to be a symbol" + param\quote scope + + body = body\quote scope + Result value: with Value.wrap FnDef param_symbols, body, scope + .meta = { + summary: "(user defined function)" + examples: { "(??? #{table.concat [p! for p in *param_symbols], ' '})" } + } + +defn = Value.meta + meta: + name: 'defn' + summary: "Define a function." + examples: { '(defn name-sym (p1 [p2…]) body-expr)' } + description: " +Declare a function and define it as `name-sym` in the current scope. +The symbols `p1`, `p2`, ... will resolve to the arguments passed when the +function is invoked." + + value: class extends Action + eval: (scope, tail) => + L\trace "evaling #{@}" + assert #tail == 3, "'defn' takes exactly three arguments" + { name, params, body } = tail + + name = (name\quote scope)\unwrap 'sym' + assert params.__class == Cell, "'defn's second argument has to be an expression" + param_symbols = for param in *params.children + assert param.type == 'sym', "function parameter declaration has to be a symbol" + param\quote scope + + body = body\quote scope + + value = with Value.wrap FnDef param_symbols, body, scope + .meta = + :name + summary: "(user defined function)" + examples: { "(#{name} #{table.concat [p! for p in *param_symbols], ' '})" } + + scope\set name, Result :value + Result! + +do_expr = Value.meta + meta: + name: 'do_expr' + summary: "Evaluate multiple expressions in a new scope." + examples: { '(do expr1 [expr2…])' } + description: " +Evaluate `expr1`, `expr2`, … and return the value of the last expression." + + value: class extends Action + eval: (scope, tail) => + scope = Scope scope + Result children: [expr\eval scope for expr in *tail] + +if_ = Value.meta + meta: + name: 'if' + summary: "Make an evaltime const choice." + examples: { '(if bool then-expr [else-expr])' } + description: " +`bool` has to be an evaltime constant. If it is truthy, this expression is equivalent +to `then-expr`, otherwise it is equivalent to `else-xpr` if given, or nil otherwise." + + value: class extends Action + eval: (scope, tail) => + L\trace "evaling #{@}" + assert #tail >= 2, "'if' needs at least two parameters" + assert #tail <= 3, "'if' needs at most three parameters" + + { xif, xthen, xelse } = tail + + xif = L\push xif\eval, scope + xif = xif\const!\unwrap! + + if xif + xthen\eval scope + elseif xelse + xelse\eval scope + +trace_ = Value.meta + meta: + name: 'trace!' + summary: "Trace an expression's value at evaltime." + examples: { '(trace! expr)' } + + value: class extends Action + eval: (scope, tail) => + L\trace "evaling #{@}" + assert #tail == 1, "'trace!' takes exactly one parameter" + + with result = L\push tail[1]\eval, scope + L\print "trace! #{tail[1]\stringify!}: #{result.value}" + +trace = Value.meta + meta: + name: 'trace' + summary: "Trace an expression's values at runtime." + examples: { '(trace expr)' } + + value: class extends Action + class traceOp extends Op + setup: (inputs) => + { prefix, value } = match 'str any', inputs + super + prefix: Input.cold prefix + value: Input.value value + + tick: => + L\print "trace #{@inputs.prefix!}: #{@inputs.value.stream}" + + eval: (scope, tail) => + L\trace "evaling #{@}" + assert #tail == 1, "'trace!' takes exactly one parameter" + + tag = @tag\clone Tag.parse '-1' + inner = Cell tag, { + LiteralValue 'opdef', traceOp, 'trace' + Value.str tostring tail[1] + tail[1] + } + inner\eval scope { :doc @@ -226,8 +292,17 @@ prints expr's value whenever it changes." import: import_ 'import*': import_star - true: Value.bool true - false: Value.bool false + true: Value.meta + meta: + name: 'true' + summary: "The boolean constant `true`." + value: Value.bool true + + false: Value.meta + meta: + name: 'false' + summary: "The boolean constant `false`." + value: Value.bool false :fn, :defn 'do': do_expr diff --git a/core/cell.moon b/core/cell.moon index 2109ade..b7f4a1d 100644 --- a/core/cell.moon +++ b/core/cell.moon @@ -83,7 +83,7 @@ class Cell else error "cannot evaluate expr with head #{head}" - Action\eval_cell scope, @tag, head, @tail! + Action\eval_cell @, scope, head --- quote this Cell, preserving its identity. -- diff --git a/core/init.moon b/core/init.moon index 00e3cd4..1aa0d2d 100644 --- a/core/init.moon +++ b/core/init.moon @@ -49,7 +49,10 @@ globals = Scope.from_table require 'core.builtin' :Registry, :SimpleRegistry, :Tag :globals - parse: program\match + + parse: (str) -> + assert (program\match str), Error 'syntax', "failed to parse" + eval: (str, inject) -> scope = Scope nil, globals scope\use inject if inject diff --git a/core/invoke.moon b/core/invoke.moon index 97d3949..654e436 100644 --- a/core/invoke.moon +++ b/core/invoke.moon @@ -8,6 +8,20 @@ import Action from require 'core.base' import Scope from require 'core.scope' import Error from require 'core.error' +get_name = (value, raw) -> + meta = if value.meta then value.meta.name + locl = if raw and raw.type == 'sym' then raw! + + if locl + if meta and meta != locl + "'#{meta}' (local '#{locl}')" + else + "'#{locl}'" + else if meta + "'#{meta}'" + else + "(unnamed)" + --- `Action` implementation that invokes an `Op`. -- -- @type op_invoke @@ -41,7 +55,9 @@ class op_invoke extends Action -- @treturn Result eval: (scope, tail) => children = [L\push expr\eval, scope for expr in *tail] - Error.wrap "invoking #{@op}#{@tag}", @op\setup, [result for result in *children], scope + + frame = "invoking op #{get_name @head, @cell\head!} at [#{@tag}]" + Error.wrap frame, @op\setup, [result for result in *children], scope any_dirty = false for input in @op\all_inputs! @@ -81,9 +97,13 @@ class fn_invoke extends Action -- @tparam {AST,...} tail the arguments to this expression -- @treturn Result the result of this evaluation eval: (outer_scope, tail) => - { :params, :body, :scope } = @head\unwrap 'fndef', "cant fn-invoke #{@head}" + name = get_name @head, @cell\head! + frame = "invoking function #{name} at [#{@tag}]" - assert #params == #tail, "argument count mismatch in #{@head}" + { :params, :body, :scope } = @head\unwrap 'fndef', "cant fn-invoke #{@head}" + if #params != #tail + error with Error 'argument', "expected #{#params} arguments, found #{#tail}" + \add_frame frame fn_scope = Scope scope, outer_scope @@ -93,7 +113,7 @@ class fn_invoke extends Action fn_scope\set name, \make_ref! clone = body\clone @tag - result = Error.wrap "invoking function #{body.tag} at #{@tag}", clone\eval, fn_scope + result = Error.wrap frame, clone\eval, fn_scope table.insert children, result Result :children, value: result.value diff --git a/core/result.moon b/core/result.moon index 357a868..52203da 100644 --- a/core/result.moon +++ b/core/result.moon @@ -61,9 +61,7 @@ class Result for stream in @op\all_inputs! if stream\dirty! self_dirty = true - break - -- L\trace "#{@op} is #{if self_dirty then 'dirty' else 'clean'}" return unless self_dirty @op\tick! diff --git a/core/value.moon b/core/value.moon index 7f5a93d..da7f6d0 100644 --- a/core/value.moon +++ b/core/value.moon @@ -72,23 +72,22 @@ class Value -- - `fndef` - `value` is a `FnDef` instance -- - `scope` - `value` is a `Scope` instance -- - -- @tfield string type the type name + -- @tfield string type --- the wrapped Lua value. + -- @tfield any value + + --- documentation metadata. -- - -- the following builtin typenames are used: + -- an optional table containing metadata for error messages and + -- documentation. The following keys are recognized: -- - -- - `str` - strings, `value` is a Lua string - -- - `sym` - symbols, `value` is a Lua string - -- - `num` - numbers, `value` is a Lua number - -- - `bool` - booleans, `value` is a Lua boolean - -- - `bang` - trigger signals, `value` is a Lua boolean - -- - `opdef` - `value` is an `Op` subclass - -- - `builtin` - `value` is an `Action` subclass - -- - `fndef` - `value` is a `FnDef` instance - -- - `scope` - `value` is a `Scope` instance + -- - `name`: optional name + -- - `summary`: single-line description (markdown) + -- - `examples`: optional list of single-line code examples + -- - `description`: optional full-text description (markdown) -- - -- @tfield any value the wrapped value + -- @tfield ?table meta --- AST interface -- @@ -139,6 +138,7 @@ class Value -- @tparam any value the Lua value to be accessed through `unwrap` -- @tparam string raw the raw string that resulted in this value. Used by `parsing`. new: (@type, @value, @raw) => + @meta = {} unescape = (str) -> str\gsub '\\([\'"\\])', '%1' --- create a capture-function (for parsing with Lpeg). @@ -157,6 +157,7 @@ class Value -- -- @tparam any val the value to wrap -- @tparam[opt] string name the name of this value (for error logging) + -- @treturn Value @wrap: (val, name='(unknown)') -> typ = switch type val when 'number' then 'num' @@ -188,21 +189,39 @@ class Value --- create a constant number. -- @tparam number num the number + -- @treturn Value @num: (num) -> Value 'num', num, tostring num --- create a constant string. -- @tparam string str the string + -- @treturn Value @str: (str) -> Value 'str', str, "'#{str}'" --- create a constant symbol. -- @tparam string sym the symbol + -- @treturn Value @sym: (sym) -> Value 'sym', sym, sym --- create a constant boolean. -- @tparam boolean bool the boolean + -- @treturn Value @bool: (bool) -> Value 'bool', bool, tostring bool + --- wrap and document a value. + -- + -- wraps `args.value` using `wrap`, then assigns `meta`. + -- + -- @tparam table args table with keys `value` and `meta` + -- @treturn Value + @meta: (args) -> + with Value.wrap args.value + .meta = args.meta if args.meta + +class LiteralValue extends Value + eval: => Result value: @ + { :Value + :LiteralValue :load_ } diff --git a/extra/docs.moon b/extra/docs.moon index d1553df..f43ecfa 100644 --- a/extra/docs.moon +++ b/extra/docs.moon @@ -1,6 +1,6 @@ import Value, Scope from require 'core' import render, layout, autoref from require 'extra.layout' -import section, h1, h2, p, ul, li, a, code, r from require 'extra.dom' +import section, h1, h2, h3, p, ul, li, a, code, r from require 'extra.dom' export OUT, BASE, require { OUT, command } = arg @@ -33,6 +33,10 @@ spit OUT, switch command title: "#{name} reference" body: section { h2 (code name), ' module reference' + h3 'index' + ul for key, res in opairs module.values + li render key, res.value, nil, true + h3 'details' ul for key, res in opairs module.values li render key, res.value } diff --git a/extra/layout.moon b/extra/layout.moon index a3d67c8..dd309dc 100644 --- a/extra/layout.moon +++ b/extra/layout.moon @@ -1,26 +1,48 @@ -v = require 'core.version' +version = require 'core.version' +import compile from require 'discount' + +render_meta = (meta) -> + import p, code from require 'extra.dom' + contents = {} + if meta.examples + -- table.insert contents, h4 'signature' + examples = p table.concat [code e for e in *meta.examples], ' ' + table.insert contents, examples + if meta.description + description = compile meta.description\match '^\n*(.+)\n*$' + table.insert contents, description.body + + contents -- render an ALV Value to a HTML string -render = (name, value, prefix=nil) -> +render = (name, value, prefix=nil, index=false) -> import div, label, code, ul, li, i, a, pre from require 'extra.dom' id = if prefix then "#{prefix}/#{name}" else name type = i value.type + assert value.meta, "#{id} doesn't have any metadata!" + summary = assert value.meta.summary, "#{id} doesn't have a summary!" - content = switch value.type - when 'scope' - ul for k, result in opairs value!.values - li render k, result.value, id - when 'opdef', 'builtin' - pre value!.doc - when 'num', 'str', 'bool' - code tostring value! - - div { - :id, class: 'def' - label (a (code name), href: "##{id}"), ' (', type, '):' - div content, class: 'nest' - } + if index + div { + label (a (code name), href: "##{id}"), ' (', type, '):  – ' + summary + } + else + content = switch value.type + when 'scope' + ul for k, result in opairs value!.values + li render k, result.value, id + else + render_meta value.meta + + content.class = 'nest' + div { + :id, class: 'def' + label (a (code name), href: "##{id}"), ' (', type, '):  – ' + summary + div content + } -- generate a relative link abs = (page) -> @@ -78,7 +100,7 @@ layout = (opts) -> span { a 'alive', href: abs 'index.html' ' ' - code v.tag + code version.tag ' documentation' } div class: 'grow' @@ -94,7 +116,7 @@ layout = (opts) -> "alive documentation" foot = footer div { 'alive ' - a (code v.tag), href: v.web + a (code version.tag), href: version.web ', generated ' os.date '!%Y-%m-%d %T' } diff --git a/lib/logic.moon b/lib/logic.moon index 7264be0..157d190 100644 --- a/lib/logic.moon +++ b/lib/logic.moon @@ -1,4 +1,4 @@ -import Op, Input, Error, match from require 'core.base' +import Op, Value, Input, Error, match from require 'core.base' all_same = (first, list) -> for v in *list @@ -31,77 +31,98 @@ class ReduceOp extends Op @out\set accum -class eq extends Op - @doc: "(eq a b [c]...) -(== a b [c]...) - check for equality +eq = Value.meta + meta: + name: 'eq' + summary: "Check for equality." + examples: { '(== a b [c]...)', '(eq a b [c]...)' } + description: "`true` if the types and values of all arguments are equal." + + value: class extends Op + new: => super 'bool', false + + setup: (inputs) => + { first, rest } = match "any *any", inputs + same = all_same first\type!, [i\type! for i in *rest] + + super if same + { + first: Input.value first + rest: [Input.value v for v in *rest] + } + else + {} + + tick: => + if not @inputs.first + @out\set false + return + + { :first, :rest } = @unwrap_all! + + equal = true + for other in *rest + if first != other + equal = false + break -If the value types dont match, the result is an eval-time constant 'false'." + @out\set equal - new: => super 'bool', false - setup: (inputs) => - { first, rest } = match "any *any", inputs - same = all_same first\type!, [i\type! for i in *rest] +not_eq = Value.meta + meta: + name: 'not-eq' + summary: "Check for inequality." + examples: { '(!= a b [c]...)', '(not-eq a b [c]...)' } + description: "`true` if types or values of any two arguments are different." - super if same - { - first: Input.value first - rest: [Input.value v for v in *rest] - } - else - {} + value: class extends Op + new: => super 'bool' - tick: => - if not @inputs.first - @out\set false - return + setup: (inputs) => + assert #inputs > 1, Error 'argument', "need at least two values" + super [Input.value v for v in *inputs] - { :first, :rest } = @unwrap_all! + tick: => + if not @inputs[1] + @out\set true + return - equal = true - for other in *rest - if first != other - equal = false - break + diff = true + for a=1, #@inputs-1 + for b=a+1, #@inputs + if @inputs[a].stream == @inputs[b].stream + diff = false + break - @out\set equal + break unless diff -class not_eq extends Op - @doc: "(not-eq a b [c]...) -(!= a b [c]...) - check for inequality" - new: => super 'bool' + @out\set diff - setup: (inputs) => - assert #inputs > 1, Error 'argument', "need at least two values" - super [Input.value v for v in *inputs] +and_ = Value.meta + meta: + name: 'and' + summary: "Logical AND." + examples: { '(and a b [c…])' } + value: class extends ReduceOp + fn: (a, b) -> a and b - tick: => - if not @inputs[1] - @out\set true - return - - diff = true - for a=1, #@inputs-1 - for b=a+1, #@inputs - if @inputs[a].stream == @inputs[b].stream - diff = false - break +or_ = Value.meta + meta: + name: 'or' + summary: "Logical OR." + examples: { '(or a b [c…])' } + value: class extends ReduceOp + fn: (a, b) -> a or b - break unless diff +not_ = Value.meta + meta: + name: 'not' + summary: "Logical NOT." + examples: { '(not a)' } - @out\set diff - -class and_ extends ReduceOp - @doc: "(and a b [c]...) - AND values" - fn: (a, b) -> a and b - -class or_ extends ReduceOp - @doc: "(or a b [c]...) - OR values" - fn: (a, b) -> a or b - -class not_ extends Op - @doc: "(not a) - boolean opposite" - new: => super 'bool' + value: class extends Op + new: => super 'bool' setup: (inputs) => { value } = match 'any', inputs @@ -109,15 +130,21 @@ class not_ extends Op tick: => @out\set not tobool @inputs.value! -class bool extends Op - @doc: "(bool a) - convert to bool" - new: => super 'bool' +bool = Value.meta + meta: + name: 'bool' + summary: "Cast value to bool." + examples: { '(bool a)' } + description: "`false` if a is `false`, `nil` or `0`, `true` otherwise." - setup: (inputs) => - { value } = match 'any', inputs - super value: Input.value value + value: class extends Op + new: => super 'bool' + + setup: (inputs) => + { value } = match 'any', inputs + super value: Input.value value - tick: => @out\set tobool @inputs\value! + tick: => @out\set tobool @inputs\value! { :eq, '==': eq diff --git a/lib/math.moon b/lib/math.moon index 84e6682..69d83bb 100644 --- a/lib/math.moon +++ b/lib/math.moon @@ -17,36 +17,31 @@ class ReduceOp extends Op accum = @.fn accum, val @out\set accum -class add extends ReduceOp - @doc: "(+ a b [c]...) -(add a b [c]...) - add values" - - fn: (a, b) -> a + b - -class sub extends ReduceOp - @doc: "(- a b [c]...) -(sub a b [c]...) - subtract values - -subtracts all other arguments from a" - - fn: (a, b) -> a - b - -class mul extends ReduceOp - @doc: "(* a b [c]...) -(mul a b [c]...) - multiply values" +func_op = (arity, func) -> + class extends Op + new: => super 'num' - fn: (a, b) -> a * b + setup: (inputs) => + { params } = match '*num', inputs + if arity != '*' + err = Error 'argument', "need exactly #{arity} arguments" + assert #params == arity, err + super [Input.value p for p in *params] -class div extends ReduceOp - @doc: "(/ a b [c]...) -(div a b [c]...) - divide values + tick: => @out\set func unpack @unwrap_all! -divides a by all other arguments" +func_def = (name, args, func, summary) -> + _, arity = args\gsub ' ', ' ' - fn: (a, b) -> a / b + Value.meta + meta: + :name + :summary + examples: { "(#{name} #{args})" } + value: func_op arity+1, func -evenodd_op = (name, remainder) -> - class k extends Op +evenodd_op = (remainder) -> + class extends Op new: => super 'bool' setup: (inputs) => @@ -59,57 +54,144 @@ evenodd_op = (name, remainder) -> { :val, :div } = @unwrap_all! @out\set (val % div) == remainder - k.__name = name - k.doc = "(#{name} a [div]) - check for #{name} divison - -div defaults to 2" - k - -func_op = (name, arity, func) -> - k = class extends Op - new: => super 'num' - - setup: (inputs) => - { params } = match '*num', inputs - if arity != '*' - err = Error 'argument', "need exactly #{arity} arguments" - assert #params == arity, err - super [Input.value p for p in *params] - - tick: => @out\set func unpack @unwrap_all! - - k.__name = name - k - -mod = func_op 'mod', 2, (a, b) -> a % b - -module = { +add = Value.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 = Value.meta + meta: + name: 'sub' + 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 + +mul = Value.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 = Value.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 = Value.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 = Value.meta + meta: + name: 'mod' + summary: 'Modulo operator.' + examples: { '(% num div)', '(mod num div)' } + description: "Calculate remainder of division by `div`." + value: func_op 2, (a, b) -> a % b + +even = Value.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 = Value.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 = Value.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 3, (a, b, i) -> i*b + (1-i)*a + +min = Value.meta + meta: + name: 'min' + summary: "Find the minimum." + examples: { '(min a b [c…])' } + description: "Return the lowest of arguments." + value: func_op '*', math.min + +max = Value.meta + meta: + name: 'max' + summary: "Find the maximum." + examples: { '(max a b [c…])' } + description: "Return the highest of arguments." + value: func_op '*', math.min + +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)." +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 given base." +log10 = func_def 'log10', 'val', math.log10, "Logarithm with base 10." +sqrt = func_def 'sqrt', 'val', math.sqrt, "Square root function." + +{ :add, '+': add :sub, '-': sub :mul, '*': mul :div, '/': div + :pow, '^': pow :mod, '%': mod - mix: func_op 'mix', 3, (a, b, i) -> i*b + (1-i)*a - even: evenodd_op 'even', 0 - odd: evenodd_op 'odd', 1 + :even, :odd - pi: math.pi - tau: math.pi*2 - huge: math.huge -} - -for name, arity in pairs { - exp: 1, log: 2, log10: 1, sqrt: 1 + :mix + :min, :max - cos: 1, sin: 1, tan: 1 - asin: 1, acos: 1, atan: 1, atan2: 2 - sinh: 1, cosh: 1, tanh: 1 + pi: with Value.wrap math.pi + .meta = summary: 'The pi constant.' + tau: with Value.wrap math.pi*2 + .meta = summary: 'The tau constant.' + huge: with Value.wrap math.huge + .meta = summary: 'Positive infinity constant.' - min: '*', max: '*' + :sin, :cos, :tan + :asin, :acos, :atan, :atan2 + :sinh, :cosh, :tanh - floor: 1, ceil: 1, abs: 1 + :floor, :ceil, :abs + :exp, :log, :log10, :sqrt } - module[name] = func_op name, arity, math[name] - -module diff --git a/lib/midi.moon b/lib/midi.moon index 33fe936..e978702 100644 --- a/lib/midi.moon +++ b/lib/midi.moon @@ -1,63 +1,73 @@ import Value, Op, Input, match from require 'core.base' import input, output, inout, apply_range from require 'lib.midi.core' -class gate extends Op - @doc: "(midi/gate port note [chan]) - gate from note-on and note-off messages" - - new: => - super 'bool', false - - setup: (inputs) => - { port, note, chan } = match 'midi/port num num?', inputs - super - port: Input.io port - note: Input.value note - chan: Input.value chan or Value.num -1 - - tick: => - { :port, :note, :chan } = @inputs - - if note\dirty! or chan\dirty! - @out\set false - - if port\dirty! - for msg in port!\receive! - if msg.a == note! and (chan! == -1 or msg.chan == chan!) - if msg.status == 'note-on' - @out\set true - elseif msg.status == 'note-off' - @out\set false - -class trig extends Op - @doc: "(midi/trig port note [chan]) - gate from note-on and note-off messages" - - new: => - super 'bang', false - - setup: (inputs) => - { port, note, chan } = match 'midi/port num num?', inputs - super - port: Input.io port - note: Input.value note - chan: Input.value chan or Value.num -1 - - tick: => - { :port, :note, :chan } = @inputs - - if note\dirty! or chan\dirty! - @out\set false - - if port\dirty! - for msg in port!\receive! - if msg.a == note! and (chan! == -1 or msg.chan == chan!) - if msg.status == 'note-on' - @out\set true - - -class cc extends Op - @doc: "(midi/cc port cc [chan [range]]) - MIDI CC to number - -range can be one of: +gate = Value.meta + meta: + name: 'gate' + summary: "gate from note-on and note-off messages." + examples: { '(midi/gate port note [chan])' } + + value: class extends Op + new: => + super 'bool', false + + setup: (inputs) => + { port, note, chan } = match 'midi/port num num?', inputs + super + port: Input.io port + note: Input.value note + chan: Input.value chan or Value.num -1 + + tick: => + { :port, :note, :chan } = @inputs + + if note\dirty! or chan\dirty! + @out\set false + + if port\dirty! + for msg in port!\receive! + if msg.a == note! and (chan! == -1 or msg.chan == chan!) + if msg.status == 'note-on' + @out\set true + elseif msg.status == 'note-off' + @out\set false + +trig = Value.meta + meta: + name: 'trig' + summary: "`bang`s from note-on messages." + examples: { '(midi/trig port note [chan])' } + + value: class extends Op + new: => + super 'bang', false + + setup: (inputs) => + { port, note, chan } = match 'midi/port num num?', inputs + super + port: Input.io port + note: Input.value note + chan: Input.value chan or Value.num -1 + + tick: => + { :port, :note, :chan } = @inputs + + if note\dirty! or chan\dirty! + @out\set false + + if port\dirty! + for msg in port!\receive! + if msg.a == note! and (chan! == -1 or msg.chan == chan!) + if msg.status == 'note-on' + @out\set true + +trig = Value.meta + meta: + name: 'trig' + summary: "`num` from cc-change messages." + examples: { '(midi/cc port cc [chan [range]])' } + description: " +`range` can be one of: - 'raw' [ 0 - 128[ - 'uni' [ 0 - 1[ (default) - 'bip' [-1 - 1[ @@ -65,28 +75,29 @@ range can be one of: - 'deg' [ 0 - 360[ - (num) [ 0 - num[" - new: => - super 'num' - - setup: (inputs) => - { port, cc, chan, range } = match 'midi/port num num? any?', inputs - super - port: Input.io port - cc: Input.value cc - chan: Input.value chan or Value.num -1 - range: Input.value range or Value.str 'uni' - - if not @out\unwrap! - @out\set apply_range @inputs.range, 0 - - tick: => - { :port, :cc, :chan, :range } = @inputs - if port\dirty! - for msg in port!\receive! - if msg.status == 'control-change' and - (chan! == -1 or msg.chan == chan!) and - msg.a == cc! - @out\set apply_range range, msg.b + value: class extends Op + new: => + super 'num' + + setup: (inputs) => + { port, cc, chan, range } = match 'midi/port num num? any?', inputs + super + port: Input.io port + cc: Input.value cc + chan: Input.value chan or Value.num -1 + range: Input.value range or Value.str 'uni' + + if not @out\unwrap! + @out\set apply_range @inputs.range, 0 + + tick: => + { :port, :cc, :chan, :range } = @inputs + if port\dirty! + for msg in port!\receive! + if msg.status == 'control-change' and + (chan! == -1 or msg.chan == chan!) and + msg.a == cc! + @out\set apply_range range, msg.b { :input diff --git a/lib/midi/core.moon b/lib/midi/core.moon index c437259..4986811 100644 --- a/lib/midi/core.moon +++ b/lib/midi/core.moon @@ -1,4 +1,4 @@ -import IO, Op, Registry, Input, Error, match from require 'core.base' +import Value, IO, Op, Registry, Input, Error, match from require 'core.base' import RtMidiIn, RtMidiOut, RtMidi from require 'luartmidi' bit = do @@ -67,34 +67,46 @@ class PortOp extends Op out = out and find_port RtMidiOut, out! @out\set MidiPort inp, out -class input extends PortOp - @doc: "(midi/input name) - create a MIDI input port" - - setup: (inputs) => - { name } = match 'str', inputs - super name: Input.value name - - tick: => super @inputs.name - -class output extends PortOp - @doc: "(midi/output name) - create a MIDI output port" - - setup: (inputs) => - { name } = match 'str', inputs - super name: Input.value name - - tick: => super nil, @inputs.name - -class inout extends PortOp - @doc: "(midi/inout inname outname) - create a bidirectional MIDI port" - - setup: (inputs) => - { inp, out } = match 'str str', inputs - super - inp: Input.value inp - out: Input.value out - - tick: => super @inputs.inp, @inputs.out +input = Value.meta + meta: + name: 'input' + summary: "Create a MIDI input port." + examples: { '(midi/input name)' } + + value: class extends PortOp + setup: (inputs) => + { name } = match 'str', inputs + super name: Input.value name + + tick: => super @inputs.name + +output = Value.meta + meta: + name: 'output' + summary: "Create a MIDI output port." + examples: { '(midi/output name)' } + + value: class extends PortOp + setup: (inputs) => + { name } = match 'str', inputs + super name: Input.value name + + tick: => super nil, @inputs.name + +inout = Value.meta + meta: + name: 'inout' + summary: "Create a bidirectional MIDI port." + examples: { '(midi/inout name)' } + + value: class extends PortOp + setup: (inputs) => + { inp, out } = match 'str str', inputs + super + inp: Input.value inp + out: Input.value out + + tick: => super @inputs.inp, @inputs.out apply_range = (range, val) -> if range\type! == 'str' diff --git a/lib/midi/launchctl.moon b/lib/midi/launchctl.moon index 1084f66..37ccb82 100644 --- a/lib/midi/launchctl.moon +++ b/lib/midi/launchctl.moon @@ -5,9 +5,12 @@ import bor, lshift from bit unpack or= table.unpack color = (r, g) -> bit.bor 12, r, (bit.lshift g, 4) -class cc_seq extends Op - @doc: "(launctl/cc-seq port i start chan [steps [range]]) - CC-Sequencer - +cc_seq = Value.meta + meta: + name: 'cc-seq' + summary: "MIDI CC-Sequencer." + examples: { '(launchctl/cc-seq port i start chan [steps [range]])' } + description: " returns the value for the i-th step steps (buttons starting from start). steps defaults to 8. @@ -19,104 +22,109 @@ range can be one of: - 'deg' [ 0 - 360[ - (num) [ 0 - num[" - new: => super 'num' - - setup: (inputs) => - { port, i, start, - chan, steps, range } = match 'midi/port num num num num? any?', inputs - - super - port: Input.io port - i: Input.value i - start: Input.value start - chan: Input.value chan - steps: Input.value steps or Value.num 8 - range: Input.value range or Value.str 'uni' - - if not @out\unwrap! - @out\set apply_range @inputs.range, 0 - - tick: => - { :port, :i, :start, :chan, :steps, :range } = @inputs - - if steps\dirty! - while steps! > #@state - table.insert @state, 0 - while steps! < #@state - table.remove @state - - curr_i = i! % #@state - if port\dirty! - changed = false - for msg in port!\receive! - if msg.status == 'control-change' and msg.chan == chan! - rel_i = msg.a - start! - if rel_i >= 0 and rel_i < #@state - @state[rel_i+1] = msg.b - changed = rel_i == curr_i - @out\set apply_range range, @state[curr_i+1] if changed - else - @out\set apply_range range, @state[curr_i+1] - -class gate_seq extends Op - @doc: "(launctl/gate-seq port i start chan [steps]) - Gate-Sequencer - -returns true or false for the i-th step steps (buttons starting from start). -steps defaults to 8." - - new: => - super 'bool', false - @state = {} - - setup: (inputs) => - { port, i, start, chan, steps } = match 'midi/port num num num num?', inputs - - super - port: Input.io port - i: Input.value i - start: Input.value start - chan: Input.value chan - steps: Input.value steps or Value.num 8 - - light = (set, active) -> - set = if set then 'S' else ' ' - active = if active then 'A' else ' ' - color switch set .. active - when ' ' then 0, 0 - when ' A' then 1, 1 - when 'S ' then 1, 0 - when 'SA' then 3, 1 - - display: (i, active) => - { :port, :start, :chan } = @unwrap_all! - port\send 'note-on', chan, (start + i), light @state[i+1], active - - tick: => - { :port, :i, :start, :chan, :steps } = @inputs - - if steps\dirty! - while steps! > #@state - table.insert @state, false - while steps! < #@state - table.remove @state - - curr_i = i! % #@state - - if port\dirty! - for msg in port!\receive! - if msg.status == 'note-on' and msg.chan == chan! - rel_i = msg.a - start! - if rel_i >= 0 and rel_i < #@state - @state[rel_i+1] = not @state[rel_i+1] - @display rel_i, rel_i == curr_i - - if i\dirty! - prev_i = (curr_i - 1) % #@state - - @display curr_i, true - @display prev_i, false - - @out\set @state[curr_i+1] + value: class extends Op + new: => super 'num' + + setup: (inputs) => + { port, i, start, + chan, steps, range } = match 'midi/port num num num num? any?', inputs + + super + port: Input.io port + i: Input.value i + start: Input.value start + chan: Input.value chan + steps: Input.value steps or Value.num 8 + range: Input.value range or Value.str 'uni' + + if not @out\unwrap! + @out\set apply_range @inputs.range, 0 + + tick: => + { :port, :i, :start, :chan, :steps, :range } = @inputs + + if steps\dirty! + while steps! > #@state + table.insert @state, 0 + while steps! < #@state + table.remove @state + + curr_i = i! % #@state + if port\dirty! + changed = false + for msg in port!\receive! + if msg.status == 'control-change' and msg.chan == chan! + rel_i = msg.a - start! + if rel_i >= 0 and rel_i < #@state + @state[rel_i+1] = msg.b + changed = rel_i == curr_i + @out\set apply_range range, @state[curr_i+1] if changed + else + @out\set apply_range range, @state[curr_i+1] + +gate_seq = Value.meta + meta: + name: 'gate-seq' + summary: "MIDI Gate-Sequencer." + examples: { '(launchctl/gate-seq port i start chan [steps])' } + description: " +returns `true` or `false` for the `i`-th note-button (MIDI-notes starting from +`start`). `steps` defaults to 8." + + value: class extends Op + new: => + super 'bool', false + @state = {} + + setup: (inputs) => + { port, i, start, chan, steps } = match 'midi/port num num num num?', inputs + + super + port: Input.io port + i: Input.value i + start: Input.value start + chan: Input.value chan + steps: Input.value steps or Value.num 8 + + light = (set, active) -> + set = if set then 'S' else ' ' + active = if active then 'A' else ' ' + color switch set .. active + when ' ' then 0, 0 + when ' A' then 1, 1 + when 'S ' then 1, 0 + when 'SA' then 3, 1 + + display: (i, active) => + { :port, :start, :chan } = @unwrap_all! + port\send 'note-on', chan, (start + i), light @state[i+1], active + + tick: => + { :port, :i, :start, :chan, :steps } = @inputs + + if steps\dirty! + while steps! > #@state + table.insert @state, false + while steps! < #@state + table.remove @state + + curr_i = i! % #@state + + if port\dirty! + for msg in port!\receive! + if msg.status == 'note-on' and msg.chan == chan! + rel_i = msg.a - start! + if rel_i >= 0 and rel_i < #@state + @state[rel_i+1] = not @state[rel_i+1] + @display rel_i, rel_i == curr_i + + if i\dirty! + prev_i = (curr_i - 1) % #@state + + @display curr_i, true + @display prev_i, false + + @out\set @state[curr_i+1] { 'gate-seq': gate_seq diff --git a/lib/osc.moon b/lib/osc.moon index 101fb11..fe4eae4 100644 --- a/lib/osc.moon +++ b/lib/osc.moon @@ -1,64 +1,74 @@ -import Op, Input, match from require 'core.base' +import Op, Value, Input, match from require 'core.base' import pack from require 'osc' import dns, udp from require 'socket' unpack or= table.unpack -class connect extends Op - @doc: "(osc/connect host port) - UDP remote definition" +connect = Value.meta + meta: + name: 'connect' + summary: "Create a UDP remote." + examples: { '(osc/connect host port)' } - new: => super 'udp/socket' + value: class extends Op + new: => super 'udp/socket' - setup: (inputs) => - { host, port } = match 'str num', inputs - super - host: Input.value host - port: Input.value port + setup: (inputs) => + { host, port } = match 'str num', inputs + super + host: Input.value host + port: Input.value port - tick: => - { :host, :port } = @unwrap_all! - ip = dns.toip host + tick: => + { :host, :port } = @unwrap_all! + ip = dns.toip host - @out\set with sock = udp! - \setpeername ip, port + @out\set with sock = udp! + \setpeername ip, port -class send extends Op - @doc: "(osc/send socket path val) - send a value via OSC +send = Value.meta + meta: + name: 'send' + summary: "Send a value via OSC." + examples: { '(osc/send socket path val)' } + description: "sends a message only when `val` is dirty." -sends a message only when val is dirty." + value: class extends Op + setup: (inputs) => + { socket, path, value } = match 'udp/socket str any', inputs + super + socket: Input.cold socket + path: Input.cold path + value: Input.value value - setup: (inputs) => - { socket, path, value } = match 'udp/socket str any', inputs - super - socket: Input.cold socket - path: Input.cold path - value: Input.value value + tick: => + if @inputs.value\dirty! + { :socket, :path, :value } = @unwrap_all! + msg = if 'table' == type value + pack path, unpack value + else + pack path, value + socket\send msg - tick: => - if @inputs.value\dirty! - { :socket, :path, :value } = @unwrap_all! - msg = if 'table' == type value - pack path, unpack value - else - pack path, value - socket\send msg - -class send_state extends Op - @doc: "(osc/send! socket path val) - synchronize a value via OSC +send_state = Value.meta + meta: + name: 'send' + summary: "Synchronize a value via OSC." + examples: { '(osc/send! socket path val)' } + description: "sends a message whenever any parameter is dirty." -sends a whenever any parameter changes." + value: class extends Op + setup: (inputs) => + { socket, path, value } = match 'udp/socket str any', inputs + super + socket: Input.value socket + path: Input.value path + value: Input.value value - setup: (inputs) => - { socket, path, value } = match 'udp/socket str any', inputs - super - socket: Input.value socket - path: Input.value path - value: Input.value value - - tick: => - { :socket, :path, :value } = @unwrap_all! - msg = pack path, value - socket\send msg + tick: => + { :socket, :path, :value } = @unwrap_all! + msg = pack path, value + socket\send msg { :connect diff --git a/lib/pilot.moon b/lib/pilot.moon index a687e48..b851440 100644 --- a/lib/pilot.moon +++ b/lib/pilot.moon @@ -1,4 +1,4 @@ -import Op, Input, Error, match from require 'core.base' +import Op, Value, Input, Error, match from require 'core.base' import udp from require 'socket' local conn @@ -20,53 +20,65 @@ send = (...) -> conn or= udp! conn\sendto str, '127.0.0.1', 49161 -class play extends Op - @doc: "(play trig ch oct note [vel [len]]) - play a note when trig is live" +play = Value.meta + meta: + name: 'play' + summary: "Play a note when a bang arrives." + examples: { '(pilot/play trig ch oct note [vel [len]])' } - setup: (inputs) => - { trig, args } = match 'bang *any', inputs - assert #args < 6, Error 'argument', "too many arguments!" - super - trig: Input.event trig - args: [Input.cold a for a in *args] + value: class extends Op + setup: (inputs) => + { trig, args } = match 'bang *any', inputs + assert #args < 6, Error 'argument', "too many arguments!" + super + trig: Input.event trig + args: [Input.cold a for a in *args] - tick: => - { :trig, :args } = @inputs - if trig\dirty! and trig! - send [a! for a in *@inputs.args] + tick: => + { :trig, :args } = @inputs + if trig\dirty! and trig! + send [a! for a in *@inputs.args] -class play_ extends Op - @doc: "(play! ch oct note [vel [len]]) - play a note when note is live" +play_ = Value.meta + meta: + name: 'play!' + summary: "Play a note when a note arrives." + examples: { '(pilot/play! ch oct note [vel [len]])' } - setup: (inputs) => - { chan, octv, note, args } = match 'any any any *any', inputs - assert #args < 3, Error 'argument', "too many arguments!" - super - chan: Input.cold chan - octv: Input.cold octv - note: Input.event note - args: [Input.cold a for a in *args] + value: class extends Op + setup: (inputs) => + { chan, octv, note, args } = match 'any any any *any', inputs + assert #args < 3, Error 'argument', "too many arguments!" + super + chan: Input.cold chan + octv: Input.cold octv + note: Input.event note + args: [Input.cold a for a in *args] - tick: => - if @inputs.note\dirty! - { :chan, :oct, :note, :args } = @unwrap_all! - send { chan, oct, note }, args + tick: => + if @inputs.note\dirty! + { :chan, :oct, :note, :args } = @unwrap_all! + send { chan, oct, note }, args -class effect extends Op - @doc: "(effect which a b) - set an effect +effect = Value.meta + meta: + name: 'effect' + summary: "Set effect parameters." + examples: { '(pilot/effect which a b)' } + description: "`effect` should be one of 'DIS', 'CHO', 'REV' or 'FEE'" -which is one of 'DIS', 'CHO', 'REV' or 'FEE'" + value: class extends Op + setup: (inputs) => + { which, a, b } = match 'str num num', inputs + super { + Input.cold which + Input.value a + Input.value b + } - setup: (inputs) => - { which, a, b } = match 'str num num', inputs - super { - Input.cold which - Input.value a - Input.value b - } + tick: => + send @unwrap_all! - tick: => - send @unwrap_all! { :play 'play!': play_ diff --git a/lib/random.moon b/lib/random.moon index b97671d..a3124fa 100644 --- a/lib/random.moon +++ b/lib/random.moon @@ -23,38 +23,20 @@ range can be one of: - (num) [ 0 - num[" class num extends Op - @doc: "(random/num [trigger] [range]) - create a random number - -generates a random value in range on create and trigger. +num = Value.meta + meta: + name: 'num' + summary: 'Generate a random number.' + examples: { '(random/num [trigger] [range]))' } + description: "generate a random value in `range` when created and on `trig`. #{range_doc}" - new: => - super 'num' - @gen! - - gen: => @state = { math.random! } - - setup: (inputs) => - { trig, range } = match 'bang? any?', inputs - super - trig: trig and Input.event trig - range: Input.value range or Value.str 'uni' - - tick: => - @gen! if @inputs.trig and @inputs.trig\dirty! - @out\set apply_range @inputs.range, @state[1] -vec_ = (n) -> - class vec extends Op - @doc: "(random/vec#{n} [trigger] [range]) - create a random number - -generates a random vec#{n} on create and trigger. -each component is in range. -#{range_doc}" + value: class extends Op new: => - super "vec#{n}" + super 'num' @gen! - gen: => @state = for i=1,n do math.random! + gen: => @state = { math.random! } setup: (inputs) => { trig, range } = match 'bang? any?', inputs @@ -64,10 +46,33 @@ each component is in range. tick: => @gen! if @inputs.trig and @inputs.trig\dirty! - @out\set [apply_range @inputs.range, v for v in *@state] + @out\set apply_range @inputs.range, @state[1] + +vec_ = (n) -> + Value.meta + meta: + name: "vec#{n}" + summary: 'Generate a random vector.' + examples: { '(random/vec#{n} [trigger] [range]))' } + description: "generate a random vec#{n} in `range` when created and on `trig`. +#{range_doc}" + + value: class extends Op + new: => + super "vec#{n}" + @gen! + + gen: => @state = for i=1,n do math.random! + + setup: (inputs) => + { trig, range } = match 'bang? any?', inputs + super + trig: trig and Input.event trig + range: Input.value range or Value.str 'uni' - vec.__name = "vec#{n}" - vec + tick: => + @gen! if @inputs.trig and @inputs.trig\dirty! + @out\set [apply_range @inputs.range, v for v in *@state] { :num diff --git a/lib/sc.moon b/lib/sc.moon index 7562620..dd9e826 100644 --- a/lib/sc.moon +++ b/lib/sc.moon @@ -1,30 +1,37 @@ -import Op, Input, Error, match from require 'core.base' +import Op, Value, Input, Error, match from require 'core.base' import pack from require 'osc' import dns, udp from require 'socket' -class play extends Op - @doc: "(sc/play socket synth trigger [name-str val]...) - play a SC SynthDef" +play = Value.meta + meta: + name: 'play' + summary: 'Play a SuperCollider SynthDef.' + examples: { '(play socket synth trig [param val…])' } + description: " +Plays the synth `synth` on the `udp/socket` `socket` whenever `trig` is live. +Any number of parameter-value pairs can be specified and are captured and sent +together with the note when triggered." + value: class extends Op + setup: (inputs) => + { socket, synth, trig, ctrls } = match 'udp/socket str bang *any?', inputs - setup: (inputs) => - { socket, synth, trig, ctrls } = match 'udp/socket str bang *any?', inputs + assert #ctrls % 2 == 0, Error 'argument', "parameters need to be specified as pairs" + for key in *ctrls[1,,2] + assert key\type! == 'str', Error 'argument', "ony strings are supported as control names" + for val in *ctrls[2,,2] + assert val\type! == 'num', Error 'argument', "only numbers are supported as control values" - assert #ctrls % 2 == 0, Error 'argument', "parameters need to be specified as pairs" - for key in *ctrls[1,,2] - assert key\type! == 'str', Error 'argument', "ony strings are supported as control names" - for val in *ctrls[2,,2] - assert val\type! == 'num', Error 'argument', "only numbers are supported as control values" + super + trig: Input.event trig + socket: Input.cold socket + synth: Input.cold synth + ctrls: [Input.cold v for v in *ctrls] - super - trig: Input.event trig - socket: Input.cold socket - synth: Input.cold synth - ctrls: [Input.cold v for v in *ctrls] - - tick: => - if @inputs.trig\dirty! and @inputs.trig! - { :socket, :synth, :ctrls } = @unwrap_all! - msg = pack '/s_new', synth, -1, 0, 1, unpack ctrls - socket\send msg + tick: => + if @inputs.trig\dirty! and @inputs.trig! + { :socket, :synth, :ctrls } = @unwrap_all! + msg = pack '/s_new', synth, -1, 0, 1, unpack ctrls + socket\send msg { :play diff --git a/lib/string.moon b/lib/string.moon index 06509f3..8b48977 100644 --- a/lib/string.moon +++ b/lib/string.moon @@ -1,12 +1,15 @@ -import Op, Input from require 'core.base' +import Op, Value, Input from require 'core.base' -class str extends Op - @doc: "(str v1 [v2]...) -(.. v1 [v2]...) - concatenate/stringify values" - new: => super 'str' +str = Value.meta + meta: + name: 'str' + summary: "Concatenate/stringify values." + examples: { '(.. v1 [v2…])', '(str v1 [v2…])' } + value: class extends Op + new: => super 'str' - setup: (inputs) => super [Input.value v for v in *inputs] - tick: => @out\set table.concat [tostring v! for v in *@inputs] + setup: (inputs) => super [Input.value v for v in *inputs] + tick: => @out\set table.concat [tostring v! for v in *@inputs] { :str, '..': str diff --git a/lib/time.moon b/lib/time.moon index 8e9aded..182cf4d 100644 --- a/lib/time.moon +++ b/lib/time.moon @@ -1,5 +1,4 @@ -import Registry, Value, IO, Op, Input, match - from require 'core.base' +import Registry, Value, IO, Op, Input, match from require 'core.base' import monotime from require 'system' class Clock extends IO @@ -20,131 +19,148 @@ class Clock extends IO dirty: => @is_dirty -class clock extends Op - @doc: "(clock [fps]) - a clock source - +clock = Value.meta + meta: + name: 'clock' + summary: "Create a clock source." + examples: { '(clock)', '(clock fps)' } + description: " IO that triggers other operators at a fixed frame rate. -fps defaults to 60 and has to be an eval-time constant" - - new: => super 'clock' - - setup: (inputs) => - { fps } = match 'num?', inputs - super fps: Input.value fps or Value.num 60 - - tick: => - if @inputs.fps\dirty! - @out\set Clock 1 / @inputs.fps! - -class lfo extends Op - @doc: "(lfo [clock] freq [wave]) - low-frequency oscillator - +`fps` defaults to 60 and has to be an eval-time constant" + value: class extends Op + new: => super 'clock' + + setup: (inputs) => + { fps } = match 'num?', inputs + super fps: Input.value fps or Value.num 60 + + tick: => + if @inputs.fps\dirty! + @out\set Clock 1 / @inputs.fps! + +lfo = Value.meta + meta: + name: 'lfo' + summary: "Low-frequency oscillator." + examples: { '(lfo [clock] freq wave)' } + description: " oscillates between 0 and 1 at the frequency freq. wave selects the wave shape from the following: -- sin (default) -- saw -- tri" - - new: => - super 'num' - @state.phase or= 0 - - default_wave = Value.str 'sin' - setup: (inputs, scope) => - { clock, freq, wave } = match 'clock? num any?', inputs - super - clock: Input.io clock or scope\get '*clock*' - freq: Input.value freq - wave: Input.value wave or default_wave - - tau = math.pi * 2 - tick: => - if @inputs.clock\dirty! - { :clock, :freq, :wave } = @unwrap_all! - - @state.phase += clock.dt * freq - @out\set switch wave - when 'sin' then .5 + .5 * math.cos @state.phase * tau - when 'saw' then @state.phase % 1 - when 'tri' then math.abs (2*@state.phase % 2) - 1 - else error "unknown wave type" - -class ramp extends Op - @doc: "(ramp [clock] period [max]) - sawtooth lfo - +- `'sin'` (default) +- `'saw'` +- `'tri'`" + value: class extends Op + new: => + super 'num' + @state.phase or= 0 + + default_wave = Value.str 'sin' + setup: (inputs, scope) => + { clock, freq, wave } = match 'clock? num any?', inputs + super + clock: Input.io clock or scope\get '*clock*' + freq: Input.value freq + wave: Input.value wave or default_wave + + tau = math.pi * 2 + tick: => + if @inputs.clock\dirty! + { :clock, :freq, :wave } = @unwrap_all! + + @state.phase += clock.dt * freq + @out\set switch wave + when 'sin' then .5 + .5 * math.cos @state.phase * tau + when 'saw' then @state.phase % 1 + when 'tri' then math.abs (2*@state.phase % 2) - 1 + else error "unknown wave type" + +ramp = Value.meta + meta: + name: 'ramp' + summary: "Sawtooth LFO." + examples: { '(ramp [clock] period [max])' } + description: " ramps from 0 to max (default same as ramp) once every period seconds." - - new: => - super 'num' - @state.phase or= 0 - - setup: (inputs, scope) => - { clock, period, max } = match 'clock? num num?', inputs - super - clock: Input.io clock or scope\get '*clock*' - period: Input.value period - max: max and Input.value max - - tick: => - clock_dirty = @inputs.clock\dirty! - if clock_dirty - { :clock, :period, :max } = @unwrap_all! - max or= period - @state.phase += clock.dt / period - - while @state.phase >= 1 - @state.phase -= 1 - - if clock_dirty or (@inputs.max and @inputs.max\dirty!) - @out\set @state.phase * max - -class tick extends Op - @doc: "(tick [clock] period) - count ticks - -counts upwards by one every period seconds and returns the number of completed ticks." - new: => - super 'num', 0 - @state.phase or= 0 - @state.count or= 0 - - setup: (inputs, scope) => - { clock, period } = match 'clock? num', inputs - super - clock: Input.io clock or scope\get '*clock*' - period: Input.value period - - tick: => - if @inputs.clock\dirty! - { :clock, :period, :max } = @unwrap_all! - @state.phase += clock.dt / period - - while @state.phase >= 1 - @state.phase -= 1 - @state.count += 1 - @out\set @state.count - -class every extends Op - @doc: "(every [clock] period) - trigger every period seconds - -returns true once every period seconds." - new: => - super 'bang' - @state.phase or= 0 - - setup: (inputs, scope) => - { clock, period } = match 'clock? num', inputs - super - clock: Input.io clock or scope\get '*clock*' - period: Input.value period - - tick: => - if @inputs.clock\dirty! - { :clock, :period, :max } = @unwrap_all! - @state.phase += clock.dt / period - - while @state.phase >= 1 - @state.phase -= 1 - @out\set true + value: class extends Op + new: => + super 'num' + @state.phase or= 0 + + setup: (inputs, scope) => + { clock, period, max } = match 'clock? num num?', inputs + super + clock: Input.io clock or scope\get '*clock*' + period: Input.value period + max: max and Input.value max + + tick: => + clock_dirty = @inputs.clock\dirty! + if clock_dirty + { :clock, :period, :max } = @unwrap_all! + max or= period + @state.phase += clock.dt / period + + while @state.phase >= 1 + @state.phase -= 1 + + if clock_dirty or (@inputs.max and @inputs.max\dirty!) + @out\set @state.phase * max + +tick = Value.meta + meta: + name: 'tick' + summary: "Count ticks." + examples: { '(tick [clock] period)' } + description: " +counts upwards by one every period seconds and returns the number of completed +ticks." + value: class extends Op + new: => + super 'num', 0 + @state.phase or= 0 + @state.count or= 0 + + setup: (inputs, scope) => + { clock, period } = match 'clock? num', inputs + super + clock: Input.io clock or scope\get '*clock*' + period: Input.value period + + tick: => + if @inputs.clock\dirty! + { :clock, :period, :max } = @unwrap_all! + @state.phase += clock.dt / period + + while @state.phase >= 1 + @state.phase -= 1 + @state.count += 1 + @out\set @state.count + +every = Value.meta + meta: + name: 'every' + summary: "Emit bangs." + examples: { '(every [clock] period)' } + description: "returns true once every period seconds." + value: class extends Op + new: => + super 'bang' + @state.phase or= 0 + + setup: (inputs, scope) => + { clock, period } = match 'clock? num', inputs + super + clock: Input.io clock or scope\get '*clock*' + period: Input.value period + + tick: => + if @inputs.clock\dirty! + { :clock, :period, :max } = @unwrap_all! + @state.phase += clock.dt / period + + while @state.phase >= 1 + @state.phase -= 1 + @out\set true { :clock @@ -152,5 +168,8 @@ returns true once every period seconds." :ramp :tick :every - '*clock*': Value 'clock', Clock 1/60 + '*clock*': with Value 'clock', Clock 1/60 + .meta = + name: '*clock*' + summary: 'Default clock source (60fps).' } diff --git a/lib/util.moon b/lib/util.moon index f816eee..a2c65ba 100644 --- a/lib/util.moon +++ b/lib/util.moon @@ -7,106 +7,100 @@ all_same = (list) -> list[1] -class switch_ extends Op - @doc: "(switch i v0 [v1 v2...]) - switch between multiple inputs - -when i is true, the first value is reproduced. -when i is false, the second value is reproduced. -when i is a num, it is (floor)ed and the matching argument (starting from 0) is reproduced." - - setup: (inputs) => - { i, values } = match 'any *any', inputs - - i_type = i\type! - assert i_type == 'bool' or i_type == 'num', Error 'argument', "i has to be bool or num" - typ = all_same [v\type! for v in *values] - @out = Value typ if not @out or typ != @out.type - - super - i: Input.value i - values: [Input.value v for v in *values] - - tick: => - { :i, :values } = @inputs - active = switch i! - when true - values[1] - when false - values[2] - else - i = 1 + (math.floor i!) % #values - values[i] - @out\set active and active! - -class route extends Op - @doc: "(route i v0 [v1 v2...]) - route between multiple inputs - -when i is true, the first value is reproduced. -when i is false, the second value is reproduced. -when i is a num, it is (floor)ed and the matching argument (starting from 0) is reproduced." - - setup: (inputs) => - { i, values } = match 'any *any', inputs - - i_type = i\type! - assert i_type == 'bool' or i_type == 'num', Error 'argument', "i has to be bool or num" - typ = all_same [v\type! for v in *values] - @out = Value typ if not @out or typ != @out.type - - super - i: Input.value i - values: [Input.value v for v in *values] - - tick: => - { :i, :values } = @inputs - active = switch i! - when true - values[1] - when false - values[2] - else - i = 1 + (math.floor i!) % #values - values[i] - if active and active\dirty! - @out\set active! - -class edge extends Op - @doc: "(edge bool) - convert rising edges to bangs" - new: => super 'bang' - - setup: (inputs) => - { value } = match 'bool', inputs - super value: Input.value value - - tick: => - now = @inputs.value! - if now and not @state.last - @out\set true - @state.last = now - -class default extends Op - @doc: "(default stream default) - provide a default value for an event stream - -starts out as default but forwards events from stream. -default defaults to zero." - - setup: (params) => - { value, init } = match 'any any', inputs - super - value: Input.event value - init: Input.cold init - - @out = Value value\type! - @out\set @inputs.init\unwrap! - - tick: => - { :value } = @inputs - if value\dirty! - @out\set value! +switch_ = Value.meta + meta: + name: 'switch' + summary: "Switch between multiple inputs." + examples: { '(switch i v0 [v1 v2…])' } + description: " +- when `i` is `true`, the first value is reproduced. +- when `i` is `false`, the second value is reproduced. +- when `i` is a `num`, it is [math/floor][]ed and the matching argument + (indexed starting from 0) is reproduced." + + value: class extends Op + setup: (inputs) => + { i, values } = match 'any *any', inputs + + i_type = i\type! + assert i_type == 'bool' or i_type == 'num', Error 'argument', "i has to be bool or num" + typ = all_same [v\type! for v in *values] + @out = Value typ if not @out or typ != @out.type + + super + i: Input.value i + values: [Input.value v for v in *values] + + tick: => + { :i, :values } = @inputs + active = switch i! + when true + values[1] + when false + values[2] + else + i = 1 + (math.floor i!) % #values + values[i] + @out\set active and active! + +route = Value.meta + meta: + name: 'route' + summary: "Route between multiple inputs." + examples: { '(route i v0 [e1 e2…])' } + description: " +- when `i` is `true`, the first event stream is reproduced. +- when `i` is `false`, the second event stream is reproduced. +- when `i` is a `num`, it is [math/floor][]ed and the matching argument + (indexed starting from 0) is reproduced." + + value: class extends Op + setup: (inputs) => + { i, values } = match 'any *any', inputs + + i_type = i\type! + assert i_type == 'bool' or i_type == 'num', Error 'argument', "i has to be bool or num" + typ = all_same [v\type! for v in *values] + @out = Value typ if not @out or typ != @out.type + + super + i: Input.value i + values: [Input.value v for v in *values] + + tick: => + { :i, :values } = @inputs + active = switch i! + when true + values[1] + when false + values[2] + else + i = 1 + (math.floor i!) % #values + values[i] + if active and active\dirty! + @out\set active! + +route = Value.meta + meta: + name: 'edge' + summary: "Convert rising edges to bangs." + examples: { '(edge bool)' } + + value: class extends Op + new: => super 'bang' + + setup: (inputs) => + { value } = match 'bool', inputs + super value: Input.value value + + tick: => + now = @inputs.value! + if now and not @state.last + @out\set true + @state.last = now { 'switch': switch_ :route :edge - :default } -- cgit v1.2.3