Merge branch 'centau:main' into main

This commit is contained in:
alicesaidhi 2025-04-15 22:21:31 +02:00 committed by GitHub
commit 92cf612cef
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 221 additions and 45 deletions

View file

@ -16,6 +16,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
effects to set children. effects to set children.
- `spring()` returns a second value, a setter to set position, velocity and - `spring()` returns a second value, a setter to set position, velocity and
impulse. impulse.
- `show()` now receives a source to its callback returning the current value
of the condition.
- Ignore `false` passed as a child.
### Changed ### Changed
@ -25,6 +28,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
destroyed. destroyed.
- Error reporting should be improved with better formatting when effects invoke - Error reporting should be improved with better formatting when effects invoke
other effects and no more loss of stack traces. other effects and no more loss of stack traces.
- Nesting parent properties now work, and they are now also checked for
duplicates like other properties.
### Removed ### Removed

View file

@ -7,7 +7,7 @@ But the disconnecting of many signals and connections is tedious and verbose.
Vide instead operates on the concept of scopes which provides a much cleaner Vide instead operates on the concept of scopes which provides a much cleaner
API, given that you follow a few rules. API, given that you follow a few rules.
Thre are two types of scopes: stable and reactive. There are two types of scopes: stable and reactive.
- A scope must be created within another scope. - A scope must be created within another scope.
- Stable scopes never rerun. - Stable scopes never rerun.

View file

