This commit is contained in:
Aaron Smith 2023-09-14 18:15:54 +01:00
parent bdd725659e
commit d77fe0f92f
34 changed files with 1215 additions and 566 deletions

View file

@ -26,6 +26,7 @@ export default defineConfig({
items: [
{ text: "Reactivity: Core", link: "/api/reactivity-core" },
{ text: "Reactivity: Utility", link: "/api/reactivity-utility" },
{ text: "Reactivity: Control Flow", link: "/api/reactivity-flow" },
{ text: "Element Creation", link: "/api/creation" },
{ text: "Animation", link: "/api/animation" },
{ text: "Strict Mode", link: "/api/strict-mode" },
@ -41,16 +42,26 @@ export default defineConfig({
{ text: "Element Creation", link: "/tut/crash-course/2-creation" },
{ text: "Components", link: "/tut/crash-course/3-components" },
{ text: "Source", link: "/tut/crash-course/4-source" },
{ text: "Derived Source", link: "/tut/crash-course/5-derived-source" },
{ text: "Table Source", link: "/tut/crash-course/6-table-source" },
{ text: "Nested Properties", link: "/tut/crash-course/7-nested-properties" },
{ text: "Actions", link: "/tut/crash-course/8-actions" },
{ text: "Effect", link: "/tut/crash-course/5-effect" },
{ text: "Derived Source", link: "/tut/crash-course/6-derived-source" },
{ text: "Cleanup", link: "/tut/crash-course/7-cleanup" },
{ text: "Control Flow", link: "/tut/crash-course/8-control-flow" },
{ text: "Property Nesting", link: "/tut/crash-course/9-property-nesting" },
{ text: "Actions", link: "/tut/crash-course/10-actions" },
]
},
{
text: "Tutorials",
text: "Control Flow WIP",
items: [
{ text: "Crash Course", link: "/tut/crash-course/index" },
{ text: "switch", link: "/tut/control-flow/switch.md" },
{ text: "indexes", link: "/tut/control-flow/indexes.md" },
{ text: "values", link: "/tut/control-flow/values.md" },
]
},
{
text: "Advanced Reactivity WIP",
items: [
{ text: "reactive-scopes", link: "/tut/reactive-scoping.md"}
]
}
],

View file

@ -2,6 +2,38 @@
<br/>
## mount()
Runs a function and applies its result to a target instance.
- **Type**
```lua
function mount<T>(component: () -> T, target: Instance?): () -> ()
```
- **Details**
The result of the function is applies to the target in the same way
properties are using `create()`.
The function is ran in a new reactive scope, just like
[root()](reactivity-core.md#root).
Returns a function that when called will destroy the reactive scope.
- **Example**
```lua
local function App()
return create "ScreenGui" {
create "TextLabel" { Text = "Vide" }
}
end
mount(App, game.StarterGui)
```
## create()
Creates a new UI element, applying any given properties.
@ -28,19 +60,18 @@ Creates a new UI element, applying any given properties.
- **Property setting rules**
- If a table index is a string:
- If its value is a table then it will attempt to perform aggregate
initialization.
- If its value is a function then it will either bind that property to
the function or connect it if the property type is a `RBXScriptSignal`.
- If the value is not a function then the property will be set to that
value.
- If a table index is a number:
- If its value is an action then that action will be queued to run after
properties are set.
- If its value is a table then that table will be recursively
- processed just like the outer table.
- If its value is a function then it will parent and bind any instances
returned by that function as children.
processed just like the outer table.
- If its value is a function then it will bind the instances children to
that function.
- If its value is an instance then it will be parented to the instance.
- **Example**
@ -54,7 +85,7 @@ Creates a new UI element, applying any given properties.
}
```
A component using property nesting/grouping.
A component using property nesting.
```lua
type Layout = {

View file

@ -22,6 +22,10 @@ Creates and runs a function in a new reactive scope.
Also returns a function to destroy the root, which will run any cleanups
and allow derived sources created to garbage collect.
::: warning
`fn()` cannot yield.
:::
## source()
Creates a new source with the given value.
@ -52,42 +56,37 @@ Creates a new source with the given value.
## effect()
Runs a callback on source update.
Runs a side-effect on source update.
- **Type**
```lua
function effect(source: () -> ()): Uneffect
type Uneffect = () -> ()
function effect(callback: () -> ())
```
- **Details**
The source callback is ran immediately to determine what states are
referenced.
The callback is ran immediately.
Any time a source referenced in the callback is changed, the callback will
be reran.
Also returns a function that when called, stops the effecter immediately.
::: warning
`source()` cannot yield.
`callback()` cannot yield.
:::
- **Example**
```lua
local state = source(1)
local num = source(1)
effect(function()
print(state())
print(num())
end)
-- prints 1
state(state() + 1)
num(num() + 1)
-- prints 2
```
@ -130,150 +129,4 @@ Derives a new source from existing sources.
text() -- "count: 1"
```
## indexes()
Maps each index in a table source to an object.
- **Type**
```lua
function indexes<KI, VI, VO>(
source: () -> Map<KI, VI>,
transform: (value: () -> VI, index: KI) -> VO
): Array<VO>
- **Details**
The transform function is called only ever *once* for each index in the
source table. The first argument is a source containing the index's value
and the second argument is just the index.
Anytime a new index is added, the transform function will be called again
for that new index.
Anytime an existing index value changes, the transform function is not rerun,
instead the source value for that index will update, causing anything
depending on it to update too.
Returns a state containing an array of all objects returned by the
transform.
::: warning
`transform()` cannot yield.
:::
- **Example**
The intended purpose of this function is to map each index in a table to
a UI element.
```lua
type Item = {
name: string,
icon: number
}
local items = source {} :: () -> Array<Item>
local displays = indexes(items, function(item, i)
return ItemDisplay {
Name = function()
return item().name
end,
Image = function()
return "rbxassetid://" .. item().icon
end,
LayoutOrder = i
}
end)
```
## values()
Maps each value in a table source to an object.
- **Type**
```lua
function values<KI, VI, VO>(
source: () -> Map<KI, VI>,
transform: (value: VI, index: () -> KI) -> VO
): Array<VO>
- **Details**
The transform function is called only ever *once* for each value in the
source table. The first argument is the index's value and
the second argument is a source containing the index.
Anytime a new value is added, the transform function will be called again
for that new value.
Anytime an existing value's index changes, the transform function is not
rerun, instead the source index for that value will update, causing anything
depending on it to update too.
Returns a state containing an array of all objects returned by the
transform.
::: warning
`transform()` cannot yield.
:::
::: warning
Having primitive values in the source table can cause unexpected behavior,
as duplicate primitives can result in multiple index sources being bound
to the same UI element.
:::
- **Example**
The intended purpose of this function is to map each value in a table to
a UI element.
```lua
type Item = {
name: string,
icon: number
}
local items = source {} :: () -> Array<Item>
local displays = values(items, function(item, i)
return ItemDisplay {
Name = item.Name
Image = "rbxassetid://" .. item.icon,
LayoutOrder = i
}
end)
```
- **Extra**
When should you use `indexes()` and `values()`?
`values()` should be used when you have a fixed set of objects where the
same objects can be re-arranged in the source table. It maps a value to a
UI element.
e.g.
- List of all players.
- Inventory of items.
- Chat message history.
- Toast notifications.
`indexes()` should be used in other cases, especially when your source table
has primitive value. It maps an index to a UI element.
e.g.
- List of character or weapon stats.
In most cases, both functions will appear to have the same behavior.
The main difference is performance, picking the right function to use can
result in less property updates and less re-renders.
--------------------------------------------------------------------------------

View file

@ -0,0 +1,186 @@
# Reactivity API: Control Flow
<br/>
## switch()
Changes object based on a source and a mapping table.
- **Type**
```lua
function switch<K, V>(source: () -> K): (map: Map<K, () -> V>) -> V?
```
- **Details**
The mapped function is ran in a new reactive scope that is destroyed when
the source changes and maps to a different function.
::: warning
Mapped functions cannot yield.
:::
- **Example**
```lua
local logged = source(false)
local button = switch(logged) {
[true] = function()
return Button { Text = "Log out", Toggle = logged }
end,
[false] = function()
return Button { Text = "Log in", Toggle = logged }
end
}
```
## indexes()
Maps each index in a table source to an object.
- **Type**
```lua
function indexes<KI, VI, VO>(
source: () -> Map<KI, VI>,
transform: (value: () -> VI, index: KI) -> VO
): Array<VO>
- **Details**
The transform function is called only ever *once* for each index in the
source table. The first argument is a source containing the index's value
and the second argument is just the index.
Anytime a new index is added, the transform function will be called again
for that new index.
Anytime an existing index value changes, the transform function is not rerun,
instead the source value for that index will update, causing anything
depending on it to update too.
Returns a state containing an array of all objects returned by the
transform.
::: warning
`transform()` cannot yield.
:::
- **Example**
The intended purpose of this function is to map each index in a table to
a UI element.
```lua
type Item = {
name: string,
icon: number
}
local items = source {} :: () -> Array<Item>
local displays = indexes(items, function(item, i)
return ItemDisplay {
Name = function()
return item().name
end,
Image = function()
return "rbxassetid://" .. item().icon
end,
LayoutOrder = i
}
end)
```
## values()
Maps each value in a table source to an object.
- **Type**
```lua
function values<KI, VI, VO>(
source: () -> Map<KI, VI>,
transform: (value: VI, index: () -> KI) -> VO
): Array<VO>
- **Details**
The transform function is called only ever *once* for each value in the
source table. The first argument is the index's value and
the second argument is a source containing the index.
Anytime a new value is added, the transform function will be called again
for that new value.
Anytime an existing value's index changes, the transform function is not
rerun, instead the source index for that value will update, causing anything
depending on it to update too.
Returns a state containing an array of all objects returned by the
transform.
::: warning
`transform()` cannot yield.
:::
::: warning
Having primitive values in the source table can cause unexpected behavior,
as duplicate primitives can result in multiple index sources being bound
to the same UI element.
:::
- **Example**
The intended purpose of this function is to map each value in a table to
a UI element.
```lua
type Item = {
name: string,
icon: number
}
local items = source {} :: () -> Array<Item>
local displays = values(items, function(item, i)
return ItemDisplay {
Name = item.Name
Image = "rbxassetid://" .. item.icon,
LayoutOrder = i
}
end)
```
- **Extra**
When should you use `indexes()` and `values()`?
`values()` should be used when you have a fixed set of objects where the
same objects can be re-arranged in the source table. It maps a value to a
UI element.
e.g.
- List of all players.
- Inventory of items.
- Chat message history.
- Toast notifications.
`indexes()` should be used in other cases, especially when your source table
has primitive value. It maps an index to a UI element.
e.g.
- List of character or weapon stats.
In most cases, both functions will appear to have the same behavior.
The main difference is performance, picking the right function to use can
result in less property updates and less re-renders.
--------------------------------------------------------------------------------

View file

@ -69,3 +69,5 @@ Runs a given function where any sources read will not track its reactive scope.
a(1)
print(sum()) -- 2
```
--------------------------------------------------------------------------------

View file

@ -12,14 +12,14 @@ and identifying improper usage.
Currently, strict mode will:
1. Run derived sources twice a source updates.
2. Run effecters twice when a source updates.
2. Run effects twice when a source updates.
3. Throw an error if yields occur where they are not allowed.
4. Checks for `indexes()` and `values()` returning primitive values.
5. Checks for duplicate nested properties at same depth.
6. Better error reporting and stack traces.
7. Checks for multiple `cleanup()` calls in the same function scope.
By rerunning sources and effecters, any side-effects are made more apparent.
By rerunning sources and effects, any side-effects are made more apparent.
This also helps ensure that cleanups are being handled correctly.
Accidental yielding within reactive scopes can break Vide's reactive graph,

View file

@ -0,0 +1,66 @@
# Control Flow
Vide has specific functions for dealing with sources that store a table value.
Often, you will have a table of values that will be displayed in a similar
manner. Rather than manually looping over each value to generate a corresponding
UI element, Vide provides functions `indexes()` and `values()` to do this for
you.
`indexes()` maps each *index* in a table to a UI element.
```lua
local names = source { "a", "b", "c" }
local elements = indexes(names, function(name, i)
return create "TextLabel" {
Text = function()
return "Name: " .. name()
end,
LayoutOrder = i
}
end)
```
What happens here is the given callback is only ever ran *once* for each index
in the table. The callback receives two arguments, a *source* containing the
index's value and then the index itself.
Anytime the value at a corresponding index changes, the source for that index
value is updated, causing the UI element depending on it to update too.
`values()` behaves similarly, except it maps each *value* in a table to a UI
element.
```lua
type Item = {
Name: string,
Icon: number
}
local items = source({} :: Array<Item>)
local elements = values(items, function(item, i)
return create "ImageLabel" {
Image = "rbxassetid://" .. item.Icon,
LayoutOrder = i
}
end)
```
The callback is again only ever ran *once* for each value in the table. The
callback receives two arguments, a value in the table and then a *source*
containing the value's corresponding index.
Any time a value in a table changes index, the source for that value is updated,
causing the UI element position to change.
In certain cases `values()` can cause less recalculation and rerenders than
`indexes()` like when items are re-arranged and shifted within a table.
It is important that each value in a table is unique when using `values()`,
and for this reason always using `indexes()` if a table contains primitive
values.
Both `indexes()` and `values()` return an array of all mapped UI elements.

View file

@ -0,0 +1,66 @@
# Control Flow
Vide has specific functions for dealing with sources that store a table value.
Often, you will have a table of values that will be displayed in a similar
manner. Rather than manually looping over each value to generate a corresponding
UI element, Vide provides functions `indexes()` and `values()` to do this for
you.
`indexes()` maps each *index* in a table to a UI element.
```lua
local names = source { "a", "b", "c" }
local elements = indexes(names, function(name, i)
return create "TextLabel" {
Text = function()
return "Name: " .. name()
end,
LayoutOrder = i
}
end)
```
What happens here is the given callback is only ever ran *once* for each index
in the table. The callback receives two arguments, a *source* containing the
index's value and then the index itself.
Anytime the value at a corresponding index changes, the source for that index
value is updated, causing the UI element depending on it to update too.
`values()` behaves similarly, except it maps each *value* in a table to a UI
element.
```lua
type Item = {
Name: string,
Icon: number
}
local items = source({} :: Array<Item>)
local elements = values(items, function(item, i)
return create "ImageLabel" {
Image = "rbxassetid://" .. item.Icon,
LayoutOrder = i
}
end)
```
The callback is again only ever ran *once* for each value in the table. The
callback receives two arguments, a value in the table and then a *source*
containing the value's corresponding index.
Any time a value in a table changes index, the source for that value is updated,
causing the UI element position to change.
In certain cases `values()` can cause less recalculation and rerenders than
`indexes()` like when items are re-arranged and shifted within a table.
It is important that each value in a table is unique when using `values()`,
and for this reason always using `indexes()` if a table contains primitive
values.
Both `indexes()` and `values()` return an array of all mapped UI elements.

View file

@ -0,0 +1,66 @@
# Control Flow
Vide has specific functions for dealing with sources that store a table value.
Often, you will have a table of values that will be displayed in a similar
manner. Rather than manually looping over each value to generate a corresponding
UI element, Vide provides functions `indexes()` and `values()` to do this for
you.
`indexes()` maps each *index* in a table to a UI element.
```lua
local names = source { "a", "b", "c" }
local elements = indexes(names, function(name, i)
return create "TextLabel" {
Text = function()
return "Name: " .. name()
end,
LayoutOrder = i
}
end)
```
What happens here is the given callback is only ever ran *once* for each index
in the table. The callback receives two arguments, a *source* containing the
index's value and then the index itself.
Anytime the value at a corresponding index changes, the source for that index
value is updated, causing the UI element depending on it to update too.
`values()` behaves similarly, except it maps each *value* in a table to a UI
element.
```lua
type Item = {
Name: string,
Icon: number
}
local items = source({} :: Array<Item>)
local elements = values(items, function(item, i)
return create "ImageLabel" {
Image = "rbxassetid://" .. item.Icon,
LayoutOrder = i
}
end)
```
The callback is again only ever ran *once* for each value in the table. The
callback receives two arguments, a value in the table and then a *source*
containing the value's corresponding index.
Any time a value in a table changes index, the source for that value is updated,
causing the UI element position to change.
In certain cases `values()` can cause less recalculation and rerenders than
`indexes()` like when items are re-arranged and shifted within a table.
It is important that each value in a table is unique when using `values()`,
and for this reason always using `indexes()` if a table contains primitive
values.
Both `indexes()` and `values()` return an array of all mapped UI elements.

View file

@ -19,11 +19,11 @@ Some of the main focuses behind Vide's design choices:
- Reducing the amount of imports needed for usage by leveraging Luau's syntax
and semantics.
- Being completely typecheckable.
- Flexibility, particularly with integrating other libraries and allowing users
to use their own patterns.
- Flexibility with integrating other libraries and allowing users to use their
own patterns.
- Independence from instance lifetimes.
- A powerful reactive system that can surgically update properties as a result
of state changes.
- A powerful reactive system that can update specific properties as a result of
state changes, updates are immediate with no diffing needed.
## Structure Of A Vide App

View file

@ -40,3 +40,6 @@ the updating of the Text property anytime the count is changed.
All observable changes to the user are considered to be side-effects of the
reactive system.
You should not update other sources using an effect. Improper usage can lead to
unecessary updates and infinite loops.

View file

@ -1,66 +1,95 @@
# Control Flow
Vide has specific functions for dealing with sources that store a table value.
Eventually you will need a way to dynamically create and destroy UI elements
resulting from state changes. Vide provides functions to help you do this,
known as *control flow* functions.
These functions return a new source, which holds the instances to be displayed.
These sources can be assigned as children, meaning the displayed children
will update when the input source updates.
One of these functions is `switch()`, used to conditionally show one of a set of
components.
```lua
local vide = require(vide)
local source = vide.source
local switch = vide.switch
local function ToggleButton(p: {
Text: string,
Toggle: (boolean) -> boolean
})
return create "TextButton" {
Size = UDim2.fromOffset(300, 300),
Text = p.Text,
Activated = function()
p.Toggle(not p.Toggle())
end
}
end
local loggedIn = source(false)
local function LoginMenu()
return Frame {
switch(loggedIn) {
[true] = function()
return ToggleButton { Text = "Log out", Toggle = loggedIn }
end,
[false] = function()
return ToggleButton { Text = "Log in", Toggle = loggedIn }
end
}
}
end
mount(function() return create "ScreenGui" { LoginMenu {} } end, game.StarterGui)
```
Above is an example of using a switch to create a login menu. Each time
`loggedIn` toggles, the current button will be destroyed, and a new button
created, which the text to represent the current action, to log in or log out.
Another control flow function, `indexes()`, is used to create elements from an
input table.
Often, you will have a table of values that will be displayed in a similar
manner. Rather than manually looping over each value to generate a corresponding
UI element, Vide provides functions `indexes()` and `values()` to do this for
you.
`indexes()` maps each *index* in a table to a UI element.
UI element, `indexes()` can autmatically run a transform function for each
index and value, generating a UI element.
```lua
local names = source { "a", "b", "c" }
local todoList = {
"Finish the crash course",
"Star vide's GitHub"
}
local elements = indexes(names, function(name, i)
local elements = indexes(todoList, function(todo, i)
return create "TextLabel" {
Text = function()
return "Name: " .. name()
return i .. ": " .. todo()
end,
LayoutOrder = i
}
end)
```
What happens here is the given callback is only ever ran *once* for each index
in the table. The callback receives two arguments, a *source* containing the
index's value and then the index itself.
Anytime the value at a corresponding index changes, the source for that index
value is updated, causing the UI element depending on it to update too.
`values()` behaves similarly, except it maps each *value* in a table to a UI
element.
```lua
type Item = {
Name: string,
Icon: number
}
local items = source({} :: Array<Item>)
local elements = values(items, function(item, i)
return create "ImageLabel" {
Image = "rbxassetid://" .. item.Icon,
LayoutOrder = i
mount(function()
return create "ScreenGui" {
create "UIListLayout" {}, elements
}
end)
end, game.StarterGui)
```
The callback is again only ever ran *once* for each value in the table. The
callback receives two arguments, a value in the table and then a *source*
containing the value's corresponding index.
For each unique index in the passed table, the transform function will be called
with 1. a source containing the value of the index, 2. the index itself.
Any time a value in a table changes index, the source for that value is updated,
causing the UI element position to change.
When the value at an index is changed, the function is not reran. Instead, the
given source is updated instead.
In certain cases `values()` can cause less recalculation and rerenders than
`indexes()` like when items are re-arranged and shifted within a table.
`indexes()` is said to map each *index* in a table to a UI element, each index
has a single corresponding element.
It is important that each value in a table is unique when using `values()`,
and for this reason always using `indexes()` if a table contains primitive
values.
Both `indexes()` and `values()` return an array of all mapped UI elements.
An element is only destroyed if the value of an index is set to `nil`.

View file

@ -0,0 +1,61 @@
# Reactive Scoping
This is a brief document designed to give the user more insight into how Vide's
reactive graph works.
Each time you create and derive sources, a new node representing that source is
created and added to the reactive graph. Each node stores a value and a
side-effect function. Each node also keeps track of its parents and children,
as well as any cleanups registered.
Any time a node is updated, Vide will traverse and update that node's children,
its children's children, etc, until all nodes descending from that node has been
updated. Traversal will stop at a node if that node's cached value does not
change after an update.
For every node that is updated, a scope is opened for that node. These scopes
are referred to as "reactive scopes". Any source read from within a node's scope
will that node as a child. This is similar to cleanups, anytime a cleanup is
registered, it is added to the node of the currently active scope.
The way Vide tracks reactive scopes, is by using a stack of nodes. The current
active reactive scope is the node at the top of this stack.
When destroying a node, its descendents are traversed and also destroyed.
When being destroyed, a node's connections (parents and children) are cleared,
and any pending cleanup functions are ran.
The purpose of `root()` (which is called internally by `mount()`) is to setup
the root node which will track any node created or derived inside its scope, or
any cleanups registered. Without it, nodes could be garbage collected without a
chance to run pending cleanups which can cause memory leakage.
Control flow functions in Vide are special, as they can dynamically create and
destroy new root scopes.
It is the combination of the above which allows us to write components like so:
```lua
local function Counter()
local count = source(0)
local connection = stepped:Connect(function() count(count() + 1) end)
cleanup(function() connection:Disconnect() end)
effect(function() print(count()) end)
return create "TextLabel" { Text = count }
end
```
Vide doesn't recognise this as a "component", that is a user abstraction. Vide
just sees this as a function that creates nodes in the reactive graph.
Whenever the reactive scope that calls this function is destroyed, like by a
control flow function, the registered cleanup will be called, and the effect
(which is just a node on the reactive graph) is destroyed. The returned instance
and the bound `count` source is just considered to be a side-effect, and with
the reactive scope from which the side-effects stem from destroyed, the instance
can be garbage collected - everything is nicely cleaned up.
> todo: add graphics

View file

@ -3,7 +3,7 @@ type Action = {
callback: (Instance) -> ()
}
local ActionMT = {}
local ActionMT = table.freeze {}
local function is_action(v: any)
return getmetatable(v) == ActionMT

View file

@ -1,6 +1,7 @@
if not game then script = require "test/relative-string" end
local throw = require(script.Parent.throw)
local trace = require(script.Parent.trace)
local flags = require(script.Parent.flags)
local graph = require(script.Parent.graph)
type Node<T> = graph.Node<T>
@ -9,34 +10,11 @@ local get_scope = graph.get_scope
local evaluate_node = graph.evaluate_node
local set_owner = graph.set_owner
-- todo: replace with throw's method
local root do
local src = debug.info(1, "s")
root = string.sub(src, 1, #src - 5)
end
local function traceback(skips: number) -- ensures trace begins outside of any vide library file
local s = 1
repeat
s += 1
local path = debug.info(s, "s")
local found = not string.find(path, root)
if found then
skips -= 1
end
until found and skips < 0
return debug.traceback(nil, s)
end
function create_binding<T>(updater: (T) -> T, binding: T)
if flags.strict then
-- track bind creation trace
local fn = updater
local bind_trace = traceback(0)
local bind_trace = debug.traceback(nil, trace()-1)
updater = function(...)
local ok, result = xpcall(fn, function(err: string)
return err

View file

@ -5,12 +5,13 @@ local graph = require(script.Parent.graph)
local get_scope = graph.get_scope
local add_cleanup = graph.add_cleanup
local function cleanup(fn: () -> ())
local function cleanup(callback: () -> ())
local scope = get_scope()
if not scope then throw("cannot cleanup in a non-reactive scope") end
assert(scope)
if not scope then
throw("cannot cleanup in a non-reactive scope")
end; assert(scope)
add_cleanup(scope, fn)
add_cleanup(scope, callback)
end
return cleanup

View file

@ -7,11 +7,11 @@ local defaults = require(script.Parent.defaults)
local apply = require(script.Parent.apply)
local memoize = require(script.Parent.memoize)
local function create_instance(class_name: string)
local ok, instance: Instance = pcall(Instance.new, class_name :: any)
if not ok then throw(`invalid class name, could not create instance of class { class_name }`) end
local function create_instance(class: string)
local ok, instance: Instance = pcall(Instance.new, class :: any)
if not ok then throw(`invalid class name, could not create instance of class { class }`) end
local default: { [string]: unknown }? = defaults[class_name]
local default: { [string]: unknown }? = defaults[class]
if default then
for i, v in next, default do
(instance :: any)[i] = v

View file

@ -1,10 +1,11 @@
local Enum = game and Enum or require "test/mock".Enum :: never
local Color3 = game and Color3 or require "test/mock".Color3 :: never
local Vector3 = game and Vector3 or require "test/mock".Vector3 :: never
return {
Part = {
Material = Enum.Material.SmoothPlastic,
--Size = Vector3.new(1, 1, 1),
Size = Vector3.new(1, 1, 1),
Anchored = true
},

View file

@ -8,13 +8,13 @@ local track = graph.track
local get_scope = graph.get_scope
local evaluate_node = graph.evaluate_node
local function derive<T>(fn: () -> T): () -> T
local function derive<T>(source: () -> T): () -> T
local owner = get_scope()
if not owner then
throw("cannot derive in non-reactive scope")
end; assert(owner)
local node = create_node(false :: any, fn)
local node = create_node(false :: any, source)
set_owner(node, owner)
evaluate_node(node)

View file

@ -7,16 +7,16 @@ local get_scope = graph.get_scope
local evaluate_node = graph.evaluate_node
local set_owner = graph.set_owner
local function effect<T>(effect: (T) -> T, initial_value: T)
local function effect<T>(callback: (T) -> T, initial_value: T)
local owner = get_scope()
if not owner then
throw("cannot create effect in non-reactive scope")
end; assert(owner)
local node = create_node(initial_value, effect)
local node = create_node(initial_value, callback)
set_owner(node, owner)
evaluate_node(node)
end
return effect :: (<T>(effect: (T) -> T, initial_value: T) -> ()) & ((effect: () -> ()) -> ())
return effect :: (<T>(callback: (T) -> T, initial_value: T) -> ()) & ((callback: () -> ()) -> ())

View file

@ -16,10 +16,11 @@ export type Node<T> = {
[number]: Node<T>
}
-- reactive scope stack
local scopes = { n = 0 } :: { [number]: Node<any>, n: number }
-- runs a given callback in a context that Luau does not allow yielding in
local check_for_yield: <T...>(fn: (T...) -> (boolean, string?), T...) -> () do
local check_for_yield: <T...>(fn: (T...) -> (), T...) -> (boolean, string?) do
local t = { __mode = "kv" }
setmetatable(t, t)
@ -40,10 +41,6 @@ local check_for_yield: <T...>(fn: (T...) -> (boolean, string?), T...) -> () do
end
end
local function get_stack_scope(offset: number): Node<unknown>?
return scopes[scopes.n - offset]
end
local function get_scope(): Node<unknown>?
return scopes[scopes.n]
end
@ -120,8 +117,18 @@ end
local function evaluate_node<T>(node: Node<T>)
local cur_value = node.cache
run_cleanups(node) -- todo: move in scope?
if flags.strict then
run_cleanups(node)
open_scope(node)
local ok, err = check_for_yield(node.effect :: (T) -> T, cur_value)
close_scope()
if not ok then throw(err :: string) end
end
run_cleanups(node) -- todo: move in scope?
open_scope(node)
local ok, new_value = pcall(node.effect :: (T) -> T, cur_value)
@ -150,6 +157,7 @@ local function update<T>(node: StartNode<T>)
table.clear(update_queue)
end
-- unparent all children and queue for eval
do
local child = node[1]
while child do -- todo: case where child in owner context
@ -162,6 +170,7 @@ local function update<T>(node: StartNode<T>)
end
end
-- evaluate all queued children
for i = n0 + 1, n do
local child = update_queue[i]
if not child.effect then continue end
@ -178,36 +187,33 @@ end
local function track<T>(node: StartNode<T>)
local scope = get_scope()
if scope and scope.effect then -- todo
if scope and scope.effect then -- do not track nodes with no effect
add_child(node, scope)
end
end
local function create_node<T>(value: T, effect: false | (T) -> T): Node<T>
local node: Node<T> = {
return {
cache = value,
effect = effect,
cleanups = false :: false,
cleanups = false,
parents = {},
}
return node
end
local function get_children<T>(node: Node<T>): { Node<unknown> }
return { unpack(node) } :: { Node<any> }
end
local function create_start_node<T>(value: T): StartNode<T>
return { cache = value }
end
local function get_children<T>(node: Node<T>): { Node<unknown> }
return { unpack(node) } :: { Node<any> }
end
return table.freeze {
open_scope = open_scope,
close_scope = close_scope,
evaluate_node = evaluate_node,
get_scope = get_scope,
get_stack_scope = get_stack_scope,
add_cleanup = add_cleanup,
set_owner = set_owner,
destroy = destroy,
@ -217,5 +223,6 @@ return table.freeze {
add_child = add_child,
create_node = create_node,
create_start_node = create_start_node,
get_children = get_children
get_children = get_children,
scopes = scopes
}

View file

@ -1,7 +1,5 @@
if not game then script = require "test/relative-string" end
-- todo: more testing needed regarding `cleanup()` usage
local throw = require(script.Parent.throw)
local flags = require(script.Parent.flags)
local graph = require(script.Parent.graph)
@ -71,7 +69,7 @@ local function indexes<K, VI, VO>(input: () -> Map<K, VI>, transform: (() -> VI,
local cv = input_cache[i]
if cv ~= v then
if cv == nil then
if cv == nil then -- create new scope and run transform
local scope = create_node(false, false)
scopes[i] = scope :: Node<any>
@ -87,20 +85,22 @@ local function indexes<K, VI, VO>(input: () -> Map<K, VI>, transform: (() -> VI,
close_scope()
if not ok then error(result, 0) end
if not ok then
close_scope() -- subowner scope
error(result, 0)
end
input_nodes[i] = node
input_cache[i] = v
output_cache[i] = result
else
else -- update source
input_nodes[i].cache = v
update(input_nodes[i])
input_cache[i] = v
end
input_cache[i] = v
end
end
-- todo: handle early error
close_scope()
local output_array = table.create(#scopes)
@ -118,7 +118,6 @@ local function indexes<K, VI, VO>(input: () -> Map<K, VI>, transform: (() -> VI,
evaluate_node(node)
return function()
track(node)
return node.cache
@ -161,7 +160,7 @@ local function values<K, VI, VO>(input: () -> Map<K, VI>, transform: (VI, () ->
local cv = cur_input_cache[v]
if cv == nil then
if cv == nil then -- create new scope and run transform
local scope = create_node(false, false)
scopes[v] = scope :: Node<any>
@ -177,20 +176,23 @@ local function values<K, VI, VO>(input: () -> Map<K, VI>, transform: (VI, () ->
close_scope()
if not ok then error(result, 0) end
if not ok then
close_scope() -- subowner scope
error(result, 0)
end
input_nodes[v] = node
output_cache[v] = result
else
else -- update source
if cv ~= i then
input_nodes[v].cache = i
update(input_nodes[v])
end
cur_input_cache[v] = nil
end
end
-- todo: handle early error
close_scope()
-- remove old values

View file

@ -31,4 +31,4 @@ local function root<T>(fn: () -> T): (T, () -> ())
end
end
return root
return root :: (<T>(fn: () -> T) -> (T, () -> ())) & ((fn: () -> ()) -> (nil, () -> ()))

View file

@ -9,8 +9,6 @@ local update = graph.update
export type Source<T> = (() -> T) & ((T) -> T)
local function source<T>(initial_value: T): Source<T>
--assert(get_scope())
local node = create_start_node(initial_value)
return function(...): T

View file

@ -28,8 +28,7 @@ type StartNode<T> = graph.StartNode<T>
local create_node = graph.create_node
local create_start_node = graph.create_start_node
local get_scope = graph.get_scope
local open_scope = graph.open_scope
local close_scope = graph.close_scope
local evaluate_node = graph.evaluate_node
local update = graph.update
local set_owner = graph.set_owner
local track = graph.track
@ -43,6 +42,8 @@ local function Vec3(x: number?, y: number?, z: number?)
return Vector3.new(x, y, z)
end
local ZERO = Vec3(0, 0, 0)
type Animatable = number | CFrame | Color3 | UDim | UDim2 | Vector2 | Vector3
type SpringData<T> = {
@ -67,21 +68,20 @@ type Vec6ToType<T> = (Vec3, Vec3) -> T
local type_to_vec6 = {
number = function(v)
return Vec3(v, 0, 0), Vec3()
return Vec3(v, 0, 0), ZERO
end :: TypeToVec6<number>,
CFrame = function(v)
-- todo: proper rotation tween
return v.Position, Vec3(v:ToEulerAnglesXYZ())
end :: TypeToVec6<CFrame>,
Color3 = function(v)
-- todo: hsv
return Vec3(v.R, v.G, v.B), Vec3()
-- todo: hsv, oklab?
return Vec3(v.R, v.G, v.B), ZERO
end :: TypeToVec6<Color3>,
UDim = function(v)
return Vec3(v.Scale, v.Offset, 0), Vec3()
return Vec3(v.Scale, v.Offset, 0), ZERO
end :: TypeToVec6<UDim>,
UDim2 = function(v)
@ -89,11 +89,11 @@ local type_to_vec6 = {
end :: TypeToVec6<UDim2>,
Vector2 = function(v)
return Vec3(v.X, v.Y, 0), Vec3()
return Vec3(v.X, v.Y, 0), ZERO
end :: TypeToVec6<Vector2>,
Vector3 = function(v)
return v, Vec3()
return v, ZERO
end :: TypeToVec6<Vector3>,
Rect = function(v)
@ -151,25 +151,11 @@ setmetatable(springs, { __mode = "v" })
local function spring<T>(source: () -> T, period: number?, damping_ratio: number?): () -> T
local owner = get_scope()
if not owner then throw("cannot derive in non-reactive scope") end
assert(owner)
local updater = create_node(false)
updater.effect = true :: any -- todo
set_owner(updater, owner)
open_scope(updater)
local initial_value = source()
close_scope()
local vtype = typeof(initial_value)
local x1_123, x1_456 = type_to_vec6[vtype](initial_value)
if not owner then
throw("cannot derive in non-reactive scope")
end; assert(owner)
-- https://en.wikipedia.org/wiki/Damping
-- todo: calculate damped freq at 10tau instead of natural freq
local w_n = 2*math.pi / (period or 1)
local z = damping_ratio or 1
@ -182,27 +168,38 @@ local function spring<T>(source: () -> T, period: number?, damping_ratio: number
k = k,
c = c,
x0_123 = x1_123,
x1_123 = x1_123,
v_123 = Vec3(),
x0_123 = ZERO,
x1_123 = ZERO,
v_123 = ZERO,
x0_456 = x1_456,
x1_456 = x1_456,
v_456 = Vec3(),
x0_456 = ZERO,
x1_456 = ZERO,
v_456 = ZERO,
source_value = initial_value,
source_value = false :: any,
}
local output = create_start_node(initial_value)
local output = create_start_node(false :: any)
updater.effect = function()
local v = source()
data.x1_123, data.x1_456 = type_to_vec6[typeof(v)](v)
data.source_value = v
local function updater_effect()
local value = source()
data.x1_123, data.x1_456 = type_to_vec6[typeof(value)](value)
data.source_value = value
springs[data] = output -- todo: investigate why insertion is not O(1) at ~20k springs
return false
return value
end
local updater = create_node(false :: any, updater_effect)
set_owner(updater, owner)
evaluate_node(updater)
-- set initial position to goal
data.x0_123, data.x0_456 = data.x1_123, data.x1_456
-- set output to goal
output.cache = data.source_value
return function()
track(output)
return output.cache

View file

@ -22,22 +22,27 @@ local function switch<T, U>(source: () -> T): (map: Map<T, ((() -> U)?)>) -> ()
throw("cannot switch in non-reactive scope")
end; assert(owner)
local scope: Node<false>?
local last_scope: Node<false>?
local last_component: (() -> U)?
local function update(): U?
local function update(cached): U?
local component = map[source()]
if component == last_component then return nil end
if component == last_component then return cached end
last_component = component
if scope then
destroy(scope :: Node<any>)
if last_scope then
destroy(last_scope :: Node<any>)
last_scope = nil
end
if component == nil then return nil end
if type(component) ~= "function" then
throw("map must map a value to a function")
end
local new_scope = create_node(false, false)
scope = new_scope :: Node<any>
last_scope = new_scope :: Node<any>
set_owner(new_scope, owner)
open_scope(new_scope)
@ -51,7 +56,7 @@ local function switch<T, U>(source: () -> T): (map: Map<T, ((() -> U)?)>) -> ()
return result
end
local node = create_node(nil, update :: () -> any)
local node = create_node(nil :: any, update)
set_owner(node, owner)
evaluate_node(node)

View file

@ -1,32 +1,9 @@
-- returns path to file as an array with each directory
-- accounts for Roblox and Luau contexts
local function get_path(s)
if string.sub(s, #s - 4, #s) == ".luau" then
s = string.sub(s, 1, #s - 5)
end
if not game then script = require "test/relative-string" end
return string.split(s, string.match(s, "%w+/") and "/" or ".")
end
local trace = require(script.Parent.trace)
-- get directory of vide root
local root do
local path = get_path(debug.info(1, "s"))
root = path[#path - 1]
end
-- throws an error, ensuring stack trace begins at the first callsite outside
-- of all vide library files
local function throw(msg: string)
local stack = 1
local path = get_path(debug.info(stack, "s"))
while path[#path] == root or path[#path - 1] == root do
stack += 1
path = get_path(debug.info(stack, "s"))
end
error(msg, stack)
local function throw(msg): any
error(msg, trace()-1)
end
return throw

29
src/trace.luau Normal file
View file

@ -0,0 +1,29 @@
-- returns path to file as an array with each directory
-- accounts for Roblox and Luau contexts
local function get_path(s)
if string.sub(s, #s - 4, #s) == ".luau" then
s = string.sub(s, 1, #s - 5)
end
return string.split(s, string.match(s, "%w+/") and "/" or ".")
end
-- get directory of vide root
local root do
local path = get_path(debug.info(1, "s"))
root = path[#path - 1]
end
-- finds the first stack depth outside of any vide library function
return function(): number
local stack = 1
local path = get_path(debug.info(stack, "s"))
while path[#path] == root or path[#path - 1] == root do
stack += 1
path = get_path(debug.info(stack, "s"))
end
return stack
end

View file

@ -7,17 +7,21 @@ local get_scope = graph.get_scope
local function untrack<T>(source: () -> T): T
local scope = get_scope()
if not scope then throw("cannot untrack in non-reactive scope") end
assert(scope)
if not scope then
throw("cannot untrack in non-reactive scope")
end; assert(scope)
-- sources are only tracked if the node in scope has an effect
local effect = scope.effect
scope.effect = false
local v = source()
local ok, result = pcall(source)
scope.effect = effect :: () -> ()
return v
if not ok then error(result, 0) end
return result
end
return untrack

View file

@ -1,14 +1,19 @@
local BENCH, START = require("test/testkit").benchmark()
local vide = require "src/init"
local root = vide.root
local source = vide.source
local derive = vide.derive
local indexes = vide.indexes
local values = vide.values
local cleanup = vide.cleanup
local create = vide.create
local N = 2^18 -- 262144
-- todo: wide and deep graph benchmarks
BENCH("create source", function()
local source = vide.source
local cache = table.create(N)
for i = 1, START(N) do
@ -17,7 +22,7 @@ BENCH("create source", function()
end)
BENCH("get value", function()
local src = vide.source(1)
local src = source(1)
for i = 1, START(N) do
src()
@ -25,7 +30,7 @@ BENCH("get value", function()
end)
BENCH("set value", function()
local src = vide.source(1)
local src = source(1)
for i = 1, START(N) do
src(i)
@ -33,12 +38,10 @@ BENCH("set value", function()
end)
BENCH("derive 1 source", function()
local derive = vide.derive
local cache = table.create(N)
local src = vide.source(1)
local src = source(1)
vide.root(function()
root(function()
for i = 1, START(N) do
cache[i] = derive(function()
return src()
@ -49,8 +52,6 @@ BENCH("derive 1 source", function()
end)
BENCH("derive 4 sources", function()
local derive = vide.derive
local cache = table.create(N)
local src = vide.source(1)
local src2 = vide.source(2)
@ -68,11 +69,11 @@ BENCH("derive 4 sources", function()
end)
end)
BENCH("set derived value", function()
local src = vide.source(1)
BENCH("update 1->1 graph", function()
local src = source(1)
vide.root(function()
local _derived = vide.derive(function() return src() end)
root(function()
local _derived = derive(function() return src() end)
for i = 1, START(N) do
src(i)
@ -82,9 +83,104 @@ BENCH("set derived value", function()
end)
end)
BENCH("update 1->1 graph with cleanup", function()
local src = source(1)
root(function()
derive(function()
cleanup(function() end)
return src()
end)
for i = 1, START(N) do
src(i)
end
return nil
end)
end)
BENCH("update 1->1000 graph", function()
local src = source(-1)
root(function()
for i = 1, 1000 do
derive(function() return src() end)
end
src(0)
for i = 1, START(10) do
src(i)
end
end)
end)
BENCH("update 1->1->1->1...1000 graph", function()
local src = source(-1)
root(function()
local last = src
for i = 1, 1000 do
local l = last
last = derive(function() return l() end)
end
src(0)
for i = 1, START(10) do
src(i)
end
end)
end)
-- todo: repeat with batching
BENCH("update 1000->1 graph", function()
local srcs = {}
for i = 1, 1000 do
srcs[i] = source(0)
end
root(function()
derive(function()
for i = 1, 1000 do
srcs[i]()
end
return false
end)
for i = 1, START(1) do
for idx = 1, 1000 do
srcs[idx](i)
end
end
end)
end)
-- todo: optimize, repeat with batching
BENCH("update 1000x 1->1 common extern. graph", function()
local ext = source(-1)
root(function()
local srcs = {}
for i = 1, 1000 do
srcs[i] = source(0)
derive(function() return srcs[i]() + ext() end)
end
ext(0)
for i = 1, START(10) do
for idx = 1, 1000 do
srcs[idx](i)
end
end
end)
end)
BENCH("apply 0 properties", function()
local apply = require "src/apply"
local instance = vide.create("Frame") {}
local instance = create("Frame") {}
for i = 1, START(N) do
apply(instance, {})
@ -93,7 +189,7 @@ end)
BENCH("apply 8 properties", function()
local apply = require "src/apply"
local instance = vide.create("Frame") {}
local instance = create("Frame") {}
for i = 1, START(N) do
apply(instance, {
@ -112,10 +208,10 @@ end)
BENCH("bind property", function()
local apply = require "src/apply"
local instance = vide.create("Frame") {}
local src = vide.source(1)
local instance = create("Frame") {}
local src = source(1)
vide.root(function()
root(function()
for i = 1, START(N) do
apply(instance, {
Text = src
@ -129,10 +225,10 @@ end)
BENCH("update binding", function()
local apply = require "src/apply"
local instance = vide.create("Frame") {}
local src = vide.source(1)
local instance = create("Frame") {}
local src = source(1)
vide.root(function()
root(function()
apply(instance, {
Text = src
})
@ -154,12 +250,12 @@ BENCH("indexes() all new", function()
data[i] = i
end
local src = vide.source(data)
local src = source(data)
vide.root(function()
root(function()
START(N)
local _list = vide.indexes(src, function(v, i)
local _list = indexes(src, function(v, i)
return {}
end)
@ -174,10 +270,10 @@ BENCH("indexes() no change", function()
data[i] = i
end
local src = vide.source(data)
local src = source(data)
vide.root(function()
local _list = vide.indexes(src, function(v, i)
root(function()
local _list = indexes(src, function(v, i)
return {}
end)
@ -196,11 +292,11 @@ BENCH("indexes() all change", function()
data[i] = i
end
local src = vide.source(data)
local src = source(data)
vide.root(function()
local _list = vide.indexes(src, function(v, i)
root(function()
local _list = indexes(src, function(v, i)
return {}
end)
@ -225,10 +321,10 @@ BENCH("indexes() all remove", function()
data[i] = i
end
local src = vide.source(data)
local src = source(data)
vide.root(function()
local _list = vide.indexes(src, function(v, i)
root(function()
local _list = indexes(src, function(v, i)
return {}
end)
@ -249,12 +345,12 @@ BENCH("values() all new", function()
data[i] = {}
end
local src = vide.source(data)
local src = source(data)
vide.root(function()
root(function()
START(N)
local _list = vide.values(src, function(v, i)
local _list = values(src, function(v, i)
return {}
end)
@ -269,10 +365,10 @@ BENCH("values() no change", function()
data[i] = {}
end
local src = vide.source(data)
local src = source(data)
vide.root(function()
local _list = vide.values(src, function(v, i)
root(function()
local _list = values(src, function(v, i)
return {}
end)
@ -293,10 +389,10 @@ BENCH("values() all change", function()
data[i] = {}
end
local src = vide.source(data)
local src = source(data)
vide.root(function()
local _list = vide.values(src, function(v, i)
root(function()
local _list = values(src, function(v, i)
return {}
end)
@ -322,10 +418,10 @@ BENCH("values() all remove", function()
data[i] = {}
end
local src = vide.source(data)
local src = source(data)
vide.root(function()
local _list = vide.values(src, function(v, i)
root(function()
local _list = values(src, function(v, i)
return {}
end)
@ -342,9 +438,9 @@ end)
N *= 1024
BENCH("register new cleanup", function()
local cleanup = vide.cleanup
local cleanup = cleanup
vide.root(function()
root(function()
local cleaner = function() end
local callers = {}
@ -368,7 +464,6 @@ do
-- the purpose of the two following benchmarks is to measure the overhead of
-- aggregate construction
BENCH("set explicit mock vector2", function()
local create = vide.create
local apply = require "src/apply"
local Vector2 = require "test/mock".Vector2
@ -384,7 +479,6 @@ do
end)
BENCH("set aggregate mock vector2", function()
local create = vide.create
local apply = require "src/apply"
local Vector2 = require "test/mock".Vector2
@ -400,4 +494,47 @@ do
end)
end
-- innacurate due to no Vector3 in vanilla Luau
-- mock vector is 200x slower than native vector
-- BENCH("spring update", function()
-- local root, source, spring = vide.root, vide.source, vide.spring
-- local src = source(0)
-- root(function()
-- for i = 1, N do
-- spring(src)
-- end
-- START(N)
-- src(1)
-- return nil
-- end)
-- end)
-- N /= 1024
-- BENCH("spring step", function()
-- local root, source, spring = vide.root, vide.source, vide.spring
-- local src = source(0)
-- root(function()
-- for i = 1, N do
-- spring(src)
-- end
-- src(1)
-- START(N)
-- vide.step(1/60)
-- return nil
-- end)
-- end)
return nil

View file

@ -1,5 +1,6 @@
local function dir(directory: string)
return setmetatable({} :: { [string]: any }, { __index = function(_, path) return directory .. path end })
return setmetatable({} :: { [string]: any },
{ __index = function(_, path) return directory .. path end })
end
local script = dir "src/"

View file

@ -65,7 +65,7 @@ local function main()
until false
end
main()
vide.root(main)

View file

@ -33,7 +33,6 @@ local NIL = nil :: any
TEST("graph", function()
local create_node = graph.create_node
local create_start_node = graph.create_start_node
local track = graph.track
local update = graph.update
local add_child = graph.add_child
@ -45,13 +44,11 @@ TEST("graph", function()
local destroy = graph.destroy
local function node<T>(v: T?)
local n = create_node(v or false)
n.effect = function() end
return n
return create_node(v or false, function(x) return not x end)
end
local function scope()
return create_node(false)
return create_node(false, false)
end
local function cleanup(fn: () -> ())
@ -82,17 +79,18 @@ TEST("graph", function()
local count = 0
local function effect()
local function effect(x)
track(a)
track(b)
count += 1
return not x
end
c.effect = effect
open_scope(c)
effect()
effect(c.cache)
close_scope()
@ -107,9 +105,9 @@ TEST("graph", function()
local a, b, c, d = node(), node(), node(), node()
local b_cnt, c_cnt, d_cnt = 0, 0, 0
function b.effect() b_cnt += 1 end
function c.effect() c_cnt += 1 end
function d.effect() d_cnt += 1 end
function b.effect(x) b_cnt += 1; return not x end
function c.effect(x) c_cnt += 1; return not x end
function d.effect(x) d_cnt += 1; return not x end
open_scope(b); track(a); close_scope()
open_scope(c); track(a); close_scope()
@ -125,9 +123,10 @@ TEST("graph", function()
do CASE "duplicate child on rerun"
local a, b, c = node(), node(), node()
function c.effect()
function c.effect(x)
track(a)
track(b)
return not x
end
open_scope(c); assert(c.effect)(NIL); close_scope()
@ -165,7 +164,6 @@ TEST("graph", function()
do open_scope(root)
clean "root"
items_updated = node()
items_updated.effect = function() end
track(items_updated) -- should not
add_child(root, items_updated)
@ -177,7 +175,6 @@ TEST("graph", function()
do open_scope(scope1)
clean "scope1"
bind1 = node()
bind1.effect = function() end
add_child(scope1, bind1)
do open_scope(bind1)
@ -190,7 +187,6 @@ TEST("graph", function()
do open_scope(scope2)
clean "scope2"
bind2 = node()
bind2.effect = function() end
add_child(scope2, bind2)
do open_scope(bind2)
clean "bind2"
@ -252,13 +248,47 @@ TEST("graph", function()
end
do CASE "nodes garbage collection"
local wref = weak { create_node(1) }
local wref = weak { node(1) }
destroy(wref[1])
gc()
CHECK(not wref[1])
end
end)
TEST("mount()", function()
local mount = vide.mount
local create = vide.create
local source = vide.source
local cleanup = vide.cleanup
local screen = create "ScreenGui" {}
local text = source "foo"
local count = 0
local unmount = mount(function()
cleanup(function()
count += 1
end)
return create "TextLabel" {
Name = "TextLabel",
Text = text
}
end, screen)
local label = screen:FindFirstChild "TextLabel" :: TextLabel
CHECK(label)
CHECK(label.Text == "foo")
text "bar"
CHECK(label.Text == "bar")
CHECK(count == 0)
unmount()
CHECK(count == 1)
end)
TEST("source()", wrap_root(function()
local source = vide.source
local effect = vide.effect
@ -422,7 +452,7 @@ TEST("derive()", wrap_root(function()
local _, destroy = root(function()
local b = derive(function()
local _b = derive(function()
cleanup(function()
count += 1
end)
@ -534,13 +564,13 @@ TEST("cleanup()", wrap_root(function()
end
do CASE "cleanup on rerun"
local state = source(1)
local src = source(1)
local effected = 0
local cleaned = 0
effect(function()
state()
src()
effected += 1
cleanup(function()
cleaned += 1
@ -550,27 +580,27 @@ TEST("cleanup()", wrap_root(function()
CHECK(effected == 1)
CHECK(cleaned == 0)
state(2)
src(2)
CHECK(effected == 2)
CHECK(cleaned == 1)
end
do CASE "multiple cleanup"
local state = source(1)
local src = source(1)
local queue = {}
effect(function()
state()
src()
cleanup(function() table.insert(queue, 1) end)
cleanup(function() table.insert(queue, 2) end)
end)
CHECK(testkit.seq(queue, {}))
state(2)
src(2)
CHECK(testkit.seq(queue, { 1, 2 }))
state(3)
src(3)
CHECK(testkit.seq(queue, { 1, 2, 1, 2 }))
end
end))
@ -660,7 +690,6 @@ TEST("create()", wrap_root(function()
CHECK(frame:FindFirstChild "C")
CHECK(frame:FindFirstChild "D")
CHECK(frame:FindFirstChild "E")
CHECK(frame:FindFirstChild "F")
CHECK(frame:FindFirstChild "G")
end
@ -706,7 +735,7 @@ TEST("create()", wrap_root(function()
CHECK(count == 1)
end
do CASE "bind same state to multiple instance properties"
do CASE "bind same source to multiple instance properties"
local src = source "1"
local text = create "TextBox" {
@ -807,11 +836,9 @@ TEST("create()", wrap_root(function()
end))
TEST("switch()", wrap_root(function()
local create = vide.create
local source = vide.source
local switch = vide.switch
local effect = vide.effect
local derive = vide.derive
local cleanup = vide.cleanup
do CASE "update on source change"
@ -828,15 +855,42 @@ TEST("switch()", wrap_root(function()
CHECK(count == 1)
CHECK(output() == 1)
input(false)
CHECK(output() == 0)
CHECK(count == 2)
input(false)
CHECK(output() == 0)
CHECK(count == 2)
input(nil)
input(NIL)
CHECK(output() == nil)
end
do CASE "same component different map"
local input = source(0)
local function component()
return {}
end
local output = switch(input) {
[1] = component,
[2] = component
}
CHECK(output() == nil)
input(1)
local instance = output()
CHECK(instance)
input(2)
CHECK(output() == instance)
end
do CASE "scoped switch"
local input = source(true)
@ -865,11 +919,30 @@ TEST("switch()", wrap_root(function()
input(true)
CHECK(switch1_count == 1)
CHECK(switch0_count == 1)
input(nil)
input(NIL)
CHECK(switch1_count == 2)
CHECK(switch0_count == 1)
CHECK(owner_count == 0)
end
do CASE "reactive stack resets after error"
local scopes = require "src/graph".scopes
local input = source(1)
local n0 = scopes.n
local ok = pcall(function()
switch(input) {
error :: any
}
end)
CHECK(not ok)
local n1 = scopes.n
CHECK(n0 == n1)
end
end))
TEST("indexes()", wrap_root(function()
@ -893,10 +966,10 @@ TEST("indexes()", wrap_root(function()
do CASE "cache result"
local input = source { 1, 2, 3 }
local runcount = table.create(3, 0)
local count = table.create(3, 0)
local output = indexes(input, function(v, i)
runcount[i] += 1
count[i] += 1
return v
end)
@ -906,9 +979,9 @@ TEST("indexes()", wrap_root(function()
CHECK(output()[2]() == 2)
CHECK(output()[3]() == 4)
CHECK(runcount[1] == 1)
CHECK(runcount[2] == 1)
CHECK(runcount[3] == 1)
CHECK(count[1] == 1)
CHECK(count[2] == 1)
CHECK(count[3] == 1)
end
do CASE "removal reflected"
@ -989,6 +1062,27 @@ TEST("indexes()", wrap_root(function()
CHECK(count[2] == 0)
CHECK(count[3] == 0)
end
do CASE "reactive stack resets after error"
local scopes = require "src/graph".scopes
local input = source { 1 }
local n0 = scopes.n
local ok = pcall(function()
indexes(input, function()
error("")
return NIL
end)
end)
CHECK(not ok)
local n1 = scopes.n
CHECK(n0 == n1)
end
end))
TEST("values()", wrap_root(function()
@ -1012,10 +1106,10 @@ TEST("values()", wrap_root(function()
do CASE "cache result"
local input = source { 1, 2, 3 }
local runcount = table.create(3, 0)
local count = table.create(3, 0)
local output = values(input, function(v, i)
runcount[v] += 1
count[v] += 1
return i
end)
@ -1025,9 +1119,9 @@ TEST("values()", wrap_root(function()
CHECK(output()[2]() == 3)
CHECK(output()[3]() == 2)
CHECK(runcount[1] == 1)
CHECK(runcount[2] == 1)
CHECK(runcount[3] == 1)
CHECK(count[1] == 1)
CHECK(count[2] == 1)
CHECK(count[3] == 1)
end
do CASE "removal reflected"
@ -1094,6 +1188,27 @@ TEST("values()", wrap_root(function()
CHECK(count[2] == 0)
CHECK(count[3] == 0)
end
do CASE "reactive stack resets after error"
local scopes = require "src/graph".scopes
local input = source { 1 }
local n0 = scopes.n
local ok = pcall(function()
values(input, function()
error("")
return NIL
end)
end)
CHECK(not ok)
local n1 = scopes.n
CHECK(n0 == n1)
end
end))
TEST("spring()", wrap_root(function()
@ -1226,7 +1341,7 @@ TEST("untrack()", wrap_root(function()
CHECK(a() == untrack(a))
end
do CASE "derived state"
do CASE "derived source"
local a = source(0)
local b = source(0)
local c = source(0)
@ -1368,127 +1483,156 @@ TEST("actions", function()
end
end)
-- TEST("strict", function()
-- vide.strict = true
TEST("changed()", wrap_root(function()
local root = vide.root
local create = vide.create
local source = vide.source
local changed = vide.changed
-- local create = vide.create
-- local source = vide.source
-- local derive = vide.derive
-- local effect = vide.effect
-- local indexes, values = vide.indexes, vide.values
-- local cleanup = vide.cleanup
do CASE "outputs"
local output = source(nil)
-- -- do CASE "error on derived callback yield"
-- -- local state = source(1)
local text = create "TextLabel" {
Text = "a",
changed("Text", output)
}
-- -- local ok = pcall(function()
-- -- local _derived = derive(function()
-- -- coroutine.yield()
-- -- return state()
-- -- end)
-- -- end)
--CHECK(output() == "a")
text.Text = "b"
CHECK(output() == "b")
end
-- -- CHECK(not ok)
-- -- end
do CASE "connection disconnected"
local text, destroy = root(function()
local output = source(nil)
-- -- do CASE "error on effecter callback yield"
-- -- local state = source(1)
return create "TextLabel" {
Text = "a",
changed("Text", output)
}
end)
-- -- local ok = pcall(function()
-- -- local _derived = effect(function()
-- -- coroutine.yield()
-- -- state()
-- -- end)
-- -- end)
destroy() -- changed() should of disconnect connection
-- -- CHECK(not ok)
-- -- end
-- check if instance can gc
local wref = weak { text }
text = NIL
gc()
CHECK(not wref[1])
end
end))
-- do CASE "run derived callback twice"
-- local state = source(1)
-- local runcount = 0
TEST("strict", wrap_root(function()
vide.strict = true
-- local _ = derive(function()
-- runcount += 1
-- return state()
-- end)
local create = vide.create
local source = vide.source
local derive = vide.derive
local effect = vide.effect
local indexes, values = vide.indexes, vide.values
-- CHECK(runcount == 2)
-- state(2)
-- CHECK(runcount == 4)
-- end
do CASE "error on derived callback yield"
local src = source(1)
-- do CASE "run effecter callback twice"
-- local state = source(1)
-- local runcount = 0
local ok = pcall(function()
local _derived = derive(function()
coroutine.yield()
return src()
end)
end)
-- effect(function()
-- runcount += 1
-- state()
-- end)
CHECK(not ok)
end
-- CHECK(runcount == 2)
-- state(2)
-- CHECK(runcount == 4)
-- end
do CASE "error on effecter callback yield"
local src = source(1)
-- do CASE "indexes() error if primitive"
-- local state = source { 1 }
local ok = pcall(function()
effect(function()
coroutine.yield()
src()
end)
end)
-- local ok = pcall(function()
-- indexes(state, function() return 1 end)
-- end)
CHECK(not ok)
end
-- CHECK(not ok)
-- end
do CASE "run derived callback twice"
local src = source(1)
local count = 0
-- do CASE "values() error if duplicate"
-- local state = source { 1, 2, 1 }
local _ = derive(function()
count += 1
return src()
end)
-- local ok = pcall(function()
-- values(state, function() return {} end)
-- end)
CHECK(count == 2)
src(2)
CHECK(count == 4)
end
-- CHECK(not ok)
-- end
do CASE "run effect callback twice"
local src = source(1)
local count = 0
-- do CASE "duplicate properties"
-- local ok = pcall(function()
-- create "TextLabel" {
-- {
-- Name = "foo"
-- },
-- {
-- Name = "bar"
-- }
-- }
-- end)
effect(function()
count += 1
src()
end)
-- CHECK(not ok)
CHECK(count == 2)
src(2)
CHECK(count == 4)
end
-- ok = pcall(function()
-- create "TextLabel" {
-- {
-- Name = "foo",
-- {
-- Name = "bar"
-- }
-- }
-- }
-- end)
do CASE "indexes() error if primitive"
local src = source { 1 }
-- CHECK(ok)
-- end
local ok = pcall(function()
indexes(src, function() return 1 end)
end)
-- do CASE "multiple cleanup per scope"
-- local ok = pcall(function()
-- cleanup(function() end)
-- cleanup(function() end)
-- end)
CHECK(not ok)
end
-- CHECK(not ok)
-- end
-- end)
do CASE "values() error if duplicate"
local src = source { 1, 2, 1 }
local ok = pcall(function()
values(src, function() return {} end)
end)
CHECK(not ok)
end
do CASE "duplicate properties"
local ok = pcall(function()
create "TextLabel" {
{
Name = "foo"
},
{
Name = "bar"
}
}
end)
CHECK(not ok)
ok = pcall(function()
create "TextLabel" {
{
Name = "foo",
{
Name = "bar"
}
}
}
end)
CHECK(ok)
end
end))
local ok = FINISH()
if not ok then error("Tests failed", 0) end

View file

@ -1,19 +1,13 @@
# todo
- better error reporting and stack traces in strict mode
- auto-enable of strict mode depending on compiler optimizaton level
- investigate if weak table iteration can be invalidated
- property binding optimization
- would no longer allow `cleanup()` usage in binding scopes
- solution to nested reactivity, see: SolidJS stores
- SolidJS control flow components
- equality checking of derived sources
- investigate performance of wide graphs
- optimize child removal
- implement from solid:
- Show
- Switch
- Dynamic
- Portal
- batch
- optimize `indexes()` double-diffing
- define behavior of deriving a source within a derived source