diff --git a/src/action.luau b/src/action.luau index f40bf3a..8cc4987 100644 --- a/src/action.luau +++ b/src/action.luau @@ -10,14 +10,14 @@ local function is_action(v: any) end local function action(callback: (Instance) -> (), priority: number?): Action - local t = { + local a = { priority = priority or 1, callback = callback } - setmetatable(t :: any, ActionMT) + setmetatable(a :: any, ActionMT) - return table.freeze(t) + return table.freeze(a) end return function() diff --git a/src/apply.luau b/src/apply.luau index 1cc533a..13e8cab 100644 --- a/src/apply.luau +++ b/src/apply.luau @@ -13,41 +13,58 @@ type Node = graph.Node type Array = { V } type Map = { [K]: V } --- buffer of event -> callback to connect after properties are set -local event_buffer = {} :: Map ()> +local free_caches: { + -- event listeners to connect after properties are set + events: Map< + string, -- event name + () -> () -- listener + >, --- buffer of priority -> callback to run after events are connected -local action_buffers = {} :: Map ()>> + -- actions to run after events are connected + actions: Map< + number, -- priority + Array<(Instance) -> ()> -- action callbacks + >, --- lazily create buffers on nil index -setmetatable(action_buffers :: any, { - __index = function(_, i: number) - action_buffers[i] = {} - return action_buffers[i] + -- cache to detect duplicate property setting at same nesting depth + nested_debug: Map< + number, -- depth + Map -- set of property names + >, + + -- use stack instead of recursive function to process nesting layers one at time + -- deeper-nested properties take precedence over shallower-nested ones + -- each nested layer occupies two indexes: 1. table ref 2. nested depth + -- e.g. { t1 = { t3 = {} }, t2 = {} } -> { t1, 1, t2, 1, t3, 2 } + nested_stack: { {} | number } +}? + +local function borrow_caches(): typeof(assert(free_caches)) + if free_caches then + local caches = free_caches :: typeof(assert(free_caches)) + free_caches = nil + return caches + else + return { + events = {}, + actions = setmetatable({} :: any, { -- lazy init + __index = function(self, i) self[i] = {}; return self[i] end + }), + nested_debug = setmetatable({} :: any, { + __index = function(self, i: number) self[i] = {}; return self[i] end + }), + nested_stack = {} + } end -}) +end --- cache in strict mode to detect duplicate property set at same nesting level -local nested_debug_cache = {} :: Map> +local function return_caches(caches: typeof(free_caches) ) + free_caches = caches +end -setmetatable(nested_debug_cache :: any, { - __index = function(_, i: number) - nested_debug_cache[i] = {} - return nested_debug_cache[i] - end -}) - --- use stack instead of recursive function to process nested layers one at time --- deeper-nested properties take precedence over shallower-nested ones --- each nested layer occupies two indexes: 1. table ref 2. nested depth --- e.g. { t1 = { t3 = {} }, t2 = {} } -> { t1, 1, t2, 1, t3, 2 } -local nested_stack = {} :: { {} | number } - --- todo: solution without manual updating of this table -- map of datatype names to class default constructor for aggregate init local aggregates = {} - -for i, v in next, { +for name, class in { CFrame = CFrame, Color3 = Color3, UDim = UDim, @@ -55,27 +72,39 @@ for i, v in next, { Vector2 = Vector2, Vector3 = Vector3, Rect = Rect -} do - aggregates[i] = v.new +} :: Map do + aggregates[name] = class.new end --- processes a potentially nested table of values to assign to an instance -local function process_props(instance: Instance, properties: Map) +-- applies table of nested properties to an instance using full vide semantics +local function apply(instance: T & Instance, properties: { [unknown]: unknown }): T + if not properties then + throw("attempt to call a constructor returned by create() with no properties") + end + local strict = flags.strict - table.clear(nested_stack) - if strict then table.clear(nested_debug_cache) end + -- queue parent assignment if any for last + local parent: unknown = properties.Parent + local caches = borrow_caches() + local events = caches.events + local actions = caches.actions + local nested_debug = caches.nested_debug + local nested_stack = caches.nested_stack + + -- process all properties local depth = 1 - repeat for property, value in properties do + if property == "Parent" then continue end + if type(property) == "string" then - if strict then -- check for duplicate prop assignment at nesting layer - if nested_debug_cache[depth][property] then + if strict then -- check for duplicate prop assignment at nesting depth + if nested_debug[depth][property] then throw(`duplicate property {property} at depth {depth}`) end - nested_debug_cache[depth][property] = true + nested_debug[depth][property] = true end if type(value) == "table" then -- attempt aggregate init @@ -86,7 +115,7 @@ local function process_props(instance: Instance, properties: Map () -- add event to buffer + events[property] = value :: () -> () -- add event to buffer else bind.property(instance, property, value :: () -> ()) -- bind property end @@ -98,7 +127,7 @@ local function process_props(instance: Instance, properties: Map Instance | Array) -- bind children elseif type(value) == "table" then if is_action(value) then - table.insert(action_buffers[(value :: any).priority], (value :: any).callback :: () -> ()) -- add action to buffer + table.insert(actions[(value :: any).priority], (value :: any).callback :: () -> ()) -- add action to buffer else table.insert(nested_stack, value :: {}) table.insert(nested_stack, depth + 1) -- push table to stack for later processing @@ -109,40 +138,17 @@ local function process_props(instance: Instance, properties: Map(instance: T & Instance, properties: { [unknown]: unknown }): T - if not properties then - throw("no properties given, did you forget to call the constructor returned by create()?") + for event, listener in next, events do + (instance :: any)[event]:Connect(listener) end - -- queue parent assignment if any for last - local parent: unknown = properties.Parent - if parent then properties.Parent = nil end - - -- reset buffers - table.clear(event_buffer) - for _, buffer in next, action_buffers do - table.clear(buffer) - end - - -- process all properties for immediate setting or buffering - process_props(instance, properties) - - -- connect buffered events - for event, fn in next, event_buffer do - (instance :: any)[event]:Connect(fn) - end - - -- run buffered actions - for _, buffer in next, action_buffers do - for _, callback in next, buffer do + for _, queued in next, actions do + for _, callback in next, queued do callback(instance) end end @@ -156,6 +162,14 @@ local function apply(instance: T & Instance, properties: { [unknown]: unknown end end + -- clear caches + table.clear(events) + for _, queued in next, actions do table.clear(queued) end + if strict then table.clear(nested_debug) end + table.clear(nested_stack) + + return_caches(caches) + return instance end diff --git a/src/bind.luau b/src/bind.luau index c614590..4a204f1 100644 --- a/src/bind.luau +++ b/src/bind.luau @@ -5,7 +5,7 @@ local flags = require(script.Parent.flags) local graph = require(script.Parent.graph) type Node = graph.Node local create_node = graph.create_node -local get_owning_scope = graph.get_owning_scope +local assert_owning_scope = graph.assert_owning_scope local evaluate_node = graph.evaluate_node local set_owner = graph.set_owner @@ -31,8 +31,7 @@ function create_binding(updater: (T) -> T, binding: T) end end - - local owner = get_owning_scope() + local owner = assert_owning_scope() local node = create_node(binding, updater) diff --git a/src/create.luau b/src/create.luau index 0ce889e..7adfc68 100644 --- a/src/create.luau +++ b/src/create.luau @@ -5,41 +5,51 @@ local Instance = game and Instance or require "test/mock".Instance :: never local throw = require(script.Parent.throw) local defaults = require(script.Parent.defaults) local apply = require(script.Parent.apply) -local memoize = require(script.Parent.memoize) + +local ctor_cache = {} :: { [string]: () -> Instance } + +setmetatable(ctor_cache :: any, { + __index = function(self, class) + 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] + if default then + for i, v in next, default do + (instance :: any)[i] = v + end + end + + local function ctor(properties: Props): Instance + return apply(instance:Clone(), properties) + end + + self[class] = ctor + return ctor + 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] - if default then - for i, v in next, default do - (instance :: any)[i] = v - end - end - - return function(properties: { [any]: unknown }): Instance - return apply(instance:Clone(), properties) - end -end; create_instance = memoize(create_instance) -- always return same constructor for given class + return ctor_cache[class] +end local function clone_instance(instance: Instance) - return function(properties: { [any]: unknown }): Instance + return function(properties: Props): Instance local clone = instance:Clone() - if not clone then error("Attempt to clone a non-archivable instance", 3) end + if not clone then throw "attempt to clone a non-archivable instance" end return apply(clone, properties) end end -local function create(class_or_instance: string|Instance) +local function create(class_or_instance: string|Instance): (Props) -> Instance if type(class_or_instance) == "string" then return create_instance(class_or_instance) elseif typeof(class_or_instance) == "Instance" then return clone_instance(class_or_instance) else - throw("bad argument #1, expected string or instance, got "..typeof(class_or_instance)) + throw("bad argument #1, expected string or instance, got " .. typeof(class_or_instance)) + return nil :: never end - return nil :: never end type Props = { [any]: any } diff --git a/src/derive.luau b/src/derive.luau index 49094d5..863fd98 100644 --- a/src/derive.luau +++ b/src/derive.luau @@ -4,11 +4,11 @@ local graph = require(script.Parent.graph) local create_node = graph.create_node local set_owner = graph.set_owner local track = graph.track -local get_owning_scope = graph.get_owning_scope +local assert_owning_scope = graph.assert_owning_scope local evaluate_node = graph.evaluate_node local function derive(source: () -> T): () -> T - local owner = get_owning_scope() + local owner = assert_owning_scope() local node = create_node(false :: any, source) diff --git a/src/effect.luau b/src/effect.luau index 43b12ab..bbe1669 100644 --- a/src/effect.luau +++ b/src/effect.luau @@ -2,12 +2,12 @@ if not game then script = require "test/relative-string" end local graph = require(script.Parent.graph) local create_node = graph.create_node -local get_owning_scope = graph.get_owning_scope +local assert_owning_scope = graph.assert_owning_scope local evaluate_node = graph.evaluate_node local set_owner = graph.set_owner local function effect(callback: (T) -> T, initial_value: T) - local owner = get_owning_scope() + local owner = assert_owning_scope() local node = create_node(initial_value, callback) diff --git a/src/graph.luau b/src/graph.luau index 66587a8..ecda1ab 100644 --- a/src/graph.luau +++ b/src/graph.luau @@ -40,7 +40,7 @@ local function get_scope(): Node? return scopes[scopes.n] end -local function get_owning_scope(): Node +local function assert_owning_scope(): Node local scope = get_scope() if not scope then @@ -187,7 +187,7 @@ end local _flushing = false local function flush_update_queue() - assert(not flushing, "recursive queue flush occured") -- todo + assert(not _flushing, "recursive queue flush occured") -- todo _flushing = true local n0 = 0 @@ -266,7 +266,7 @@ return table.freeze { close_scope = close_scope, evaluate_node = evaluate_node, get_scope = get_scope, - get_owning_scope = get_owning_scope, + assert_owning_scope = assert_owning_scope, add_cleanup = add_cleanup, set_owner = set_owner, destroy = destroy, diff --git a/src/init.luau b/src/init.luau index cfc49d0..17812c3 100644 --- a/src/init.luau +++ b/src/init.luau @@ -14,6 +14,7 @@ local effect = require(script.effect) local derive = require(script.derive) local cleanup = require(script.cleanup) local untrack = require(script.untrack) +local read = require(script.read) local batch = require(script.batch) local switch = require(script.switch) local show = require(script.show) @@ -60,10 +61,8 @@ local vide = { -- util cleanup = cleanup, untrack = untrack, + read = read, batch = batch, - read = function(value: T | () -> T): T - return if type(value) == "function" then value() else value - end, -- animations spring = spring, diff --git a/src/maps.luau b/src/maps.luau index 1034e9a..ef36209 100644 --- a/src/maps.luau +++ b/src/maps.luau @@ -10,7 +10,7 @@ local create_start_node = graph.create_start_node local set_owner = graph.set_owner local track = graph.track local update = graph.update -local get_owning_scope = graph.get_owning_scope +local assert_owning_scope = graph.assert_owning_scope local open_scope = graph.open_scope local close_scope = graph.close_scope local evaluate_node = graph.evaluate_node @@ -28,7 +28,7 @@ local function check_primitives(t: {}) end local function indexes(input: () -> Map, transform: (() -> VI, K) -> VO): () -> { VO } - local owner = get_owning_scope() + local owner = assert_owning_scope() local subowner = create_node(false, false) set_owner(subowner, owner) @@ -123,7 +123,7 @@ local function indexes(input: () -> Map, transform: (() -> VI, end local function values(input: () -> Map, transform: (VI, () -> K) -> VO): () -> { VO } - local owner = get_owning_scope() + local owner = assert_owning_scope() local subowner = create_node(false, false) set_owner(subowner, owner) diff --git a/src/memoize.luau b/src/memoize.luau deleted file mode 100644 index cf83427..0000000 --- a/src/memoize.luau +++ /dev/null @@ -1,17 +0,0 @@ -local function memoize(f: (X) -> Y): (X) -> Y - local cache: { [X]: Y? } = {} - - return function(x: X): Y - local y = cache[x] - - if not y then - y = f(x) - cache[x] = y - end - - return y :: Y - end -end - -return memoize - diff --git a/src/read.luau b/src/read.luau new file mode 100644 index 0000000..d3a2fb7 --- /dev/null +++ b/src/read.luau @@ -0,0 +1,7 @@ +if not game then script = require "test/relative-string" end + +local function read(value: T | () -> T): T + return if type(value) == "function" then value() else value +end + +return read diff --git a/src/source.luau b/src/source.luau index f1307fc..dd633bb 100644 --- a/src/source.luau +++ b/src/source.luau @@ -6,7 +6,7 @@ local create_start_node = graph.create_start_node local track = graph.track local update = graph.update -export type Source = (() -> T) & ((T) -> T) +export type Source = (() -> T) & ((value: T) -> T) local function source(initial_value: T): Source local node = create_start_node(initial_value) diff --git a/src/spring.luau b/src/spring.luau index b627a74..91f17b1 100644 --- a/src/spring.luau +++ b/src/spring.luau @@ -27,7 +27,7 @@ type Node = graph.Node type StartNode = graph.StartNode local create_node = graph.create_node local create_start_node = graph.create_start_node -local get_owning_scope = graph.get_owning_scope +local assert_owning_scope = graph.assert_owning_scope local evaluate_node = graph.evaluate_node local update = graph.update local set_owner = graph.set_owner @@ -150,7 +150,7 @@ local springs: { [SpringData]: StartNode } = {} setmetatable(springs, { __mode = "v" }) local function spring(source: () -> T, period: number?, damping_ratio: number?): () -> T - local owner = get_owning_scope() + local owner = assert_owning_scope() -- https://en.wikipedia.org/wiki/Damping diff --git a/src/switch.luau b/src/switch.luau index 1ddc8e6..12fd376 100644 --- a/src/switch.luau +++ b/src/switch.luau @@ -9,14 +9,14 @@ local evaluate_node = graph.evaluate_node local set_owner = graph.set_owner local track = graph.track local destroy = graph.destroy -local get_owning_scope = graph.get_owning_scope +local assert_owning_scope = graph.assert_owning_scope local open_scope = graph.open_scope local close_scope = graph.close_scope type Map = { [K]: V } local function switch(source: () -> T): (map: Map U)?)>) -> () -> U? - local owner = get_owning_scope() + local owner = assert_owning_scope() return function(map) local last_scope: Node? @@ -35,7 +35,7 @@ local function switch(source: () -> T): (map: Map U)?)>) -> () if component == nil then return nil end if type(component) ~= "function" then - throw("map must map a value to a function") + throw "map must map a value to a function" end local new_scope = create_node(false, false) diff --git a/src/throw.luau b/src/throw.luau index d3ea687..70b7973 100644 --- a/src/throw.luau +++ b/src/throw.luau @@ -3,7 +3,7 @@ if not game then script = require "test/relative-string" end local trace = require(script.Parent.trace) local function throw(msg): any - error(msg, trace()-1) + error(msg, trace() - 1) end return throw diff --git a/test/tests.luau b/test/tests.luau index 94c7bb4..c38c6a7 100644 --- a/test/tests.luau +++ b/test/tests.luau @@ -942,6 +942,33 @@ TEST("create()", wrap_root(function() CHECK(not wref[1]) end + do CASE "recursive create" + local set_test_to_true = vide.action(function(self) (self :: any).test = true end) + + local f2 + + local to_apply = { + { a = 1 }, + set_test_to_true, + b = function() f2 = create "Frame" { a = 2 } end, + } :: { [number|string]: unknown } + + -- do -- confirm iteration order + -- local t = {} + -- for i in to_apply do + -- table.insert(t, i) + -- end + -- assert(t[1] == "a") + -- end + + local f = create "Frame" (to_apply) + + CHECK((f :: any).a == 1) + CHECK((f :: any).test == true ) + + CHECK((f2 :: any).a == 2) + end + do CASE "garbage collection test" local wref