@ -23,6 +23,9 @@ type Cache = {
Array<(Instance) -> ()> -- action callbacks Array<(Instance) -> ()> -- action callbacks
>, >,
-- what to parent the instance to after running actions
parent: unknown,
-- cache to detect duplicate property setting at same nesting depth -- cache to detect duplicate property setting at same nesting depth
nested_debug: Map< nested_debug: Map<
number, -- depth number, -- depth
@ -47,6 +50,7 @@ local function borrow_cache(): Cache
actions = setmetatable({} :: any, { -- lazy init actions = setmetatable({} :: any, { -- lazy init
__index = function(self, i) self[i] = {}; return self[i] end __index = function(self, i) self[i] = {}; return self[i] end
}), }),
parent = nil,
nested_debug = setmetatable({} :: any, { nested_debug = setmetatable({} :: any, {
__index = function(self, i: number) self[i] = {}; return self[i] end __index = function(self, i: number) self[i] = {}; return self[i] end
}), }),
@ -61,8 +65,6 @@ end
local function process_properties(properties: Map<unknown, unknown>, instance: Instance, cache: Cache, depth: number) local function process_properties(properties: Map<unknown, unknown>, instance: Instance, cache: Cache, depth: number)
for property, value in properties do for property, value in properties do
if property == "Parent" then continue end
if type(property) == "string" then if type(property) == "string" then
if flags.strict then -- check for duplicate property assignment at nesting depth if flags.strict then -- check for duplicate property assignment at nesting depth
if cache.nested_debug[depth][property] then if cache.nested_debug[depth][property] then
@ -71,6 +73,11 @@ local function process_properties(properties: Map<unknown, unknown>, instance: I
cache.nested_debug[depth][property] = true cache.nested_debug[depth][property] = true
end end
if property == "Parent" then
cache.parent = value
continue
end
if type(value) == "function" then if type(value) == "function" then
if typeof((instance :: any)[property]) == "RBXScriptSignal" then if typeof((instance :: any)[property]) == "RBXScriptSignal" then
table.insert(cache.events, property) -- add event name to buffer table.insert(cache.events, property) -- add event name to buffer
@ -93,7 +100,7 @@ local function process_properties(properties: Map<unknown, unknown>, instance: I
else else
process_properties(value :: Map<unknown, unknown>, instance, cache, depth + 1) process_properties(value :: Map<unknown, unknown>, instance, cache, depth + 1)
end end
else elseif type(value) == "userdata" then
(value :: Instance).Parent = instance -- parent child (value :: Instance).Parent = instance -- parent child
end end
end end
@ -106,9 +113,6 @@ local function apply<T>(instance: T & Instance, properties: { [unknown]: unknown
error "attempt to call a constructor returned by create() with no properties" error "attempt to call a constructor returned by create() with no properties"
end end
-- queue parent assignment if any for last
local parent: unknown = properties.Parent
local caches = borrow_cache() local caches = borrow_cache()
local events = caches.events local events = caches.events
local actions = caches.actions local actions = caches.actions
@ -135,6 +139,7 @@ local function apply<T>(instance: T & Instance, properties: { [unknown]: unknown
end end
end end
local parent = caches.parent
if parent then if parent then
if type(parent) == "function" then if type(parent) == "function" then
implicit_effect.parent(instance, parent :: () -> Instance) implicit_effect.parent(instance, parent :: () -> Instance)
@ -145,6 +150,7 @@ local function apply<T>(instance: T & Instance, properties: { [unknown]: unknown
table.clear(events) table.clear(events)
for _, queued in next, actions do table.clear(queued) end for _, queued in next, actions do table.clear(queued) end
caches.parent = nil
if flags.strict then table.clear(nested_debug) end if flags.strict then table.clear(nested_debug) end
table.clear(nested_stack) table.clear(nested_stack)

View file

@ -1,16 +1,33 @@
local switch = require "./switch" local source = require "./source"
local derive = require "./derive"
local effect = require "./effect"
local untrack = require "./untrack"
local function show<T>(source: () -> any, component: () -> T, fallback: (() -> T)?): () -> T? local function show<T, U>(input: () -> T?, component: (() -> T) -> U, fallback: (() -> U)?): () -> U?
local function truthy() local filtered_input = source()
return not not source()
effect(function()
local v = input()
if v then
filtered_input(v)
end end
end)
return switch(truthy) { local input_is_truthy = derive(function()
[true] = component, return not not input()
[false] = fallback, end)
}
-- todo: is this needed?
-- local filtered_input_is_truthy = derive(function()
-- return not not filtered_input()
-- end)
return derive(function()
return
if input_is_truthy() then untrack(function() return component(filtered_input :: () -> T) end)
elseif fallback then untrack(fallback)
else nil
end)
end end
return show :: return show
(<T>(source: () -> any, component: () -> T) -> () -> T?) &
(<T, U>(source: () -> any, component: () -> T, fallback: () -> U) -> () -> (T | U)?)

View file

@ -22,4 +22,4 @@ local function untrack<T>(source: () -> T): T
end end
end end
return untrack return untrack :: ( <T>(fn: () -> T) -> T ) & ( (fn: () -> ()) -> () )

View file

@ -86,6 +86,9 @@ local Instance = {} :: any do
local proxies = {} :: { [Data]: userdata? } local proxies = {} :: { [Data]: userdata? }
setmetatable(proxies :: any, { __mode = "v" }) setmetatable(proxies :: any, { __mode = "v" })
-- allocate variables for the metamethods as __index and get_proxy cross refrences each other
local __index, __newindex
local function get_data(userdata: userdata): Data local function get_data(userdata: userdata): Data
local function f(userdata: userdata): ProxyMT local function f(userdata: userdata): ProxyMT
return getmetatable(userdata :: any) return getmetatable(userdata :: any)
@ -94,6 +97,19 @@ local Instance = {} :: any do
return f(userdata).data return f(userdata).data
end end
local function get_proxy(data: Data): userdata
return proxies[data] or (function()
local userdata = newproxy(true)
local proxy = getmetatable(userdata)
proxy.proxy = userdata
proxy.data = data
proxy.__index = __index
proxy.__newindex = __newindex
proxies[data] = userdata
return userdata
end)()
end
local function is_instance(value: unknown): boolean local function is_instance(value: unknown): boolean
local mt = getmetatable(value :: any) local mt = getmetatable(value :: any)
return mt and mt.data and mt.data.type == "Instance" return mt and mt.data and mt.data.type == "Instance"
@ -101,16 +117,16 @@ local Instance = {} :: any do
local methods = {} local methods = {}
local function __index(userdata: userdata, property: string): () __index = function(userdata: userdata, property: string): ()
local data = get_data(userdata) local data = get_data(userdata)
return if methods[property] then methods[property] return if methods[property] then methods[property]
elseif property == "Name" then data.name elseif property == "Name" then data.name
elseif property == "Parent" then data.parent elseif property == "Parent" then (data.parent and get_proxy(data.parent))
elseif property == "Destroying" then data.destroying elseif property == "Destroying" then data.destroying
else data.properties[property] else data.properties[property]
end end
local function __newindex(userdata: userdata, property: string, value: unknown) __newindex = function(userdata: userdata, property: string, value: unknown)
local data = get_data(userdata) local data = get_data(userdata)
if property == "Name" then if property == "Name" then
if type(value) ~= "string" then error("name must be a string", 2) end if type(value) ~= "string" then error("name must be a string", 2) end
@ -135,19 +151,6 @@ local Instance = {} :: any do
end end
end end
local function get_proxy(data: Data): userdata
return proxies[data] or (function()
local userdata = newproxy(true)
local proxy = getmetatable(userdata)
proxy.proxy = userdata
proxy.data = data
proxy.__index = __index
proxy.__newindex = __newindex
proxies[data] = userdata
return userdata
end)()
end
function Instance.new(class: string): Instance function Instance.new(class: string): Instance
local data = { local data = {
name = "UNNAMED", name = "UNNAMED",

View file

@ -759,6 +759,13 @@ TEST("create()", wrap_root(function()
CHECK(text.Text == "test") CHECK(text.Text == "test")
end end
do CASE "set nested parent"
local frame = create "Frame" {}
local text = create "TextLavel" { { Parent = frame } }
CHECK(frame:GetChildren()[1] == text)
CHECK(text.Parent == frame)
end
do CASE "nested deferred" do CASE "nested deferred"
local text = create "TextLabel" { local text = create "TextLabel" {
{ {
@ -819,6 +826,20 @@ TEST("create()", wrap_root(function()
CHECK(frame:FindFirstChild "G") CHECK(frame:FindFirstChild "G")
end end
do CASE "set false as child"
create "Frame" {
false
}
create "Frame" {
function() return false end
}
create "Frame" {
function() return { false } end
}
end
do CASE "binding properties to source" do CASE "binding properties to source"
local name = source("Hi") local name = source("Hi")
local text = source("Bye") local text = source("Bye")
@ -1073,20 +1094,147 @@ TEST("show()", wrap_root(function()
local show = vide.show local show = vide.show
local root = vide.root local root = vide.root
do CASE "main" do CASE "show component"
-- uses switch() internally, more extensive testing of scoping not needed local input = source(true)
local value = source("truey" :: unknown) local function one() return 1 end
local output = show(input, one)
CHECK(output() == 1)
input(false)
CHECK(output() == nil)
end
do CASE "fallback component"
local input = source(true)
local function one() return 1 end local function one() return 1 end
local function two() return 2 end local function two() return 2 end
local output = show(value, one, two) local output = show(input, one, two)
CHECK(output() == 1) CHECK(output() == 1)
value(nil) input(false)
CHECK(output() == 2) CHECK(output() == 2)
end end
do CASE "alt" do CASE "updating truth to truthy does not rerun"
local input = source(1)
local count = 0
local function component()
count += 1
return 1
end
local output = show(input, component)
CHECK(count == 1)
CHECK(output() == 1)
input(2)
CHECK(count == 1)
end
do CASE "updating source passed to component"
local input = source(1 :: number?)
local count = 0
show(input :: () -> number?, function(value: () -> number)
vide.cleanup(function() print "destroyed" end)
effect(function()
local v = value()
count += 1
CHECK(v == count)
if v ~= count then error(count) end
end)
return true
end)
input(2)
CHECK(count == 2)
input(3)
CHECK(count == 3)
input(nil)
CHECK(count == 3)
end
do CASE "special strict case"
type Weapon = {
id: string,
enchant: string?
}
vide.strict = true
local count = 0
local branch = 0
local weapon = source(nil :: Weapon?)
effect(function()
weapon()
end)
effect(function()
weapon()
end)
show(weapon, function(weapon: () -> Weapon)
local enchant = function() return weapon().enchant end
show(enchant, function(enchant: () -> string)
effect(function()
local e = enchant()
count += 1
CHECK(e ~= nil)
if branch == 1 then
CHECK(e == "fire")
elseif branch == 2 then
CHECK(e == "poison")
end
end)
return {}
end)
return {}
end)
effect(function()
weapon()
end)
effect(function()
weapon()
end)
branch = 1
weapon { id = "1", enchant = "fire" }
CHECK(count == 8)
branch = 2
weapon { id = "1", enchant = "poison" }
CHECK(count == 10)
weapon { id = "1", enchant = nil }
CHECK(count == 10)
branch = 1
weapon { id = "1", enchant = "fire" }
CHECK(count == 14)
weapon(nil)
branch = 2
weapon { id = "1", enchant = "poison" }
CHECK(count == 22)
vide.strict = false
end
do CASE "alt" -- todo: move test
local visible = vide.source(true) local visible = vide.source(true)
local count = vide.source(0) local count = vide.source(0)
@ -1109,7 +1257,6 @@ TEST("show()", wrap_root(function()
destroyed += 1 destroyed += 1
end) end)
end) end)
return nil
end) end)
end) end)
end) end)
@ -2722,7 +2869,6 @@ TEST("strict", wrap_root(function()
vide.cleanup(function() count_2 += 1 end) vide.cleanup(function() count_2 += 1 end)
return {} return {}
end) end)
return nil
end) end)
end) end)
end) end)
@ -2749,7 +2895,6 @@ TEST("strict", wrap_root(function()
vide.cleanup(function() count_2 += 1 end) vide.cleanup(function() count_2 += 1 end)
return {} return {}
end) end)
return nil
end) end)
end) end)
end) end)