aboutsummaryrefslogtreecommitdiffstats
path: root/alv
diff options
context:
space:
mode:
Diffstat (limited to 'alv')
-rw-r--r--alv/ast.moon40
-rw-r--r--alv/base/builtin.moon96
-rw-r--r--alv/base/fndef.moon26
-rw-r--r--alv/base/init.moon39
-rw-r--r--alv/base/input.moon146
-rw-r--r--alv/base/match.moon302
-rw-r--r--alv/base/op.moon168
-rw-r--r--alv/builtin.moon333
-rw-r--r--alv/cell.moon189
-rw-r--r--alv/config.ld16
-rw-r--r--alv/copilot.moon85
-rw-r--r--alv/cycle.moon26
-rw-r--r--alv/error.moon94
-rw-r--r--alv/extensions.md221
-rw-r--r--alv/init.moon66
-rw-r--r--alv/invoke.moon124
-rw-r--r--alv/logger.moon82
-rw-r--r--alv/parsing.moon73
-rw-r--r--alv/registry.moon145
-rw-r--r--alv/result.moon126
-rw-r--r--alv/scope.moon118
-rw-r--r--alv/stream/base.moon68
-rw-r--r--alv/stream/event.moon100
-rw-r--r--alv/stream/init.moon18
-rw-r--r--alv/stream/io.moon55
-rw-r--r--alv/stream/value.moon237
-rw-r--r--alv/tag.moon122
-rw-r--r--alv/version.moon17
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"
+}