diff options
| author | s-ol <s-ol@users.noreply.github.com> | 2020-04-13 18:40:35 +0000 |
|---|---|---|
| committer | s-ol <s-ol@users.noreply.github.com> | 2020-04-14 08:46:54 +0000 |
| commit | 1a8debe87762072b3a63b769aa515ebf63b4c70d (patch) | |
| tree | 33b42866307d11d2c0878b23e0ec0bb8925d3fcc /alv/base | |
| parent | spec base.match __tostring (diff) | |
| download | alive-1a8debe87762072b3a63b769aa515ebf63b4c70d.tar.gz alive-1a8debe87762072b3a63b769aa515ebf63b4c70d.zip | |
move into proper Lua module (`alv`)
Diffstat (limited to 'alv/base')
| -rw-r--r-- | alv/base/builtin.moon | 96 | ||||
| -rw-r--r-- | alv/base/fndef.moon | 26 | ||||
| -rw-r--r-- | alv/base/init.moon | 39 | ||||
| -rw-r--r-- | alv/base/input.moon | 146 | ||||
| -rw-r--r-- | alv/base/match.moon | 302 | ||||
| -rw-r--r-- | alv/base/op.moon | 168 |
6 files changed, 777 insertions, 0 deletions
diff --git a/alv/base/builtin.moon b/alv/base/builtin.moon new file mode 100644 index 0000000..fd36c69 --- /dev/null +++ b/alv/base/builtin.moon @@ -0,0 +1,96 @@ +---- +-- Builtin / Special Form evaluation Strategy (`builtin`). +-- +-- Responsible for quoting/evaluating subexpressions, instantiating and setting +-- up `Op`s, updating the current `Scope`, etc. +-- See `builtin` and `invoke` for examples. +-- +-- @classmod Builtin + +class Builtin +--- Builtin interface. +-- +-- methods that have to be implemented by `Builtin` implementations. +-- @section interface + + --- create a new instance. + -- + -- @tparam Cell cell the Cell to evaluate + -- @tparam Value head the (`AST:eval`d) `head` of the Cell to evaluate + new: (@cell, @head) => + @tag = @cell.tag + @tag\register @ + + --- perform the actual evaluation. + -- + -- Implementations should: + -- + -- - eval or quote `tail` values + -- - perform scope effects + -- - wrap all child-results + -- + -- @tparam Scope scope the active scope + -- @tparam {AST,...} tail the arguments to this expression + -- @treturn Result the result of this evaluation + eval: (scope, tail) => error "not implemented" + + --- free resources + destroy: => + + --- setup or copy state from previous instance of same type. + -- + -- `prev` is only passed if Builtin types of prev and current expression match. + -- Otherwise, or when no previous expression exists, `nil` is passed. + -- + -- @tparam ?Builtin prev the previous Builtin instance + setup: (prev) => + + --- the `Cell` this Builtin was created for. + -- @tfield Cell cell + + --- the evaluated head of `cell`. + -- @tfield AST head + + --- the identity of `cell`. + -- @tfield Tag tag + +--- static functions +-- @section static + + --- create and setup a `Builtin` for a given tag, then evaluate it. + -- + -- Create a new instance using `tag` and `head` and call `setup` on it. + -- If a previous instance with the same `tag` exists and has the same `head`, + -- it pass it to `setup`. Register the `Builtin` with `tag`, evaluate it + -- and return the `Result`. + -- + -- @tparam Cell cell the `Cell` being evaluated + -- @tparam Scope scope the active scope + -- @tparam Value head the (`AST:eval`d) head of the `Cell` being evaluated + -- @treturn Result the result of evaluation + @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 #{cell.tag} <#{@__name} #{head}>" + else if last + "replacing #{last} with new #{cell.tag} <#{@__name} #{head}>" + else + "initializing #{cell.tag} <#{@__name} #{head}>" + + builtin = @ cell, head + if compatible + builtin\setup last + else + last\destroy! if last + builtin\setup nil + + builtin\eval scope, cell\tail! + + __tostring: => "<#{@@__name} #{@head}>" + __inherited: (cls) => cls.__base.__tostring = @__tostring + +{ + :Builtin +} diff --git a/alv/base/fndef.moon b/alv/base/fndef.moon new file mode 100644 index 0000000..b79ad85 --- /dev/null +++ b/alv/base/fndef.moon @@ -0,0 +1,26 @@ +---- +-- user-function definition (`fndef`). +-- +-- When called, expands to its body with params bound to the fn arguments (see +-- `invoke.fn_invoke`). +-- +-- @classmod FnDef + +class FnDef +--- static functions +-- @section static + + --- create a new instance + -- + -- @classmethod + -- @tparam {Value,...} params (`AST:quote`d) naming the function parameters + -- @tparam AST body (`AST:quote`d) expression the function evaluates to + -- @tparam Scope scope the lexical scope the function was defined in (closure) + new: (@params, @body, @scope) => + + __tostring: => + "(fn (#{table.concat [p\stringify! for p in *@params], ' '}) ...)" + +{ + :FnDef +} diff --git a/alv/base/init.moon b/alv/base/init.moon new file mode 100644 index 0000000..c37d85c --- /dev/null +++ b/alv/base/init.moon @@ -0,0 +1,39 @@ +---- +-- Base definitions for extensions. +-- +-- This module exports the following classes and tables that extension modules +-- may need: +-- +-- @module base +-- @see Op +-- @see Builtin +-- @see FnDef +-- @see Input +-- @see base.match.val +-- @see base.match.evt +-- @see ValueStream +-- @see EventStream +-- @see IOStream +-- @see Result +-- @see Error + +import Op from require 'alv.base.op' +import Builtin from require 'alv.base.builtin' +import FnDef from require 'alv.base.fndef' +import Input from require 'alv.base.input' +import val, evt from require 'alv.base.match' +import ValueStream, EventStream, IOStream from require 'alv.stream' +import Result from require 'alv.result' +import Error from require 'alv.error' + +{ + :Op + :Builtin + :FnDef + :Input + :val, :evt + + -- redundant exports, to keep anything an extension might need in one import + :ValueStream, :EventStream, :IOStream + :Result, :Error +} diff --git a/alv/base/input.moon b/alv/base/input.moon new file mode 100644 index 0000000..19e55a4 --- /dev/null +++ b/alv/base/input.moon @@ -0,0 +1,146 @@ +---- +-- Update scheduling policy for `Op` arguments. +-- +-- @classmod Input +import ValueStream, EventStream, IOStream from require 'alv.stream' +import Result from require 'alv.result' + +inherits = (klass, frm) -> + assert klass, "cant find the ancestor of nil" + return true if klass == frm + while klass.__parent + return true if klass.__parent == frm + klass = klass.__parent + false + +match_parent = (inst, map) -> + klass = assert inst and inst.__class, "not an instance" + if key = map[klass] + return key + + while klass.__parent + if key = map[klass.__parent] + return key + + klass = klass.__parent + +local ColdInput, ValueInput, IOInput, mapping + +class Input +--- Input interface. +-- +-- Methods that have to be implemented by `Input` implementations. +-- @section interface + + --- create a new Input. + -- + -- @classmethod + -- @tparam Stream stream + new: (@stream) => + assert @stream, "nil passed to Input: #{value}" + + --- copy state from old instance (optional). + -- + -- called by `Op:setup` with another `Input` instance or `nil` once this instance is + -- registered. Must prepare this instance for `dirty`. + -- + -- May enter a 'setup state' that is exited using `finish_setup`. + -- + -- @tparam ?Input prev previous `Input` intance or nil + setup: (prev) => + + --- whether this input requires processing (optional). + -- + -- must return a boolean indicating whether `Op`s that refer to this instance + -- should be notified (via `Op:tick`). If not overwritten, delegates to + -- `stream`:@{ValueStream:dirty|dirty}. + -- + -- @treturn bool whether processing is necessary + dirty: => @stream\dirty! + + --- leave setup state (optional). + -- + -- called after the `Op` has completed (or skipped) its first `Op:tick` after + -- `Op:setup`. Must prepare this instance for dataflow operation. + finish_setup: => + + --- unwrap to Lua value (optional). + -- + -- @treturn any the raw Lua value + unwrap: => @stream\unwrap! + + --- return the type name of this `Input` (optional). + type: => @stream.type + + --- return the metatype name of this `Input` (optional). + metatype: => @stream.metatype + + --- the current value + -- + -- @tfield ValueStream stream + +--- members +-- @section members + + --- alias for `unwrap`. + __call: => @stream\unwrap! + + __tostring: => "#{@@__name}:#{@stream}" + __inherited: (cls) => + cls.__base.__call = @__call + cls.__base.__tostring = @__tostring + +--- static functions +-- @section static + + --- Create a `cold` `Input`. + -- + -- Never marked dirty. Use this for input streams that are only read when + -- another `Input` is dirty. + -- + -- @tparam Stream|Result value + @cold: (value) -> + if value.__class == Result + value = assert value.value, "Input from result without value!" + ColdInput value + + --- Create a `hot` `Input`. + -- + -- Behaviour depends on what kind of `Stream` `value` is: + -- + -- - `ValueStream`: Marked dirty for the eval-tick if old and new `ValueStream` differ. + -- - `EventStream` and `IOStream`: Marked dirty only if the current `EventStream` is dirty. + -- + -- This is the most common `Input` strategy. + -- + -- @tparam Stream|Result value + @hot: (value) -> + if value.__class == Result + value = assert value.value, "Input from result without value!" + + InputType = match_parent value, mapping + assert InputType, "Input from unknown value: #{value}" + InputType value + +class ColdInput extends Input + dirty: => false + +class ValueInput extends Input + setup: (old) => @dirty_setup = not old or @stream != old.stream + finish_setup: => @dirty_setup = nil + dirty: => + return @dirty_setup if @dirty_setup != nil + @stream\dirty! + +class IOInput extends Input + io: true + +mapping = { + [ValueStream]: ValueInput + [EventStream]: Input + [IOStream]: IOInput +} + +{ + :Input +} diff --git a/alv/base/match.moon b/alv/base/match.moon new file mode 100644 index 0000000..62075c0 --- /dev/null +++ b/alv/base/match.moon @@ -0,0 +1,302 @@ +----- +--- Pattern capturing for Op argument parsing. +-- +-- There is only one basic buildings block for assembling patterns: +-- `Type`. It can match `ValueStream`s and `EventStream`s depending on its +-- metatype argument and can take an optional type name to match as an argument. +-- +-- In addition to this primitive, the following modifiers are available: +-- `Repeat`, `Sequence`, `Choice`, and `Optional`. They can be used directly, +-- but there is also a number of shorthands for assembling patterns quickly: +-- +-- - `val()` and `evt()`: Shorthands for `Type('value')` and `Type('event')` +-- - `val.num`: Shorthand for `Type('value', 'num')` +-- - `evt.str`: Shorthand for `Type('event', 'str')` +-- - `pat * 2`: Shorthand for `Repeat(pat, 1, 2)` (1-4 times `pat`) +-- - `pat * 0`: Shorthand for `Repeat(pat, 1, nil)` (1-* times `pat`) +-- - `pat ^ 2`: Shorthand for `Repeat(pat, 0, 2)` (0-4 times `pat`) +-- - `pat ^ 0`: Shorthand for `Repeat(pat, 0, nil)` (0-* times `pat`) +-- - `a + b + … + z`: Shorthand for `Sequence{ a, b, ..., z }` +-- - `a / b / … / z`: Shorthand for `Choice{ a, b, ..., z }` +-- - `-pat`: Shorthand for `Optional(pat)` +-- +-- To perform the actual matching, call the `:match` method on a pattern and +-- pass a sequence of `Result`s. The method will either return the captured +-- `Result`s (or a table structuring them) +-- +-- Any ambiguous pattern can be set to 'recall mode' by invoking it. +-- Recalling patterns will memorize the first Result they match, and +-- only match further Results of the same type. For example +-- +-- arg = (val.num / val.str)! +-- pattern = arg + arg +-- +-- ...will match either two numbers or two strings, but not one number and one +-- string. Recalling works on `Choice` and `Type` patterns. `Type` patterns +-- without a type (`val!` and `evt!`) always behave like this. +-- +-- On `Sequence` patterns, a special method `:named` exists. It takes a +-- sequence of keys that are used instead of integers when constructing the +-- capture table: +-- +-- pattern = (val.str + val.num):named('key', 'value') +-- pattern:match(...) +-- -- returns { {key='a', value=1}, {key='b', value=2}, ...} +-- +-- @module base.match +import Error from require 'alv.error' +import ValueStream, EventStream from require 'alv.stream' + +local Repeat, Sequence, Choice, Optional + +typestr = (result) -> + str = result\type! + str ..= '!' if result\metatype! == 'event' + str + +class Pattern + match: (seq) => + @reset! + num, cap = @capture seq, 1 + if num != #seq + args = table.concat [typestr arg for arg in *seq], ' ' + msg = "couldn't match arguments (#{args}) against pattern #{@}" + error Error 'argument', msg + cap + + remember: (key) => + return true unless @recall + + @recalled or= key + @recalled == key + + rep: (min, max) => Repeat @, min, max + + reset: => @recalled = nil + + __call: => @ + __mul: (num) => Repeat @, 1, if num != 0 then num + __pow: (num) => Repeat @, 0, if num != 0 then num + __add: (other) => Sequence { @, other } + __div: (other) => Choice { @, other } + __unm: => Optional @ + + __inherited: (cls) => + cls.__base.__call or= @__call + cls.__base.__mul or= @__mul + cls.__base.__pow or= @__pow + cls.__base.__add or= @__add + cls.__base.__div or= @__div + cls.__base.__unm or= @__unm + +--- Base Stream Pattern. +-- +-- When instantiated with `type`, only succeeds for `Stream`s whose value and +-- meta types match. +-- +-- Otherwise, matches Streams based only on `metatype` for the first match, but +-- using both afterwards (recall mode). +-- +-- @function Stream +-- @tparam string metatype "value" or "event" +-- @tparam ?string type type name +class Type extends Pattern + new: (@metatype, @type) => + @recall = not @type + + capture: (seq, i) => + return unless seq[i] + type, mt = seq[i]\type!, seq[i]\metatype! + return unless @metatype == mt + match = if @type then type == @type else @remember type + if match + 1, seq[i] + + __tostring: => + str = @type or @metatype + str ..= '!' if @metatype == 'event' + str + +--- Repeat a pattern. +-- +-- Matches a given `inner` pattern as many times as possible, within the given +-- minimum/maximum counts. Matching this pattern results in a sequence of the +-- individual captures produced by the inner pattern. +-- +-- @function Repeat +-- @tparam Pattern inner the original pattern +-- @tparam ?number min minimum amount of repetitions +-- @tparam ?number max maximum amount of repetitions (default infinite) +class Repeat extends Pattern + new: (@inner, @min, @max) => + + capture: (seq, i) => + take, all = 0, {} + while true + num, cap = @inner\capture seq, i+take + break unless num + + take += num + table.insert all, cap + + break if @max and take >= @max + + return if @min and take < @min + return if @max and take > @max + + take, all + + reset: => + @inner\reset! + + __call: => + @@ @inner!, @min, @max + + __tostring: => + min = @min or '0' + max = @max or '*' + "#{@inner}{#{min}-#{max}}" + +--- Match multiple patterns in order. +-- +-- Matches the inner patterns in order, only succeeds if all of them match. +-- Captures the individual captures produced by the inner patterns in a +-- sequence, or table with keys specified in `keys` or using the `:named(...)` +-- modifier. +-- +-- @function Sequence +-- @tparam {Pattern,...} elements the inner patterns +-- @tparam ?{string,...} keys the keys to use when capturing matches +class Sequence extends Pattern + new: (@elements, @keys) => + + capture: (seq, i) => + take, all = 0, {} + for key, elem in ipairs @elements + num, cap = elem\capture seq, i+take + return unless num + + take += num + key = @keys[key] if @keys + all[key] = cap + + take, all + + reset: => + for elem in *@elements + elem\reset! + + named: (...) => + @@ [e for e in *@elements], { ... } + + __call: => + @@ [e! for e in *@elements] + + __add: (other) => + elements = [e for e in *@elements] + table.insert elements, other + @@ elements + + __tostring: => + core = table.concat [tostring e for e in *@elements], ' ' + "(#{core})" + +--- Match one of multiple options. +-- +-- Matches using the first matching pattern in `elements` and returns its +-- captured value. Supports recalling the matched subpattern. +-- +-- @function Choice +-- @tparam {Pattern,...} elements the inner patterns +-- @tparam ?{string,...} keys the keys to use when capturing matches +class Choice extends Pattern + new: (@elements, @recall=false) => + + capture: (seq, i) => + for key, elem in ipairs @elements + num, cap = elem\capture seq, i + if num and @remember key + return num, cap + + reset: => + super! + for elem in *@elements + elem\reset! + + __call: => + @@ [e! for e in *@elements], true + + __div: (other) => + elements = [e for e in *@elements] + table.insert elements, other + @@ elements + + __tostring: => + core = table.concat [tostring e for e in *@elements], ' | ' + "(#{core})" + +--- Optionally match a pattern. +-- +-- Matches using the first matching pattern in `elements` and returns its +-- captured value. Supports recalling the matched subpattern. +-- +-- @function Optional +-- @tparam {Pattern,...} elements the inner patterns +-- @tparam ?{string,...} keys the keys to use when capturing matches +class Optional extends Pattern + new: (@inner) => + + capture: (seq, i) => + num, cap = @inner\capture seq, i + num or 0, cap + + reset: => + @inner\reset! + + __call: => + @@ @inner! + + __unm: => @ + + __tostring: => "#{@inner}?" + +--- `Value` shorthands. +-- +-- Call or index with a string to obtain a `Type` instance. +-- Call to obtain a wildcard pattern. +-- +-- val.str, val.num +-- val['vec3'], val('vec3') +-- val() +-- +-- @table val +val = setmetatable {}, { + __index: (key) => + with v = Type 'value', key + @[key] = v + + __call: (...) => Type 'value', ... +} + +--- `Event` shorthands. +-- +-- Call or index with a string to obtain an `Type` instance. +-- Call to obtain a wildcard pattern. +-- +-- evt.bang, evt.str, evt.num +-- evt['midi/message'], evt('midi/message') +-- evt() +-- +-- @table evt +evt = setmetatable {}, { + __index: (key) => + with v = Type 'event', key + @[key] = v + + __call: (...) => Type 'event', ... +} + +{ + :Type, :Repeat, :Sequence, :Choice, :Optional + :val, :evt +} diff --git a/alv/base/op.moon b/alv/base/op.moon new file mode 100644 index 0000000..b0a83dc --- /dev/null +++ b/alv/base/op.moon @@ -0,0 +1,168 @@ +---- +-- Persistent expression Operator. +-- +-- @classmod Op + +deepcopy = (val) -> + switch type val + when 'number', 'string', 'boolean', 'nil' + val + when 'table' + assert (not getmetatable {}), "state should only contain simple tables!" + {(deepcopy k), (deepcopy v) for k,v in pairs val} + else + error "state cannot contain values of type '#{type val}'" + +class Op +--- members +-- @section members + + do_yield = (table) -> + for k, v in pairs table + if v.__class + coroutine.yield v + else + do_yield v + --- yield all `Input`s from the (potentially nested) `inputs` table + -- + -- @treturn iterator iterator over `inputs` + all_inputs: => coroutine.wrap -> do_yield @inputs + + --- create a mutable copy of this Op. + -- + -- Used to wrap insulate eval-cycles from each other. The copy does not have + -- `inputs` set, since it is expected that this is (re)set in `setup`. + -- + -- @treturn Op + fork: => + out = if @out then @out\fork! + state = if @state then deepcopy @state + @@ out, state + + --- internal state of this Op. + -- + -- This may be any simple Lua value, including Lua tables, as long as it has + -- no metatables, multiple references/loops, userdata etc. + -- + -- @tfield table state + + --- `Stream` instance representing this Op's computed output value. + -- + -- Must be set to a `Stream` instance once `setup` finishes. Must not change + -- type, be removed or replaced outside of `new` and `setup`. If it is a + -- `ValueStream`, it should have a value assigned via `set` or the + -- constructor once `tick` is called the first time. If `out`'s value is not + -- initialized in `new` or `setup`, the implementation must make sure + -- `tick``(true)` is called at least on the first eval-cycle the Op goes + -- through, e.g. by using an `Input.hot` with a `ValueStream`. + -- + -- @tfield Stream out + + --- table containing `Input`s to this Op. + -- + -- The `inputs` table can be nested with string or integer keys, + -- but all leaf-entries must be `Input` instances. It must not contain loops + -- or instances of other classes. + -- + -- @tfield {Input,...} inputs + +--- Op interface. +-- +-- methods that have to be implemented by `Op` implementations. +-- @section interface + + --- construct a new instance. + -- + -- The optional parameters `out` and `state` are used by `fork` to duplicate + -- an instance. If the constructor is overriden, these parameters must be + -- forwarded to the superconstructor unchanged. + -- + -- @function new + -- @classmethod + -- @tparam ?Stream out `out` + -- @tparam ?table state `state` + + --- parse arguments and patch self. + -- + -- Called once every eval-cycle. `inputs` is a list of `Result`s that are the + -- argument to this op. The `inputs` have to be wrapped in `Input` instances + -- to define update behaviour. Use `base.match` to parse them, then delegate to + -- `super:setup` to patch the `Input` instances. + -- + -- @function setup + -- @tparam {Result,...} inputs a sequence of `Result`s + -- @tparam Scope scope the active scope + + --- handle incoming events and update `out` (optional). + -- + -- Called once per frame if any `Input`s are dirty. Some `Input`s may have + -- special behaviour immediately after `setup` that can cause them to become + -- dirty at eval-time. In this case, an eval-time tick is executed. You can + -- detect this using the `setup` parameter. + -- + -- `tick` is called after `setup`. `tick` is not called immediately after + -- `setup` if no `inputs` are dirty. Update `out` here. + -- + -- @tparam bool setup whether this is an eval-time tick + tick: => + + --- called when the Op is destroyed (optional). + destroy: => + +--- implementation utilities. +-- +-- super-methods and utilities for use by implementations. +-- @section super + + --- if `type` is passed, an output stream is instantiated. + -- if `init` is passed, the stream is initialized to that Lua value. + -- it is okay not to use this and create the output stream in :setup() if the + -- type is not known at this time. + -- + -- @classmethod + -- @tparam ?Stream out `out` + -- @tparam ?table state `state` + new: (@out, @state) => + + do_setup = (old, cur) -> + for k, cur_val in pairs cur + old_val = old and old[k] + + -- are these inputs or nested tables? + cur_plain = cur_val and not cur_val.__class + old_plain = old_val and not old_val.__class + + if cur_plain and old_plain + -- both are tables, recurse + do_setup old_val, cur_val + elseif not (cur_plain or old_plain) + -- both are streams (or nil), setup them + cur_val\setup old_val + --- setup previous `inputs`, if any, with the new inputs, and write them to + -- `inputs`. The `inputs` table can be nested with string or integer keys, + -- but all leaf-entries must be `Input` instances. It must not contain loops + -- or instances of other classes. + -- + -- @tparam table inputs table of `Input`s + setup: (inputs) => + old_inputs = @inputs + @inputs = inputs + do_setup old_inputs, @inputs + + do_unwrap = (value) -> + if value.__class + value\unwrap! + else + {k, do_unwrap v for k,v in pairs value} + --- `\unwrap` all `Input`s in `@inputs` and return a table with the same + -- shape. + -- + -- @treturn table the values of all `Input`s + unwrap_all: => do_unwrap @inputs + + __tostring: => "<op: #{@@__name}>" + __inherited: (cls) => cls.__base.__tostring = @__tostring + +{ + :Op +} |
