diff --git a/src/Change.lua b/src/Change.lua index 17c83b7..262dc33 100644 --- a/src/Change.lua +++ b/src/Change.lua @@ -39,7 +39,7 @@ local getChangeSymbol = memoize(function(name: string): Types.Symbol, instance, event) else error("Attempt to connect non-function to changed event", 2) diff --git a/src/derive.lua b/src/derive.lua index 1a0269a..c48f39c 100644 --- a/src/derive.lua +++ b/src/derive.lua @@ -24,7 +24,7 @@ local function derive(deriveValue: (Unwrapper) -> T, cleanup: (T) -> ()?): St end local value: T = captureAndLink(node, deriveValue) - node.cache = value + rawset(node, "__cache", value) return node :: State end diff --git a/src/graph.lua b/src/graph.lua index 3238bfc..d1c5531 100644 --- a/src/graph.lua +++ b/src/graph.lua @@ -8,11 +8,11 @@ local flags = require(script.Parent.flags) export type State = typeof(setmetatable( {} :: { - cache: T, - updated: boolean, - derive: (any) -> T, - effects: { [(unknown) -> ()]: unknown } | false, -- weak values - children: { State } | false -- weak values + __cache: T, + __updated: boolean, + __derive: (any) -> T, + __effects: { [(unknown) -> ()]: unknown } | false, -- weak values + __children: { State } | false -- weak values }, {} :: { __concat: (any, any) -> any, __add: (any, any) -> any, @@ -32,7 +32,7 @@ export type MaybeState = State | T export type Unwrapper = (T) -> T local WEAK_VALUES_RESIZABLE = { __mode = "vs" } -local EVALUATION_ERR = "Error while evaluating state:\n\n" +local EVALUATION_ERR = "error while evaluating state:\n\n" local State = {} @@ -56,7 +56,7 @@ local checkForYield do if not ok then if err == "attempt to yield across metamethod/C-call boundary" or err == "thread is not yieldable" then - error(EVALUATION_ERR .. "Cannot yield when deriving state in watcher", 3) + error(EVALUATION_ERR .. "cannot yield when deriving state in watcher", 3) else error(EVALUATION_ERR..err, 3) end @@ -65,16 +65,16 @@ local checkForYield do end local function setEffect(state: State, fn: (T) -> (), key: T) - if not state.effects then - state.effects = setmetatable({ [fn] = key }, WEAK_VALUES_RESIZABLE) :: any + if not state.__effects then + state.__effects = setmetatable({ [fn] = key }, WEAK_VALUES_RESIZABLE) :: any else - state.effects[fn :: () -> ()] = key + state.__effects[fn :: () -> ()] = key end end local function runEffects(state: State) - if state.effects then - for effect, key in next, state.effects do + if state.__effects then + for effect, key in next, state.__effects do if flags.strict then effect(key) end effect(key) end @@ -84,17 +84,17 @@ end -- retrieves a state's cached value -- recalculates value if an ancestor was updated local function get(state: State): T - if state.updated then - state.updated = false + if state.__updated then + state.__updated = false - if flags.strict then checkForYield(state.derive) end + if flags.strict then checkForYield(state.__derive) end - local ok, result: T|string? = pcall(state.derive, unwrap); if ok then - rawset(state :: any, "cache", result :: T) - else error(EVALUATION_ERR .. result :: string, 2) end + local ok, result: T|string? = pcall(state.__derive, unwrap); if ok then + rawset(state :: any, "__cache", result :: T) + else error(EVALUATION_ERR .. result :: string, 0) end end - return state.cache + return rawget(state :: any, "__cache") end -- utility function for retrieving a value from state and allowing passthrough of non-state @@ -107,20 +107,20 @@ unwrap = function(value: MaybeState): T end local function addChild(parent: State, child: State) - if parent.children then - table.insert(parent.children, child) + if parent.__children then + table.insert(parent.__children, child) else - parent.children = setmetatable({ child }, WEAK_VALUES_RESIZABLE) :: any + parent.__children = setmetatable({ child }, WEAK_VALUES_RESIZABLE) :: any end end -- marks all state descendants for recalculation and runs effects local function update(state: State) runEffects(state) - if state.children then - for _, child in state.children do - if not child.updated then - child.updated = true + if state.__children then + for _, child in state.__children do + if not child.__updated then + child.__updated = true update(child) end end @@ -129,13 +129,13 @@ end -- sets a state's cached value and updates all descendants local function set(state: State, value: T) - state.cache = value + state.__cache = value update(state) end -- links two states as parent-child local function link(parent: State, child: State, derive: () -> unknown) - child.derive = derive + child.__derive = derive addChild(parent, child) end @@ -154,7 +154,7 @@ local function capture(callback: (Unwrapper) -> T): ({ State }, T) end end) - if not ok then error("Error while detecting watcher: " .. result :: string, 2) end + if not ok then error("error while detecting watcher: " .. result :: string, 0) end return states, result :: T end @@ -163,7 +163,7 @@ end local function captureAndLink(child: State, callback: (Unwrapper) -> T): T local states, value = capture(callback) - child.derive = callback + child.__derive = callback for _, parent: State in next, states do addChild(parent, child) end @@ -191,30 +191,61 @@ local function overload(op: (unknown, unknown) -> unknown): (any, any) -> any link(b :: State, derived, function() return op(a, get(b :: State)) end) end - derived.updated = true + derived.__updated = true return derived end end -function State.__index(_, index) - if index == "cache" then return nil end -- todo: better solution - error("attempt to index state", 2) +local function __unm(self: State) + local derived = create(nil :: any) + + link(self, derived, function() + return -get(self) :: number + end) + + derived.__updated = true + return derived end +local function __index(self: State, index: unknown) + local derived = create(nil :: any) + + link(self, derived, function() + return (get(self) :: {})[index] + end) + + derived.__updated = true + return derived +end + +State.__index = __index State.__concat = overload(function(a: any, b: any) return tostring(a) .. tostring(b) end) State.__add = overload(function(a: any, b: any) return a + b end) State.__sub = overload(function(a: any, b: any) return a - b end) State.__mul = overload(function(a: any, b: any) return a * b end) State.__div = overload(function(a: any, b: any) return a / b end) ---State.__eq = overload(function(a: any, b: any) return a == b end) +State.__pow = overload(function(a: any, b: any) return a ^ b end) +State.__mod = overload(function(a: any, b: any) return a % b end) +State.__unm = __unm + +-- todo: what to do +do + local function err() + error("cannot perform equality comparison with state", 2) + end + State.__eq = err + State.__lt = err + State.__le = err +end + function create(value: T): State return setmetatable({ - cache = value, - updated = false, - derive = function() return nil end :: any, - effects = false :: false, - children = false :: false + __cache = value, + __updated = false, + __derive = function() return nil end :: any, + __effects = false :: false, + __children = false :: false }, State) end diff --git a/src/map.lua b/src/map.lua index b805a23..11563bf 100644 --- a/src/map.lua +++ b/src/map.lua @@ -55,7 +55,7 @@ local function map(input: unknown, transform: (K, VI) -> VO, cleanup: end link(input :: State>, output, derive) - output.updated = true + output.__updated = true return output elseif type(input) == "table" then diff --git a/src/wrap.lua b/src/wrap.lua index 5eb41dc..ef00069 100644 --- a/src/wrap.lua +++ b/src/wrap.lua @@ -27,7 +27,7 @@ local function wrap(value: MaybeState?): (State, Setter) local v = if wrapped(vi) then get(vi :: State) else vi :: T - if v ~= state.cache or force then + if v ~= state.__cache or force then set(state, v) elseif flags.strict and type(v) == "table" then throw("attempt to set same table object") diff --git a/test/tests.luau b/test/tests.luau index d199aa5..04865e5 100644 --- a/test/tests.luau +++ b/test/tests.luau @@ -105,8 +105,8 @@ TEST("graph", function() set(a, get(a) + 1) -- mark `b` for recomputation again captureAndLink(c, function(from) return from(b) end) -- check that only `b` was linked - CHECK(not rawfind(assert(a.children), c)) - CHECK(rawfind(assert(b.children), c)) + CHECK(not rawfind(assert(a.__children), c)) + CHECK(rawfind(assert(b.__children), c)) end do CASE "Nodes garbage collection" @@ -291,6 +291,30 @@ TEST("derive()", function() set(2) CHECK(unwrap(text) == "22") end + + do + local a, seta = wrap { b = { c = 1 } } + local b = a.b + local c = b.c + + CHECK(unwrap(c) == 1) + + seta { b = { c = 2 } } + + CHECK(unwrap(c) == 2) + end + + do + local state, set = wrap { profiles = { decimal = { level = 1 }}} + local stringified = "Level: " .. state.profiles.decimal.level + + set(function(state) + state.profiles.decimal.level += 1 + return state + end, true) + + CHECK(unwrap(stringified) == "Level: 2") + end end do CASE "Derive from updated" @@ -704,7 +728,7 @@ TEST("create()", function() } wref.instance = instance - wref.binding = next((state :: any).effects) + wref.binding = next((state :: any).__effects) end CHECK(wref.binding)