aboutsummaryrefslogtreecommitdiffstats
path: root/core
diff options
context:
space:
mode:
authors-ol <s-ol@users.noreply.github.com>2020-03-02 19:08:48 +0000
committers-ol <s-ol@users.noreply.github.com>2020-03-02 19:09:56 +0000
commitc26b90317cb67ddcf71237e1d2f5fad87a5a3191 (patch)
tree3577416ff058cbd5e955c33693684a2b78c0c364 /core
parentfind IO via Result tree, not Registry (diff)
downloadalive-c26b90317cb67ddcf71237e1d2f5fad87a5a3191.tar.gz
alive-c26b90317cb67ddcf71237e1d2f5fad87a5a3191.zip
document more interfaces
Diffstat (limited to 'core')
-rw-r--r--core/base.moon225
-rw-r--r--core/cell.moon4
-rw-r--r--core/init.moon8
3 files changed, 151 insertions, 86 deletions
diff --git a/core/base.moon b/core/base.moon
index 41ddd4c..8bd6271 100644
--- a/core/base.moon
+++ b/core/base.moon
@@ -1,58 +1,29 @@
-- base definitions for extensions
import Value, Result from require 'core.value'
+import match from require 'core.pattern'
unpack or= table.unpack
-class Input
- new: (value) =>
- assert value, "nil passed to Input: #{value}"
- @stream = switch value.__class
- when Result
- assert value.value, "Input from result without value!"
- when Value
- value
- else
- error "Input from unknown value: #{value}"
-
- merge: (previous) =>
-
- finish_setup: =>
- dirty: => @stream\dirty!
- unwrap: => @stream\unwrap!
- type: => @stream.type
-
- __call: => @stream\unwrap!
- __tostring: => "#{@@__name}:#{@stream}"
- __inherited: (cls) =>
- cls.__base.__call = @__call
- cls.__base.__tostring = @__tostring
-
--- ColdInput scheduling policy
+-- an incoming side-effect adapter, polled by the main event loop to pump
+-- events into the dataflow graph.
--
--- never marked dirty
-class ColdInput extends Input
- dirty: => false
-
--- ValueInput scheduling policy
+-- subclasses must implement this interface:
--
--- during setup, only marked dirty if old and new stream differ in value
-class ValueInput extends Input
- merge: (old) => @dirty_setup = not old or @stream != old.stream
- finish_setup: => @dirty_setup = false
- dirty: => @dirty_setup or @stream\dirty!
-
--- EventInput scheduling policy
+-- :new() - construct a new instance
--
--- only marked dirty if the input stream itself is dirty
-class EventInput extends Input
-
--- IOInput scheduling policy
+-- must prepare the instance for :dirty().
+--
+-- :tick() - poll for changes
+--
+-- called every frame by the event loop to update internal state.
+--
+-- :dirty() - whether this adapter requires processing
+--
+-- must return a boolean indicating whether `Op`s that refer to this instance
+-- via `IOInput` should be notified (via `Op:tick()`). May be called multiple
+-- multiple times. May be called before :tick() on the first frame after
+-- construction.
--
--- lifts streams of IO objects to events
-class IOInput extends Input
- impure: true
- dirty: => @stream\unwrap!\dirty!
-
class IO
-- called in the main event loop
tick: =>
@@ -62,20 +33,55 @@ class IO
-- a persistent expression Operator
--
--- accepts Const or Stream inputs and produces a Stream output
+-- subclasses must implement this interface:
+--
+-- :new() - construct a new instance
+--
+-- the super-constructor can be used to construct a `Value` instance in @out.
+--
+-- :setup(inputs, scope) - 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 `match` to parse them, then delegate to
+-- super to patch the `Input` instances.
+--
+-- :tick(setup) - handle incoming events and update @out
+--
+-- called once per frame if any inputs are dirty. Some `Input`s (like
+-- `ValueInput`) have special behaviour immediately after :setup(). You can
+-- detect this using the `setup` parameter, which is true the first time
+-- :tick() is called after :setup(). :tick() is not called immediately after
+-- :setup() if no `@inputs` are dirty. Update @out here.
+--
+-- :destroy() - called when the Op is destroyed
+--
+-- .out - a `Value` instance representing this Op's computed output value.
+--
+-- @out must be set to a `Value` instance once :setup() finishes. @out must
+-- not change type, be removed or replaced outside of :new() and :setup().
+-- @out should have a value assigned via :set() or the `Value` constructor
+-- once :tick(true) is called. 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 a
+-- `ValueInput`.
+--
class Op
+-- super-implementations for extensions
+ -- 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.
new: (type, init) =>
- @impulses = {}
-
if type
@out = Value type, init
- -- (re)-initialize this Op with the given inputs
- -- after this method finishes, :tick(true) is called once, after which
- -- @impulses and @out have to be set and may not change until :setup()
- -- is called again.
+ -- setups previous @inputs, if any, with the new inputs, and writes 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.
setup: do
- do_merge = (old, cur) ->
+ do_setup = (old, cur) ->
for k, cur_val in pairs cur
old_val = old and old[k]
@@ -85,16 +91,20 @@ class Op
if cur_plain and old_plain
-- both are tables, recurse
- do_merge old_val, cur_val
+ do_setup old_val, cur_val
elseif not (cur_plain or old_plain)
- -- both are streams (or nil), merge them
- cur_val\merge old_val
+ -- both are streams (or nil), setup them
+ cur_val\setup old_val
(inputs) =>
old_inputs = @inputs
@inputs = inputs
- do_merge old_inputs, @inputs
+ do_setup old_inputs, @inputs
+
+ tick: =>
+ destroy: =>
+-- utilities
-- iterate over the (potentially nested) inputs table
all_inputs: do
do_yield = (table) ->
@@ -106,15 +116,6 @@ class Op
=> coroutine.wrap -> do_yield @inputs
- -- called once per frame if any inputs or impulses are dirty, and once
- -- immediately after :setup(). 'first' will be true in the latter case.
- -- Should update @out.
- tick: (first) =>
-
- -- called once when the Op is destroyed
- destroy: =>
-
--- utilities
unwrap_all: do
do_unwrap = (value) ->
if value.__class
@@ -139,19 +140,17 @@ class Op
__tostring: => "<op: #{@@__name}>"
__inherited: (cls) => cls.__base.__tostring = @__tostring
--- a builtin / special form / cell-evaluation strategy
+-- a builtin / special form / cell-evaluation strategy.
--
--- responsible for quoting/evaluating subexpressions,
--- instantiating and patching Ops,
--- updating the current Scope,
--- etc.
+-- responsible for quoting/evaluating subexpressions, instantiating and patching
+-- Ops updating the current Scope, etc. See core.builtin and core.invoke for
+-- many examples.
class Action
-- head: the (:eval'd) head of the Cell to evaluate (a Const)
-- tag: the Tag of the expression to evaluate
new: (head, @tag) =>
@patch head
--- AST interface
-- * eval args
-- * perform scope effects
-- * patch nested exprs
@@ -202,23 +201,97 @@ class Action
__tostring: => "<#{@@__name} #{@head}>"
__inherited: (cls) => cls.__base.__tostring = @__tostring
--- a ALV function definition
+-- an ALV function definition
--
-- when called, expands its body with params bound to the fn arguments
-- (see core.invoke.fn-invoke)
class FnDef
-- params: sequence of (:quote'd) symbols, each naming a function parameter
-- body: (:quote'd) expression the function evaluates to
- -- scoe: the lexical scope the function was defined in (closure)
+ -- scope: the lexical scope the function was defined in (closure)
new: (@params, @body, @scope) =>
__tostring: =>
"(fn (#{table.concat [p\stringify! for p in *@params], ' '}) ...)"
+-- an update scheduling policy for `Op`.
+--
+-- subclasses must implement this interface:
+--
+-- :new(value) - create an instance
+--
+-- `value` is either a `Value` or a `Result` instance and should be unwrapped.
+--
+-- :setup(prev) - copy state from old instance
+--
+-- called by `Op:setup()` with another `Input` instance or `nil` once this instance is
+-- registered. Must prepare this instance for :dirty().
+--
+--- :dirty() - whether this input requires processing
+--
+-- must return a boolean indicating whether `Op`s that refer to this instance
+-- should be notified (via `Op:tick()`).
+--
+-- :finish_setup() - leave setup state
+--
+-- called after the Op has completed (or skipped) its first `Op:tick()` after
+-- `Op:setup()`. Must prepare this instance for dataflow operation.
+--
+class Input
+ new: (value) =>
+ assert value, "nil passed to Input: #{value}"
+ @stream = switch value.__class
+ when Result
+ assert value.value, "Input from result without value!"
+ when Value
+ value
+ else
+ error "Input from unknown value: #{value}"
+
+ setup: (previous) =>
+
+ finish_setup: =>
+ dirty: => @stream\dirty!
+ unwrap: => @stream\unwrap!
+ type: => @stream.type
+
+ __call: => @stream\unwrap!
+ __tostring: => "#{@@__name}:#{@stream}"
+ __inherited: (cls) =>
+ cls.__base.__call = @__call
+ cls.__base.__tostring = @__tostring
+
+-- Never marked dirty. Use this for input streams that are only read when
+-- another input fires.
+class ColdInput extends Input
+ dirty: => false
+
+-- Marked dirty for the setup-tick if old and new stream differ in current
+-- value. This is the most common `Input` strategy. Should be used whenever a
+-- value denotes state.
+class ValueInput extends Input
+ setup: (old) => @dirty_setup = not old or @stream != old.stream
+ finish_setup: => @dirty_setup = false
+ dirty: => @dirty_setup or @stream\dirty!
+
+-- Only marked dirty if the input stream itself is dirty. Should be used
+-- whenever a value denotes a momentary event or impulse.
+class EventInput extends Input
+
+-- Marked dirty when an IO object is dirty. Must be used for IO values.
+class IOInput extends Input
+ impure: true
+ dirty: => @stream\unwrap!\dirty!
+
{
- :ValueInput, :EventInput, :IOInput, :ColdInput
:IO
:Op
:Action
:FnDef
+
+ :ValueInput, :EventInput, :IOInput, :ColdInput
+
+ -- redundant exports, to keep anything an extension might need in one import
+ :Value, :Result
+ :match
}
diff --git a/core/cell.moon b/core/cell.moon
index c052d46..724b74e 100644
--- a/core/cell.moon
+++ b/core/cell.moon
@@ -23,8 +23,8 @@ class Cell
Action\eval_cell scope, @tag, head, @tail!
- -- quoting a Cell recursively quotes children, but preserves identity this
- -- means that a quoted Cell may only be 'used' once. use :clone() otherwise.
+ -- quoting a Cell recursively quotes children, but preserves identity. This
+ -- means that a quoted Cell may only be 'used' once. Use :clone() otherwise.
quote: (scope) =>
children = [child\quote scope for child in *@children]
Cell @tag, children, @white
diff --git a/core/init.moon b/core/init.moon
index 6a8c5d4..73f63f9 100644
--- a/core/init.moon
+++ b/core/init.moon
@@ -1,9 +1,5 @@
L or= setmetatable {}, __index: => ->
-import Op, IO, Action, FnDef, EventInput, ValueInput, IOInput, ColdInput
- from require 'core.base'
-import match from require 'core.pattern'
-
import Value, Result, load_ from require 'core.value'
import Scope from require 'core.scope'
load_!
@@ -19,10 +15,6 @@ globals = Scope.from_table require 'core.builtin'
{
:Value, :Result
:Cell, :RootCell
-
- :Op, :IO, :Action, :FnDef
- :EventInput, :ValueInput, :IOInput, :ColdInput,
- :match
:Scope
:Registry, :Tag