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 | |
| parent | spec base.match __tostring (diff) | |
| download | alive-1a8debe87762072b3a63b769aa515ebf63b4c70d.tar.gz alive-1a8debe87762072b3a63b769aa515ebf63b4c70d.zip | |
move into proper Lua module (`alv`)
Diffstat (limited to 'alv')
| -rw-r--r-- | alv/ast.moon | 40 | ||||
| -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 | ||||
| -rw-r--r-- | alv/builtin.moon | 333 | ||||
| -rw-r--r-- | alv/cell.moon | 189 | ||||
| -rw-r--r-- | alv/config.ld | 16 | ||||
| -rw-r--r-- | alv/copilot.moon | 85 | ||||
| -rw-r--r-- | alv/cycle.moon | 26 | ||||
| -rw-r--r-- | alv/error.moon | 94 | ||||
| -rw-r--r-- | alv/extensions.md | 221 | ||||
| -rw-r--r-- | alv/init.moon | 66 | ||||
| -rw-r--r-- | alv/invoke.moon | 124 | ||||
| -rw-r--r-- | alv/logger.moon | 82 | ||||
| -rw-r--r-- | alv/parsing.moon | 73 | ||||
| -rw-r--r-- | alv/registry.moon | 145 | ||||
| -rw-r--r-- | alv/result.moon | 126 | ||||
| -rw-r--r-- | alv/scope.moon | 118 | ||||
| -rw-r--r-- | alv/stream/base.moon | 68 | ||||
| -rw-r--r-- | alv/stream/event.moon | 100 | ||||
| -rw-r--r-- | alv/stream/init.moon | 18 | ||||
| -rw-r--r-- | alv/stream/io.moon | 55 | ||||
| -rw-r--r-- | alv/stream/value.moon | 237 | ||||
| -rw-r--r-- | alv/tag.moon | 122 | ||||
| -rw-r--r-- | alv/version.moon | 17 |
28 files changed, 3132 insertions, 0 deletions
diff --git a/alv/ast.moon b/alv/ast.moon new file mode 100644 index 0000000..a25d1cd --- /dev/null +++ b/alv/ast.moon @@ -0,0 +1,40 @@ +---- +-- AST Node Interface. +-- +-- implemented by `Value` and `Cell`. +-- +-- @classmod AST + +--- members +-- @section members + + --- evaluate this AST Node. + -- + -- Evaluate this node and return a `Result`. + -- + -- @function eval + -- @tparam Scope scope the scope to evaluate in + -- @treturn Result the evaluation result + + --- quote this AST Node, preserving its identity. + -- + --- Returns a mutable copy of this Node that shares its identity. + -- + -- @function quote + -- @treturn AST + + --- create a clone with its own identity. + -- + -- creates a clone of this Cell with its own identity by prepending a `parent` + -- Tag (and cloning all child expressions recursively). + -- + -- @function clone + -- @tparam Tag parent + -- @treturn AST + + --- stringify this AST Node. + -- + -- Should return the exact string this node was parsed from (if it was parsed). + -- + -- @function stringify + -- @treturn string the exact string this Node was parsed from 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 +} diff --git a/alv/builtin.moon b/alv/builtin.moon new file mode 100644 index 0000000..23412bf --- /dev/null +++ b/alv/builtin.moon @@ -0,0 +1,333 @@ +---- +-- Builtin `Builtin`s and `Op`s. +-- +-- Please see the [reference](../../reference/index.html#builtins) for +-- documentation. +-- +-- @module builtin +import Builtin, Op, FnDef, Input, val, evt from require 'alv.base' +import ValueStream, LiteralValue from require 'alv.stream.value' +import Result from require 'alv.result' +import Cell from require 'alv.cell' +import Scope from require 'alv.scope' +import Tag from require 'alv.tag' +import op_invoke from require 'alv.invoke' + +doc = ValueStream.meta + meta: + name: 'doc' + summary: "Print documentation in console." + examples: { '(doc sym)' } + description: "Print the documentation for `sym` to the console" + + value: class extends Builtin + 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 = ValueStream.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 Builtin + 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 = ValueStream.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 Builtin + 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_ = ValueStream.meta + meta: + name: 'require' + summary: "Load a module." + examples: { '(require name)' } + description: "Load a module and return its scope." + + value: class extends Builtin + 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 = ValueStream.wrap require "lib.#{name\unwrap 'str'}" + Result :value + +import_ = ValueStream.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 Builtin + 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 = ValueStream.wrap require "lib.#{name}" + scope\set name, Result :value -- (require "lib.#{name})\unwrap 'scope' + Result! + +import_star = ValueStream.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 Builtin + 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 = ValueStream.wrap require "lib.#{name}" + scope\use value\unwrap 'scope' -- (require "lib.#{name}")\unwrap 'scope' + + Result! + +fn = ValueStream.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 Builtin + 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 ValueStream.wrap FnDef param_symbols, body, scope + .meta = { + summary: "(user defined function)" + examples: { "(??? #{table.concat [p! for p in *param_symbols], ' '})" } + } + +defn = ValueStream.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 Builtin + 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 ValueStream.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 = ValueStream.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 Builtin + eval: (scope, tail) => + scope = Scope scope + Result children: [expr\eval scope for expr in *tail] + +if_ = ValueStream.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 Builtin + 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_ = ValueStream.meta + meta: + name: 'trace!' + summary: "Trace an expression's value at evaltime." + examples: { '(trace! expr)' } + + value: class extends Builtin + 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 = ValueStream.meta + meta: + name: 'trace' + summary: "Trace an expression's values at runtime." + examples: { '(trace expr)' } + + value: class extends Builtin + class traceOp extends Op + setup: (inputs) => + super + prefix: Input.cold inputs[1] + value: Input.hot inputs[2] + + 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' + ValueStream.str tostring tail[1] + tail[1] + } + inner\eval scope + +print = ValueStream.meta + meta: + name: 'print' + summary: "Print string values." + examples: { '(print str)' } + + value: class extends Op + setup: (inputs) => + value = (val.str / evt.str)\match inputs + super value: Input.hot value + + tick: => + if @inputs.value\metatype! == 'event' + for msg in *@inputs.value! + print msg + else + print @inputs.value! + +{ + :doc + :trace, 'trace!': trace_, :print + + :def, :use + require: require_ + import: import_ + 'import*': import_star + + true: ValueStream.meta + meta: + name: 'true' + summary: "The boolean constant `true`." + value: ValueStream.bool true + + false: ValueStream.meta + meta: + name: 'false' + summary: "The boolean constant `false`." + value: ValueStream.bool false + + bang: ValueStream.meta + meta: + name: 'bang' + summary: "A `bang` value-constant." + value: ValueStream 'bang', true + + :fn, :defn + 'do': do_expr + if: if_ +} diff --git a/alv/cell.moon b/alv/cell.moon new file mode 100644 index 0000000..f7e9773 --- /dev/null +++ b/alv/cell.moon @@ -0,0 +1,189 @@ +---- +-- S-Expression Cell, implements the `AST` interface. +-- +-- Consists of a head expression and any number of tail expressions (both `AST` +-- nodes), a `Tag`, and optionally the internal whitespace as parsed. +-- +-- @classmod Cell +import ValueStream from require 'alv.stream' +import Error from require 'alv.error' +import op_invoke, fn_invoke from require 'alv.invoke' +import Tag from require 'alv.tag' + +local RootCell + +class Cell +--- members +-- @section members + + new: (@tag=Tag.blank!, @children, @white) => + if not @white + @white = [' ' for i=1,#@children] + @white[0] = '' + + assert #@white == #@children, "mismatched whitespace length" + + --- get the head of the cell. + -- + -- @treturn AST + head: => @children[1] + + --- get the tail of the cell. + -- + -- @treturn {AST,...} + tail: => [c for c in *@children[2,]] + + __tostring: => @stringify 2 + + --- the parsed Tag. + -- + -- @tfield Tag tag + + --- sequence of child AST Nodes + -- + -- @tfield {AST,...} children + + --- optional sequence of whitespace segments. + -- + -- If set, `whitespace[i]` is the whitespace between `children[i]` and + -- `children[i+1]`, or the closing parenthesis of this Cell. `whitespace[0]` + -- is the space between the opening parenthesis and `children[1]`. + -- + -- @tfield ?{string,...} whitespace + +--- AST interface +-- +-- `Cell` implements the `AST` interface. +-- @section ast + + --- evaluate this Cell. + -- + -- `AST:eval`uates the head of the expression, and finds the appropriate + -- `Builtin` to invoke: + -- + -- - if head is an `opdef`, use `invoke.op_invoke` + -- - if head is a `fndef`, use `invoke.fn_invoke` + -- - if head is a `builtin`, unwrap it + -- + -- then calls `Builtin:eval_cell` on it. + -- + -- @tparam Scope scope the scope to evaluate in + -- @treturn Result the evaluation result + eval: (scope) => + head = assert @head!, Error 'syntax', "cannot evaluate empty expr" + head = (head\eval scope)\const! + Builtin = switch head.type + when 'opdef' + -- scope\get 'op-invoke' + op_invoke + when 'fndef' + -- scope\get 'fn-invoke' + fn_invoke + when 'builtin' + head\unwrap! + else + error Error 'type', "#{head} is not an opdef, fndef or builtin" + + Builtin\eval_cell @, scope, head + + --- quote this Cell, preserving its identity. + -- + -- Recursively quotes children, but preserves identity (i.e, shares the + -- `Tag`). A quoted Cell may only be 'used' once. If you want to `eval` a + -- `Cell` multiple times, use `clone`. + -- + -- @treturn Cell + quote: => + children = [child\quote scope for child in *@children] + Cell @tag, children, @white + + --- create a clone with its own identity. + -- + -- creates a clone of this Cell with its own identity by prepending a `parent` + -- to `tag` and cloning all child expressions recursively. + -- + -- @tparam Tag parent + -- @treturn Cell + clone: (parent) => + tag = @tag\clone parent + children = [child\clone parent for child in *@children] + Cell tag, children, @white + + --- stringify this Cell. + -- + -- if `depth` is passed, does not faithfully recreate the original string but + -- rather create useful debug output. + -- + -- @tparam[opt] int depth the maximum depth, defaults to infinite + -- @treturn string the exact string this Cell was parsed from, unless `@tag` + -- changed + stringify: (depth=-1) => + buf = '' + buf ..= if depth > 0 then '' else @white[0] + if depth == 0 + buf ..= '...' + else + for i, child in ipairs @children + buf ..= child\stringify if depth == -1 then -1 else depth - 1 + buf ..= if depth > 0 then ' ' else @white[i] + + if depth > 0 + buf = buf\sub 1, #buf - 1 + + tag = if depth == -1 then @tag\stringify! else '' + + '(' .. tag .. buf .. ')' + +--- static functions +-- @section static + + parse_args = (tag, parts) -> + if not parts + parts, tag = tag, nil + + children, white = {}, { [0]: parts[1] } + + for i = 2,#parts,2 + children[i/2] = parts[i] + white[i/2] = parts[i+1] + + tag, children, white + --- parse a Cell (for parsing with Lpeg). + -- + -- @tparam[opt] Tag tag + -- @tparam table parts + -- @treturn Cell + @parse: (...) -> + tag, children, white = parse_args ... + Cell tag, children, white + + --- parse a root Cell (for parsing with Lpeg). + -- + -- Root-Cells are at the root of an ALV document. + -- They have an implicit head of 'do' and a `[0]` tag. + -- + -- @tparam table parts + -- @treturn Cell + @parse_root: (...) -> + tag, children, white = parse_args (Tag.parse '0'), ... + RootCell tag, children, white + +-- @type RootCell +class RootCell extends Cell + head: => ValueStream.sym 'do' + tail: => @children + + stringify: => + buf = '' + buf ..= @white[0] + + for i, child in ipairs @children + buf ..= child\stringify! + buf ..= @white[i] + + buf + +{ + :Cell + :RootCell +} diff --git a/alv/config.ld b/alv/config.ld new file mode 100644 index 0000000..01160f8 --- /dev/null +++ b/alv/config.ld @@ -0,0 +1,16 @@ +project = 'alive internals' +title = 'developer docs' + +description = "`alive` developer documentation" +full_description = [[This section documents the *implementation* of the alive +language and copilot. It is relevant to everyone who is looking to modify, +improve or extend alive with new modules, language or interpreter features. + +If you are looking for the language reference for users, head over to the +[reference](../reference/index.html) section of the documentation.]] + +format = 'discount' +style = 'docs' +template = 'docs' +topics={'alv/extensions.md'} +dir = 'docs/internals' diff --git a/alv/copilot.moon b/alv/copilot.moon new file mode 100644 index 0000000..478cbae --- /dev/null +++ b/alv/copilot.moon @@ -0,0 +1,85 @@ +---- +-- File watcher and CLI entrypoint. +-- +-- @classmod Copilot +lfs = require 'lfs' +import Scope from require 'alv.scope' +import Registry from require 'alv.registry' +import Error from require 'alv.error' +import program from require 'alv.parsing' +globals = Scope.from_table require 'alv.builtin' + +slurp = (file) -> + file = io.open file, 'r' + with file\read '*all' + file\close! + +spit = (file, str) -> + file = io.open file, 'w' + file\write str + file\close! + +class Copilot +--- static functions +-- @section static + + --- create a new Copilot. + -- @classmethod + -- @tparam string file name/path of the alive file to watch and execute + new: (@file) => + @registry = Registry! + + @last_modification = 0 + + mode = lfs.attributes @file, 'mode' + if mode != 'file' + error "not a file: #{@file}" + +--- members +-- @section members + + --- poll for changes and tick. + tick: => + @poll! + + if @root + @registry\begin_tick! + ok, error = Error.try "updating", -> + @root\tick_io! + @root\tick! + if not ok + print error + @registry\end_tick! + + eval: => + @registry\begin_eval! + ok, ast = Error.try "parsing '#{@file}'", program\match, slurp @file + if not (ok and ast) + print ast or Error 'syntax', "failed to parse" + @registry\rollback_eval! + return + + scope = Scope globals + ok, root = Error.try "evaluating '#{@file}'", ast\eval, scope, @registry + if not ok + print root + @registry\rollback_eval! + return + + @registry\end_eval! + @root = root + spit @file, ast\stringify! + + poll: => + { :mode, :modification } = (lfs.attributes @file) or {} + if mode != 'file' + return + + if @last_modification < modification + L\log "#{@file} changed at #{modification}" + @eval! + @last_modification = os.time! + +{ + :Copilot +} diff --git a/alv/cycle.moon b/alv/cycle.moon new file mode 100644 index 0000000..f5b7e15 --- /dev/null +++ b/alv/cycle.moon @@ -0,0 +1,26 @@ +-- late-resolve cyclic dependencies +-- +-- this module provides a proxy for resolving values from modules which cannot +-- be loaded due to cyclic dependencies. Instead of +-- +-- import Something from require 'alv.somewhere' +-- Something ... +-- +-- use +-- +-- import somewhere from require 'alv.cycle' +-- somewhere.Something ... +-- +-- Make sure cycle:load() is called before you access or dereference +-- `somewhere`. + +load = => + for name, module in pairs @ + for k, v in pairs require "alv.#{name}" + module[k] = v + +setmetatable {}, __index: (key) => + return load if key == 'load' + + with v = {} + rawset @, key, v diff --git a/alv/error.moon b/alv/error.moon new file mode 100644 index 0000000..e72cc5f --- /dev/null +++ b/alv/error.moon @@ -0,0 +1,94 @@ +---- +-- Language error and traceback. +-- +-- @classmod Error + +unpack or= table.unpack + +class Error +--- members +-- @section members + + --- append a traceback frame. + -- + -- `where` should denote where the Error occured and fit grammatically to + -- complete the sentence `"{error} occured while {where}"` + -- + -- @tparam string where + add_frame: (where) => @trace ..= "\n while #{where}" + + __tostring: => + str = "#{@kind} error: #{@message}" + if @detail + str ..= "\n#{@detail}" + if @trace + str ..= @trace + str + +--- static functions +-- @section static + + --- create a new Error. + -- + -- `kind` should be one of: + -- + -- - `'reference'`: error concerning symbol resolution + -- - `'argument'`: error concerning Op argument matching + -- - `'implementation'`: error in the Lua/MoonScript implementation of alive. + -- Should not occur in normal operation, and constitutes a bug. + -- + -- @classmethod + -- @tparam string kind + -- @tparam string message + -- @tparam ?string detail + new: (@kind, @message, @detail) => + @trace = '' + + handler = (err) -> + if err.__class == Error + err + else + Error 'implementation', err, debug.traceback "Lua error below:", 2 + --- Wrap function errors in a traceback frame. + -- + -- Execute `fn(...)`, and turn any error thrown as a result into an + -- `Error` instance, before re-throwing it. + -- + -- When `Error` instances are caught, `frame` is added to the traceback. + -- All other error values are turned into `'implementation'` Errors. + -- + -- @tparam string frame + -- @tparam function fn + @wrap: (frame, fn, ...) -> + results = { xpcall fn, handler, ... } + ok = table.remove results, 1 + if ok + unpack results + else + error with results[1] + \add_frame frame + + --- Capture and wrap function errors in traceback frame. + -- + -- Execute `fn(...)`, and turn any error thrown as a result into an + -- `Error` instance, before re-throwing it. + -- + -- When `Error` instances are caught, `frame` is added to the traceback. + -- All other error values are turned into `'implementation'` Errors. + -- + -- @tparam string frame + -- @tparam function fn + -- @treturn boolean `ok` true if exeuction suceeded without errors + -- @treturn Error|any `error_or_results` the `Error` instance or results + @try: (frame, fn, ...) -> + results = { xpcall fn, handler, ... } + ok = table.remove results, 1 + if ok + ok, unpack results + else + ok, with results[1] + \add_frame frame + +{ + :Error +} diff --git a/alv/extensions.md b/alv/extensions.md new file mode 100644 index 0000000..82a4d10 --- /dev/null +++ b/alv/extensions.md @@ -0,0 +1,221 @@ +# writing `alive` extensions + +Extensions for `alive` are implemented in [Lua][lua] or [MoonScript][moonscript] +(which runs as Lua). When an `alive` module is [`(require)`][builtins-req]d, +alive looks for a Lua module `lib.[module]`. You can simply add a new file with +extension `.lua` or `.moon` in the `lib` directory of your alive installation or +somewhere else in your `LUA_PATH`. + +To write extensions, a number of classes and utilities are required. All of +these are exported in the `base` module. + +## documentation metadata +The lua module should return a `Scope` or a table that will be converted using +`Scope.from_table`. All exports should be documented using `ValueStream.meta`, +which attaches a `meta` table to the value that is used for error messages, +documentation generation and [`(doc)`][builtins-doc]. + + import ValueStream from require 'alv.base' + + two = ValueStream.meta + meta: + name: 'two' + summary: "the number two" + value: 2 + + { + :two + } + +In the `meta` table `summary` is the only required key, but all of the +information that applies should be provided. + +- `name`: the name of this export (for error reporting). +- `summary`: a one-line plain-text description of this entry. Should be + capitalized and end with a period. +- `examples`: a table of strings, each of which is a short one-line code + example illustrating the argument names for an Op. +- `description`: a longer markdown-formatted description of the functionality + of this entry. + +## defining `Op`s +Most extensions will want to define a number of *Op*s to be used by the user. +They are implemented by deriving from the `Op` class and implementing at least +the `Op:setup` and `Op:tick` methods. + + import ValueStream, Op, Input, evt from require 'alv.base' + + total_sum = ValueStream.meta + meta: + name: 'total-sum' + summary: "Keep a total of incoming numbers." + examples: { '(total-sum num!)' } + description: "Keep a total sum of incoming number events, extension-style." + + value: class extends Op + new: (...) => + super ... + @state or= { total: 0 } + @out or= ValueStream 'num', @state.total + + setup: (inputs, scope) => + num = evt.num\match inputs + super num: Inputs.hot num + + tick: => + @state.total += @inputs.num! + @out\set @state.total + + { + 'total-sum': total_sum + } + +### Op:setup +`Op:setup` is called once every *eval cycle* to parse the Op's arguments, check +their types, choose the updating behaviour and define the output type. + +The arguments to `:setup` are a list of inputs (each is a `Result` instance), +and the `Scope` the evaluation happened in. Ops generally shouldn't use the +scope, but might look up 'magic' dynamic symbols like `\*clock\*`. + +#### argument parsing +Arguments should be parsed using `base.match`. The two exports `base.match.val` +and `base.match.evt` are used to build complex patterns that can parse and +validate the Op arguments into complex structures (see the module documentation +for more information). + + import val, evt from require 'alv.base' + + pattern = evt.bang + val.str + val.num*3 + -evt! + { trig, str, numbers, optional } = pattern\match inputs + +This example matches first an `EventStream` of type `bang`, then a `ValueStream` +of type `str`, followed by one, two or three `num`-values and finally an +optional argument `EventStream` of any type. `:match` will throw an error if it +couldn't (fully) match the arguments and otherwise return a structured mapping +of the inputs. + +If there are more complex dependencies between arguments, it is recommended to +do as much of the parsing as possible using the `base.match` and then continue +manually. For invalid or missing arguments, `Error` instances should be thrown +using `error` or `assert`. + +#### input setup +There are two types of inputs: `Input.hot` and `Input.cold`: + +*Cold* inputs do not cause the Op to update when changes to the input stream +are made. They are useful to 'ignore' changes to inputs which are only relevant +when another input changed value. Imagine for example a `send-value-when` Op, +which sends a value only when a `bang!` input is live. This Op doesn't have to +update when the value changes, it's enough to update only when the trigger +input changes and simply read the value in that moment. + +*Hot* inputs on the other hand mark the input stream as a dependency for the +Op. Depending on the type of `Stream`, the semantics are a little different: + +- For `ValueStream`s, the Op updates whenever the current value changes. When + an input stream is swapped out for another one at evaltime, but their values + are momentarily equal, the input is not considered dirty. +- For `EventStream`s and `IOStream`s, the Op updates whenever the stream is + dirty. There is no special handling when the stream is swapped out at + evaltime. + +All `Result`s from the `inputs` argument that are taken into consideration +should be wrapped in an `Input` instance using either `Input.hot` or +`Input.cold`, and need to be passed to the `Op:setup` super implementation. +To illustrate with the `send-value-when` example: + + setup: (inputs, scope) => + { trig, value } = match 'bang! any', inputs + + super + trig: Inputs.hot trig + value: Inputs.cold value + +`Op:setup` takes a table that can have any (even nested) shape you want, as +long as all 'leaf values' are `Input` instances. The following are both valid: + + super { (Inputs.hot trig), (Inputs.cold value) } + + super + trigger: Inputs.hot trig + values: { (Inputs.cold a), (Inputs.cold b), (Inputs.cold c) } + +#### output setup +When `Op:setup` finishes, `@out` has to be set to a `Stream` instance. The +instance can be created in `Op:setup`, or by overriding the constructor and +delegating to the original one using `super`. In general setting it in the +constructor is preferred, and it is only moved to `Op:setup` if the output +type depends on the arguments received. + +There are three types of `Stream`s that can be created: + +- `ValueStream`s track *continuous values*. They can only have one value per + tick, and downstream Ops will not update when a *ValueStream* has been set + to the same value it already had. They are updated using `ValueStream:set`. +- `EventStream`s transmit *momentary events*. They can transmit multiple events + in a single tick. `EventStream`s do not keep a value set on the last tick on + the next tick. They are updated using `EventStream:add`. +- `IOStream`s are like `EventStream`s, but their `IOStream:tick` method is + polled by the event loop at the start of every tick. This gives them a chance + to effectively create changes 'out of thin air' and kickstart the execution + of the dataflow engine. All *runtime* execution is due to an `IOStream` + becoming dirty somewhere. See the section on implementing `IOStream`s below + for more information. + +### Op:tick +`Op:tick` is called whenever any of the inputs are *dirty*. This is where the +Op's main logic will go. Generally here it should be checked which input(s) +changed, and then internal state and the output value may be updated. + +## defining `Builtin`s +Builtins are more powerful than Ops, because they control whether, which and +how their arguments are evaluated. They roughly correspond to *macros* in Lisps. +There is less of a concrete guideline for implementing Builtins because there +are a lot more options, and it really depends a lot on what the Builtin should +achieve. Nevertheless, a good starting point is to read the `Builtin` class +documentation, take a look at `Builtin`s in `alv/builtin.moon` and get +familiar with the relevant internal interfaces (especially `AST`, `Result`, and +`Scope`). + +## defining `IOStream`s +`IOStream`s are `EventStream`s that can 'magically' create events out of +nothing. They are the source of all processing in alive. Whenever you want to +bring events into alive from an external protocol or application, an IOStream +will be necessary. + +To implement a custom IOStream, create it as a class that inherits from the +`IOStream` base and implement the constructor and `IOStream:tick`: + + import IOStream from require 'alv.base' + + class UnreliableStream extends IOStream + new: => super 'bang' + + tick: => + if math.random! < 0.1 + @add true + +In the constructor, you should call the super-constructor `EventStream.new` to +set the event type. Often this will be a custom event that is only used inside +your extension (such as e.g. the `midi/port` type in the [midi][modules-midi] +module), but it can also be a primitive type like `'num'` in this example. In +`:tick`, your IOStream is given a chance to communicate with the external world +and create any resulting events. The example stream above randomly sends bang +events out, with a 10% chance each 'tick' of the system. Note that there is no +guarantee about when or how often ticks occur, so you really shouldn't rely on +them this way in a real extension. + +### using `IOStream`s +There's a couple of ways IOStreams can be used and exposed to the user of your +extension. You can either expose an instance of your IOStream directly +(documented using `ValueStream.meta`), or offer an Op that creates and returns +an instance in `Op.out` - that way the IOStream can be created only on demand +and take parameters. It is also possible to not exepose the IOStream at all, +and rather pass it as a hardcoded input into an Op's `Op.inputs`. + +[lua]: https://www.lua.org/ +[moonscript]: http://moonscript.org/ +[builtins-req]: ../../reference/index.html#require +[builtins-doc]: ../../reference/index.html#doc +[modules-midi]: ../../reference/midi.html diff --git a/alv/init.moon b/alv/init.moon new file mode 100644 index 0000000..d86cd1f --- /dev/null +++ b/alv/init.moon @@ -0,0 +1,66 @@ +---- +-- `alive` public API. +-- +-- @module init +if _VERSION == 'Lua 5.1' + export assert + assert = (a, msg, ...) -> + if not a + error msg + a, msg, ... + +import Logger from require 'alv.logger' +import ValueStream, EventStream, IOStream from require 'alv.stream' +import Result from require 'alv.result' +import Scope from require 'alv.scope' +import Error from require 'alv.error' +import Registry, SimpleRegistry from require 'alv.registry' +import Tag from require 'alv.tag' + +import Cell, RootCell from require 'alv.cell' +import program from require 'alv.parsing' + +with require 'alv.cycle' + \load! + +import Copilot from require 'alv.copilot' +globals = Scope.from_table require 'alv.builtin' + +--- exports +-- @table exports +-- @tfield ValueStream ValueStream +-- @tfield EventStream EventStream +-- @tfield IOStream IOStream +-- @tfield Result Result +-- @tfield Cell Cell +-- @tfield RootCell RootCell +-- @tfield Scope Scope +-- @tfield Error Error +-- @tfield Registry Registry +-- @tfield Tag Tag +-- @tfield Copilot Copilot +-- @tfield Logger Logger +-- @tfield Scope globals global definitons +-- @tfield parse function to turn a `string` into a root `Cell` +{ + :ValueStream, :EventStream, :IOStream + :Cell, :RootCell + :Result, :Scope, :Error + + :Registry, :SimpleRegistry, :Tag + + :globals + + :Copilot, :Logger + + parse: (str) -> + assert (program\match str), Error 'syntax', "failed to parse" + + eval: (str, inject) -> + scope = Scope nil, globals + scope\use inject if inject + + ast = assert (program\match str), "failed to parse" + result = ast\eval scope + result\const! +} diff --git a/alv/invoke.moon b/alv/invoke.moon new file mode 100644 index 0000000..6afa57a --- /dev/null +++ b/alv/invoke.moon @@ -0,0 +1,124 @@ +---- +-- Builtins for invoking `Op`s and `FnDef`s. +-- +-- @module invoke +import Result from require 'alv.result' +import Builtin from require 'alv.base' +import Scope from require 'alv.scope' +import Error from require 'alv.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)" + +--- `Builtin` implementation that invokes an `Op`. +-- +-- @type op_invoke +class op_invoke extends Builtin + --- `Builtin:setup` implementation. + -- + -- `Op:fork`s the `prev`'s `Op` instance if given. Creates a new instance + -- otherwise. + setup: (prev) => + if prev + @op = prev.op\fork! + else + def = @head\unwrap 'opdef', "cant op-invoke #{@head}" + @op = def! + + --- `Builtin:destroy` implementation. + -- + -- calls `op`:@{Op:destroy|destroy}. + destroy: => @op\destroy! + + --- evaluate an `Op` invocation. + -- + -- `AST:eval`s the tail, and passes the result to `op`:@{Op:setup|setup}. Then + -- checks if any of `op`:@{Op:all_inputs|all_inputs} are @{Input:dirty|dirty}, + -- and if so, calls `op`:@{Op:tick|tick}. + -- + -- The `Result` contains `op`, `Op.value` and all the `Result`s from the tail. + -- + -- @tparam Scope scope the active scope + -- @tparam {AST,...} tail the arguments to this expression + -- @treturn Result + eval: (scope, tail) => + children = [L\push expr\eval, scope for expr in *tail] + + 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! + if input\dirty! + any_dirty = true + break + + if any_dirty + @op\tick true + + for input in @op\all_inputs! + input\finish_setup! + + Result :children, value: @op.out, op: @op + + --- The `Op` instance. + -- + -- @tfield Op op + +--- `Builtin` implementation that invokes a `FnDef`. +-- +-- @type fn_invoke +class fn_invoke extends Builtin + --- evaluate a user-function invocation. + -- + -- Creates a new `Scope` that inherits from `FnDef.scope` and has + -- `outer_scope` as an additional parent for dynamic symbol resolution. + -- Then `AST:eval`s the tail in `outer_scope`, and defines the results to the + -- names in `FnDef.params` in the newly created scope. Lastly, `AST:clone`s + -- `FnDef.body` with the prefix `Builtin.tag`, and `AST:eval`s it in the newly + -- created `Scope`. + -- + -- The `Result` contains the `Stream` from the cloned AST, and its children + -- are all the `Result`s from evaluating the tail as well as the cloned + -- `AST`s. + -- + -- @tparam Scope outer_scope the active scope + -- @tparam {AST,...} tail the arguments to this expression + -- @treturn Result the result of this evaluation + eval: (outer_scope, tail) => + name = get_name @head, @cell\head! + frame = "invoking function #{name} at [#{@tag}]" + + { :params, :body, :scope } = @head\unwrap 'fndef', "cant fn-invoke #{@head}" + if #params != #tail + err = Error 'argument', "expected #{#params} arguments, found #{#tail}" + err\add_frame frame + error err + + fn_scope = Scope scope, outer_scope + + children = for i=1,#params + name = params[i]\unwrap 'sym' + with L\push tail[i]\eval, outer_scope + fn_scope\set name, \make_ref! + + clone = body\clone @tag + result = Error.wrap frame, clone\eval, fn_scope + + table.insert children, result + Result :children, value: result.value + +{ + :op_invoke, :fn_invoke +} diff --git a/alv/logger.moon b/alv/logger.moon new file mode 100644 index 0000000..973e8b5 --- /dev/null +++ b/alv/logger.moon @@ -0,0 +1,82 @@ +---- +-- Logger implementation. +-- +-- @classmod Logger +unpack or= table.unpack + +export L +L or= setmetatable {}, __index: => -> + +class Logger + levels = { + debug: 0 + trace: 1 + log: 2 + warn: 3 + error: 4 + print: 5 + silent: 6 + } + +--- members +-- @section members + + --- push an indentation level and execute a function in it. + -- @tparam function fn the function to execute + -- @param ... parameters to `fn` + push: (fn, ...) => + last = @prefix + @prefix ..= ' ' + + res = { xpcall fn, debug.traceback, ... } + + @prefix = last + + if ok = table.remove res, 1 + unpack res + else + error unpack res + +--- static functions +-- @section static + + --- create a new Logger. + -- @classmethod + -- @tparam string level the log-level to log at. + new: (level='log') => + @level = levels[level] or level + @prefix = '' + + for name, level in pairs levels + @[name] = (first, ...) => + return unless @level <= level + + where = debug.traceback '', 2 + if level == levels.error or @level == levels.debug + print @prefix .. first, ... + print where + else + print @prefix .. first, ... + + if level == levels.print + @push = (fn, ...) => fn ... + + --- set up the global Logger singleton. + -- + -- The available log-levels are: + -- + -- - `'debug'` + -- - `'trace'` + -- - `'log'` (the default) + -- - `'warn'` + -- - `'error'` + -- - `'print'` + -- - `'silent'` + -- + -- @tparam ?string level the level to initialize the logger at. + @init: (...) -> + L = Logger ... + +{ + :Logger +} diff --git a/alv/parsing.moon b/alv/parsing.moon new file mode 100644 index 0000000..f9d6f98 --- /dev/null +++ b/alv/parsing.moon @@ -0,0 +1,73 @@ +---- +-- Lpeg Grammar for parsing `alive` code. +-- +-- @module parsing +import ValueStream from require 'alv.stream' +import Cell from require 'alv.cell' +import Tag from require 'alv.tag' +import R, S, P, V, C, Ct from require 'lpeg' + +-- whitespace +wc = S ' \t\r\n' +comment = P { + 'comment', + expr: (P '(') * ((V 'expr') + (1 - P ')'))^0 * (P ')') + comment: (P '#(') * ((V 'expr') + (1 - P ')'))^0 * (P ')') +} +space = (wc^1 * (comment * wc^1)^0) / 1 -- required whitespace +mspace = (comment + wc)^0 / 1 -- optional whitespace + +-- atoms +digit = R '09' +first = (R 'az', 'AZ') + S '-_+*/.!?=%' +sym = first * (first + digit)^0 / ValueStream\parse 'sym' + +strd = '"' * (C ((P '\\"') + (P '\\\\') + (1 - P '"'))^0) * '"' / ValueStream\parse 'str', '\"' +strq = "'" * (C ((P "\\'") + (P '\\\\') + (1 - P "'"))^0) * "'" / ValueStream\parse 'str', '\'' +str = strd + strq + +int = digit^1 +float = (digit^1 * '.' * digit^0) + (digit^0 * '.' * digit^1) +num = ((P '-')^-1 * (float + int)) / ValueStream\parse 'num' + +atom = num + sym + str + +expr = (V 'cell') + atom +explist = Ct mspace * ((V 'expr') * (space * (V 'expr'))^0 * mspace)^-1 + +tag = (P '[') * (digit^1 / Tag.parse) * (P ']') +cell = (P '(') * tag^-1 * (V 'explist') * (P ')') / Cell.parse + +root = P { + (V 'explist') / Cell.parse_root + :expr, :explist, :cell +} + +cell = P { + 'cell' + :expr, :explist, :cell +} + +program = root * -1 + +--- exports +-- @table exports +-- @tfield pattern comment +-- @tfield pattern space +-- @tfield pattern atom +-- @tfield pattern expr +-- @tfield pattern explist +-- @tfield pattern explist +-- @tfield pattern cell +-- @tfield pattern root +-- @tfield pattern program the main parsing entrypoint +{ + :comment + :space + :atom + :expr + :explist + :cell + :root + :program +} diff --git a/alv/registry.moon b/alv/registry.moon new file mode 100644 index 0000000..c41e684 --- /dev/null +++ b/alv/registry.moon @@ -0,0 +1,145 @@ +---- +-- `Tag` Registry. +-- +-- @classmod Registry +import Result from require 'alv.result' +import Error from require 'alv.error' + +class Registry +--- internals for `Tag` +-- @section internals + + --- lookup the last registration. + -- + -- @tparam number|string index the registration index + -- @treturn any + last: (index) => @last_map[index] + + --- set the current registration. + -- + -- @tparam string\number index the registration index + -- @tparam any expr the registration value + -- @tparam[default=false] boolean ignore_dup ignore duplicate registrations + register: (index, expr, ignore_dup=false) => + L\trace "reg: setting #{index} to #{expr}" + if not ignore_dup and @map[index] + error Error 'tag', "duplicate tags [#{index}]!" + @map[index] = expr + + --- request identity and registration for blank tag. + -- + -- @tparam Tag tag the blank tag + -- @tparam any expr the registration value + init: (tag, expr) => + L\trace "reg: init pending to #{expr}" + table.insert @pending, { :tag, :expr } + +--- members +-- @section members + + --- begin an evaluation cycle. + -- + -- Begin an evaltime cycle (and tick). + -- Set the active Registry and clear out pending registrations. + -- + -- All calls go `begin_eval` must be matched with either a call to + -- `end_eval` or `rollback_eval`. + begin_eval: => + @latest_map = @last_map + @begin_tick! + @map, @pending = {}, {} + + --- end an evaluation cycle. + -- + -- Register all pending `Tag`s and destroy all orphaned registrations. + -- Unset the active Registry. + end_eval: => + for tag, val in pairs @last_map + val\destroy! unless @map[tag] + + for { :tag, :expr } in *@pending + -- tag was solved by another pending registration + -- (e.g. first [A] is solved, then [5.A] is solved) + continue if tag\index! + + next_tag = #@map + 1 + L\trace "assigned new tag #{next_tag} to #{tag} #{expr}" + tag\set next_tag + @map[tag\index!] = expr + + @last_map = @map + @end_tick! + + --- abort an evaluation cycle. + -- + -- Unset the active Registry. + rollback_eval: => + @end_tick! + + --- begin a run cycle. + -- + -- Increment the tick index and set the active Registry. + begin_tick: => + @grab! + @next_tick! + + --- end a run cycle. + -- + -- Unset the active Registry. + end_tick: => + @release! + + --- manually increment the tick index (for testing). + next_tick: => + @tick += 1 + + --- set the active Registry. + grab: => + assert not @prev, "already have a previous registry? #{@prev}" + @prev, Registry.active_registry = Registry.active_registry, @ + + --- unset the active Registry. + release: => + assert @ == Registry.active_registry, "not the active registry!" + Registry.active_registry, @prev = @prev, nil + +--- static functions +-- @section static + + --- create a new Registry. + -- @classmethod + new: => + @last_map, @map = {}, {} + @tick = 0 + + --- get the active Registry. + -- + -- Raises an erro when there is no active Regsitry. + -- + -- @treturn Registry + @active: -> assert Registry.active_registry, "no active Registry!" + +class SimpleRegistry extends Registry + new: => + @cnt = 1 + @tick = 0 + + next_tick: => + @tick += 1 + + init: (tag, expr) => + tag\set @cnt + @cnt += 1 + + last: (index) => + register: (index, expr) => + + wrap: (fn) => (...) -> + @grab! + with fn ... + @release! + +{ + :Registry + :SimpleRegistry +} diff --git a/alv/result.moon b/alv/result.moon new file mode 100644 index 0000000..6c994cb --- /dev/null +++ b/alv/result.moon @@ -0,0 +1,126 @@ +---- +-- Result of evaluating an expression. +-- +-- `Result`s form a tree that controls execution order and message passing +-- between `Op`s. +-- +-- @classmod Result +class Result +--- members +-- @section members + + --- return whether this Result's value is const. + is_const: => not next @side_inputs + + --- assert value-constness and return the value. + -- @tparam[opt] string msg the error message to throw + -- @treturn any + const: (msg) => + assert not (next @side_inputs), msg or "eval-time const expected" + @value + + --- assert this result has a value, return its type. + -- @treturn string + type: => + assert @value, "Result with value expected" + @value.type + + --- assert this result has a value, returns its metatype. + -- @treturn string `"value"` or `"event"` + metatype: => + assert @value, "Result with value expected" + @value.metatype + + --- create a copy of this result with value-copy semantics. + -- the copy has the same @value and @side_inputs, but will not update + -- anything on \tick. + make_ref: => + with Result value: @value + .side_inputs = @side_inputs + + --- tick all IOStream instances that are effecting this (sub)tree. + -- should be called once per frame on the root, right before tick. + tick_io: => + for stream, input in pairs @side_inputs + stream\tick! if input.io + + --- in depth-first order, tick all Ops which have dirty Inputs. + -- + -- short-circuits if there are no dirty Inputs in the entire subtree + tick: => + any_dirty = false + for stream, input in pairs @side_inputs + if input\dirty! + any_dirty = true + break + + -- early-out if no Inputs are dirty in this whole subtree + return unless any_dirty + + for child in *@children + child\tick! + + if @op + -- we have to check self_dirty here, because Inputs from children may + -- have become dirty due to \tick + self_dirty = false + for stream in @op\all_inputs! + if stream\dirty! + self_dirty = true + + return unless self_dirty + + @op\tick! + + __tostring: => + buf = "<result=#{@value}" + buf ..= " #{@op}" if @op + buf ..= " (#{#@children} children)" if #@children > 0 + buf ..= ">" + buf + + --- the `Stream` result + -- + -- @tfield ?Stream value + + --- an Op + -- + -- @tfield ?Op op + + --- list of child `Result`s from subexpressions + -- + -- @tfield {}|{Result,...} children + + --- cached mapping of all `Stream`/`Input` pairs affecting this Result. + -- + -- This is the union of all `children`s `side_inputs` and all `Input`s from + -- `op` that are not the `value` of any child. + -- + -- @tfield {[Stream]=Input,...} side_inputs + +--- static functions +-- @section static + + --- create a new Result. + -- @classmethod + -- @param params table with optional keys op, value, children. default: {} + new: (params={}) => + @value = params.value + @op = params.op + @children = params.children or {} + + @side_inputs, is_child = {}, {} + for child in *@children + for stream, input in pairs child.side_inputs + @side_inputs[stream] = input + if child.value + is_child[child.value] = true + + if @op + for input in @op\all_inputs! + if input.io or not is_child[input.stream] + @side_inputs[input.stream] = input + +{ + :Result +} diff --git a/alv/scope.moon b/alv/scope.moon new file mode 100644 index 0000000..6a0e2a5 --- /dev/null +++ b/alv/scope.moon @@ -0,0 +1,118 @@ +---- +-- Mapping from `sym`s to `Result`s. +-- +-- @classmod Scope +import ValueStream from require 'alv.stream' +import Result from require 'alv.result' +import Error from require 'alv.error' + +class Scope +--- members +-- @section members + + --- set a Lua value in the scope. + -- + -- wraps `val` in a `ValueStream` and `Result` before calling `set`. + -- + -- @tparam string key + -- @tparam any val + set_raw: (key, val) => + value = ValueStream.wrap val, key + @values[key] = Result :value + + --- set a symbol to a `Result`. + -- + -- @tparam string key + -- @tparam Result val + set: (key, val) => + L\trace "setting #{key} = #{val} in #{@}" + assert val.__class == Result, "expected #{key}=#{val} to be Result" + assert val.value, Error 'type', "cannot define symbol to nil" + assert not @values[key], Error 'type', "cannot redefine symbol '#{key}'!" + @values[key] = val + + recurse: (key) => + parent = if key\match '^%*.*%*$' then @dynamic_parent else @parent + parent or= @parent + if parent + L\push parent\get, key + else + error Error 'reference', "undefined symbol '#{key}'" + + --- resolve a key in this Scope. + -- + -- @tparam string key the key to resolve + -- @treturn ?Result the value of the definition that was found, or `nil` + get: (key) => + L\debug "checking for #{key} in #{@}" + if val = @values[key] + L\trace "found #{val} in #{@}" + return val + + start, rest = key\match '^(.-)/(.+)' + if not start + return @recurse key + + child = @get start + if not child + error Error 'reference', "undefined symbol '#{start}'" + if child\type! != 'scope' + error Error 'reference', "'#{start}' is not a scope" + child.value!\get rest, while_msg + + --- copy definitions from another scope. + -- + -- copies all definitions from `other`. Does not copy inherited definitions. + -- + -- @tparam Scope other + use: (other) => + L\trace "using defs from #{other} in #{@}" + for k, v in pairs other.values + @values[k] = v + + __tostring: => + buf = "<Scope" + + depth = -1 + parent = @parent + while parent + depth += 1 + parent = parent.parent + buf ..= " ^#{depth}" if depth != 0 + + keys = [key for key in pairs @values] + if #keys > 5 + keys = [key for key in *keys[,5]] + keys[6] = '...' + buf ..= " [#{table.concat keys, ', '}]" + + buf ..= ">" + buf + +--- static functions +-- @section static + + --- create a new Scope. + -- + -- @classmethod + -- @tparam[opt] Scope parent a parent this scope inherits definitions from + -- @tparam[opt] Scope dynamic_parent a parent scope that should be checked for + -- dynamic definitions + new: (@parent, @dynamic_parent) => + @values = {} + + --- convert a Lua table to a Scope. + -- + -- `tbl` may contain more tables (or `Scope`s). + -- Uses `ValueStream.wrap` on the values recursively. + -- + -- @tparam table tbl the table to convert + -- @treturn Scope + @from_table: (tbl) -> + with Scope! + for k, v in pairs tbl + \set_raw k, v + +{ + :Scope +} diff --git a/alv/stream/base.moon b/alv/stream/base.moon new file mode 100644 index 0000000..37cc8a4 --- /dev/null +++ b/alv/stream/base.moon @@ -0,0 +1,68 @@ +---- +-- base Stream interface. +-- +-- implemented by `ValueStream`, `EventStream`, and `IOStream`. +-- +-- @classmod Stream + +class Stream +--- Stream interface. +-- +-- Methods that have to be implemented by `Stream` implementations +-- (`ValueStream`, `EventStream`, `IOStream`). +-- +-- @section interface + + --- return whether this Stream was changed in the current tick. + -- + -- @function dirty + -- @treturn boolean + + --- create a mutable copy of this Stream. + -- + -- Used to insulate eval-cycles from each other. + -- + -- @function fork + -- @treturn Stream + + --- the type name of this Stream's value. + -- + -- the following builtin typenames are used: + -- + -- - `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 a `Builtin` subclass + -- - `fndef` - `value` is a `FnDef` instance + -- - `scope` - `value` is a `Scope` instance + -- + -- @tfield string type + + --- documentation metadata. + -- + -- an optional table containing metadata for error messages and + -- documentation. The following keys are recognized: + -- + -- - `name`: optional name + -- - `summary`: single-line description (markdown) + -- - `examples`: optional list of single-line code examples + -- - `description`: optional full-text description (markdown) + -- + -- @tfield ?table meta + +--- static functions +-- @section static + + --- construct a new Stream. + -- + -- @classmethod + -- @tparam string type the type name + -- @tparam ?table meta the `meta` table + new: (@type, @meta={}) => + +{ + :Stream +} diff --git a/alv/stream/event.moon b/alv/stream/event.moon new file mode 100644 index 0000000..97570a2 --- /dev/null +++ b/alv/stream/event.moon @@ -0,0 +1,100 @@ +---- +-- Stream of momentary events. +-- +-- @classmod EventStream +import Stream from require 'alv.stream.base' +import Result from require 'alv.result' +import Error from require 'alv.error' +import scope, base, registry from require 'alv.cycle' + +class EventStream extends Stream +--- members +-- @section members + + --- return whether this stream was changed in the current tick. + -- + -- @treturn bool + dirty: => @updated == registry.Registry.active!.tick + + --- push an event value into the stream. + -- + -- Marks this stream as dirty for the remainder of the current tick. + -- + -- @tparam any event + add: (event) => + if not @dirty! + @events = {} + + @updated = registry.Registry.active!.tick + table.insert @events, event + + --- get the sequence of current events (if any). + -- + -- Returns `events` if `dirty`, or an empty table otherwise. + -- Asserts `@type == type` if `type` is given. + -- + -- @tparam[opt] string type the type to check for + -- @tparam[optchain] string msg message to throw if type don't match + -- @treturn {any,...} `events` + unwrap: (type, msg) => + assert type == @type, msg or "#{@} is not a #{type}" if type + if @dirty! then @events else {} + + --- create a mutable copy of this stream. + -- + -- Used to wrap insulate eval-cycles from each other. + -- + -- @treturn EventStream + fork: => @@ @type + + --- alias for `unwrap`. + __call: (...) => @unwrap ... + + __tostring: => + "<#{@@__name} #{@type}>" + + --- Stream metatype. + -- + -- @tfield string metatype + metatype: 'event' + + --- the type name of the stream. + -- + -- the following builtin typenames are used: + -- + -- - `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 a `Builtin` subclass + -- - `fndef` - `value` is a `FnDef` instance + -- - `scope` - `value` is a `Scope` instance + -- + -- @tfield string type + + --- documentation metadata. + -- + -- an optional table containing metadata for error messages and + -- documentation. The following keys are recognized: + -- + -- - `name`: optional name + -- - `summary`: single-line description (markdown) + -- - `examples`: optional list of single-line code examples + -- - `description`: optional full-text description (markdown) + -- + -- @tfield ?table meta + +--- static functions +-- @section static + + --- construct a new EventStream. + -- + -- @classmethod + -- @tparam string type the type name + new: (type) => super type + +{ + :EventStream +} diff --git a/alv/stream/init.moon b/alv/stream/init.moon new file mode 100644 index 0000000..f9393b0 --- /dev/null +++ b/alv/stream/init.moon @@ -0,0 +1,18 @@ +---- +-- `Stream` interface and implementations. +-- +-- @see Stream +-- @see ValueStream +-- @see EventStream +-- @see IOStream +-- +-- @module stream +import ValueStream from require 'alv.stream.value' +import EventStream from require 'alv.stream.event' +import IOStream from require 'alv.stream.io' + +{ + :ValueStream + :EventStream + :IOStream +} diff --git a/alv/stream/io.moon b/alv/stream/io.moon new file mode 100644 index 0000000..9b0159c --- /dev/null +++ b/alv/stream/io.moon @@ -0,0 +1,55 @@ +---- +-- Stream of external side-effects. +-- +-- Unlike other `Stream`s, this is not updated/set by an `Op` instace, but is +-- continuously polled for changes by the runtime and may mark itself as +-- *dirty* at any point in time. All runtime execution happens due to IOStream +-- updates, which ripple through the `Result` tree. +-- +-- @classmod IOStream +import EventStream from require 'alv.stream.event' + +class IOStream extends EventStream +--- IOStream interface. +-- +-- methods that have to be implemented by `IOStream` implementations. +-- @section interface + + --- construct a new IOStream. + -- + -- Must prepare the instance for `dirty` to be called. + -- The super-constructor should be called to set `Stream.type`. + -- + -- @classmethod + -- @tparam string type the typename of this stream. + new: (type) => super type + + --- create a mutable copy of this stream. + -- + -- Used to wrap insulate eval-cycles from each other. + -- + -- @treturn IOStream + fork: => @ + + --- poll for changes. + -- + -- Called every frame by the main event loop to update internal state. + tick: => + + --- check whether this adapter requires processing. + -- + -- Must return a boolean indicating whether `Op`s that refer to this instance + -- via `Input.hot` should be notified (via `Op:tick`). May be called multiple + -- times. May be called before `tick` on the first frame after construction. + -- + -- If this is not overrided, the `EventStream` interface can be used, see + -- `EventStream.add`, `EventStream.unwrap`, and `EventStream.dirty`. + -- + -- @function dirty + -- @treturn bool whether processing is required + + __inherited: (cls) => cls.__base.__tostring = @__tostring + +{ + :IOStream +} diff --git a/alv/stream/value.moon b/alv/stream/value.moon new file mode 100644 index 0000000..213a26c --- /dev/null +++ b/alv/stream/value.moon @@ -0,0 +1,237 @@ +---- +-- Continuous stream of values. +-- +-- Implements the `Stream` and `AST` intefaces. +-- +-- @classmod ValueStream +import Stream from require 'alv.stream.base' +import Result from require 'alv.result' +import Error from require 'alv.error' +import scope, base, registry from require 'alv.cycle' + +ancestor = (klass) -> + assert klass, "cant find the ancestor of nil" + while klass.__parent + klass = klass.__parent + klass + +class ValueStream extends Stream +--- members +-- @section members + + --- return whether this stream was changed in the current tick. + -- + -- @treturn bool + dirty: => @updated == registry.Registry.active!.tick + + --- update this stream's value. + -- + -- Marks this stream as dirty for the remainder of the current tick. + set: (@value) => @updated = registry.Registry.active!.tick + + --- unwrap to the Lua type. + -- + -- Asserts `@type == type` if `type` is given. + -- + -- @tparam[opt] string type the type to check for + -- @tparam[optchain] string msg message to throw if type don't match + -- @treturn any `value` + unwrap: (type, msg) => + assert type == @type, msg or "#{@} is not a #{type}" if type + @value + + --- create a mutable copy of this stream. + -- + -- Used to insulate eval-cycles from each other. + -- + -- @treturn ValueStream + fork: => + with ValueStream @type, @value, @raw + .updated = @updated + + --- alias for `unwrap`. + __call: (...) => @unwrap ... + + --- compare two values. + -- + -- Compares two `ValueStream`s by comparing their types and their Lua values. + __eq: (other) => other.type == @type and other.value == @value + + __tostring: => + value = if @meta.name + @meta.name + else if 'table' == (type @value) and rawget @value, '__base' + @value.__name + else + tostring @value + "<#{@@__name} #{@type}: #{value}>" + + --- Stream metatype. + -- + -- @tfield string metatype + metatype: 'value' + + --- the type name of this stream. + -- + -- the following builtin typenames are used: + -- + -- - `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 a `Builtin` subclass + -- - `fndef` - `value` is a `FnDef` instance + -- - `scope` - `value` is a `Scope` instance + -- + -- @tfield string type + + --- the wrapped Lua value. + -- @tfield any value + + --- documentation metadata. + -- + -- an optional table containing metadata for error messages and + -- documentation. The following keys are recognized: + -- + -- - `name`: optional name + -- - `summary`: single-line description (markdown) + -- - `examples`: optional list of single-line code examples + -- - `description`: optional full-text description (markdown) + -- + -- @tfield ?table meta + +--- AST interface +-- +-- `ValueStream` implements the `AST` interface. +-- @section ast + + --- evaluate this literal constant. + -- + -- Throws an error if `type` is not a literal (`num`, `str` or `sym`). + -- Returns an eval-time const result for `num` and `str`. + -- Resolves `sym`s in `scope` and returns a reference to them. + -- + -- @tparam Scope scope the scope to evaluate in + -- @treturn Result the evaluation result + eval: (scope) => + switch @type + when 'num', 'str' + Result value: @ + when 'sym' + Error.wrap "resolving symbol '#{@value}'", scope\get, @value + else + error "cannot evaluate #{@}" + + --- quote this literal constant. + -- + -- @treturn ValueStream self + quote: => @ + + --- stringify this literal constant. + -- + -- Throws an error if `raw` is not set. + -- + -- @treturn string the exact string this stream was parsed from + stringify: => assert @raw, "stringifying ValueStream that wasn't parsed" + + --- clone this literal constant. + -- + -- @treturn ValueStream self + clone: (prefix) => @ + +--- static functions +-- @section static + + --- construct a new ValueStream. + -- + -- @classmethod + -- @tparam string type the type name + -- @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) => super type + + unescape = (str) -> str\gsub '\\([\'"\\])', '%1' + --- create a capture-function (for parsing with Lpeg). + -- + -- @tparam string type the type name (one of `num`, `sym` or `str`) + -- @tparam string sep the seperator char (only for `str`) + @parse: (type, sep) => + switch type + when 'num' then (match) -> @ 'num', (tonumber match), match + when 'sym' then (match) -> @ 'sym', match, match + when 'str' then (match) -> @ 'str', (unescape match), sep .. match .. sep + + --- wrap a Lua value. + -- + -- Attempts to guess the type and wrap a Lua value. + -- + -- @tparam any val the value to wrap + -- @tparam[opt] string name the name of this value (for error logging) + -- @treturn ValueStream + @wrap: (val, name='(unknown)') -> + typ = switch type val + when 'number' then 'num' + when 'string' then 'str' + when 'table' + if rawget val, '__base' + -- a class + switch ancestor val + when base.Op then 'opdef' + when base.Builtin then 'builtin' + else + error "#{name}: cannot wrap class '#{val.__name}'" + elseif val.__class + -- an instance + switch ancestor val.__class + when scope.Scope then 'scope' + when base.FnDef then 'fndef' + when Stream then return val + else + error "#{name}: cannot wrap '#{val.__class.__name}' instance" + else + -- plain table + return ValueStream 'scope', scope.Scope.from_table val + else + error "#{name}: cannot wrap Lua type '#{type val}'" + + ValueStream typ, val + + --- create a constant number. + -- @tparam number num the number + -- @treturn ValueStream + @num: (num) -> ValueStream 'num', num, tostring num + + --- create a constant string. + -- @tparam string str the string + -- @treturn ValueStream + @str: (str) -> ValueStream 'str', str, "'#{str}'" + + --- create a constant symbol. + -- @tparam string sym the symbol + -- @treturn ValueStream + @sym: (sym) -> ValueStream 'sym', sym, sym + + --- create a constant boolean. + -- @tparam boolean bool the boolean + -- @treturn ValueStream + @bool: (bool) -> ValueStream '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 ValueStream + @meta: (args) -> + with ValueStream.wrap args.value + .meta = args.meta if args.meta + +class LiteralValue extends ValueStream + eval: => Result value: @ + +{ + :ValueStream + :LiteralValue +} diff --git a/alv/tag.moon b/alv/tag.moon new file mode 100644 index 0000000..904cd20 --- /dev/null +++ b/alv/tag.moon @@ -0,0 +1,122 @@ +---- +-- Identity provider for `Cell`s and `Builtin`s. +-- +-- Tags are one of: +-- - 'blank' (`[?]`, to be auto-assigned by the Copilot) +-- - literal (`[1]`) +-- - cloned (`[X.Y]`, obtained by cloning Y with parent X) +-- +-- @classmod Tag +import Registry from require 'alv.registry' + +local ClonedTag + +class DummyReg + destroy: => + + __tostring: => "<dummy>" + +dummy = DummyReg! + +class Tag +--- members +-- @section members + + --- obtain the registered value of the last eval-cycle. + -- + -- Obtain the value that was previously registered for this tag on the last + -- eval-cylce. + -- + -- @treturn ?any + last: => + if index = @index! + Registry.active!\last index + + --- register `expr` for this tag for the current eval cycle. + -- + -- Will mark blank tags for auto-assignment at the end of the eval cycle. + -- + -- @tparam any expr the value to register + register: (expr) => + if index = @index! + Registry.active!\register index, expr + else + Registry.active!\init @, expr + + --- create a copy of this tag scoped to a `parent` tag. + -- + -- Will mark blank tags for auto-assignment at the end of the eval cycle. + -- + -- @tparam Tag parent the parent tag + -- @treturn Tag the cloned tag + clone: (parent) => + -- ensure this tag is registered for the current eval cycle, + -- even if it is blank and has no associated value + if index = @index! + Registry.active!\register index, dummy, true + else + Registry.active!\init @, dummy + + assert parent, "need parent to clone!" + ClonedTag @, parent + + stringify: => if @value then "[#{@value}]" else '' + __tostring: => if @value then "#{@value}" else '?' + +--- internals for `Registry` +-- @section internals + + new: (@value) => + + --- get a unique index value for this Tag. + -- + -- The index is equal to `value` for simple tags and a path-like string for + -- cloned tags. + -- + -- @treturn ?number|string + index: => @value + + --- callback to set value for blank tags. + -- @tparam number value + set: (value) => + assert not @value, "#{@} is not blank" + @value = value + +--- static functions +-- @section static + + --- create a blank `Tag`. + -- + -- @treturn Tag + @blank: -> Tag! + + --- parse a `Tag` (for Lpeg parsing). + -- + -- @tparam string num the number-string + -- @treturn Tag + @parse: (num) -> Tag tonumber num + +class ClonedTag extends Tag + new: (@original, @parent) => + + index: => + orig = @original\index! + parent = @parent\index! + if orig and parent + "#{parent}.#{orig}" + + set: (value) => + assert @parent.value, "cloned tag #{@} set before parent" + @original\set value + + stringify: => error "cant stringify ClonedTag" + + __tostring: => + if @parent + "#{@parent}.#{@original}" + else + tostring @original + +{ + :Tag +} diff --git a/alv/version.moon b/alv/version.moon new file mode 100644 index 0000000..6cee1b1 --- /dev/null +++ b/alv/version.moon @@ -0,0 +1,17 @@ +---- +-- `alive` source code version information. +-- +-- @module version + +--- exports +-- @table exports +-- @tfield string tag the last versions git tag +-- @tfield string web the repo web URL +-- @tfield string repo the git repo URL +-- @tfield string release the web URL of this release +{ + tag: "v0.1-rc2" + web: "https://github.com/s-ol/alivecoding" + repo: "https://github.com/s-ol/alivecoding.git" + release: "https://github.com/s-ol/alivecoding/releases/tag/v0.1-rc2" +} |
