Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 28 additions & 22 deletions src/signals/Signals__Computed.res
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,42 @@ module Core = Signals__Core
module Scheduler = Signals__Scheduler

let make = (compute: unit => 'a, ~name: option<string>=?): Signal.t<'a> => {
// Create backing signal with magic initial value (optimized path)
let backingSignal = Signal.makeForComputed((Obj.magic(): 'a), ~name?)
let id = Id.make()

// Create observer ID
let observerId = Id.make()
// Create a mutable ref to hold the signal so the compute function can update it
// Using Obj.magic to avoid Option wrapper overhead
let signalRef: ref<Signal.t<'a>> = ref(Obj.magic())

// Recompute function - updates backing signal's value directly
// Recompute function - updates the signal's value directly
let recompute = () => {
let newValue = compute()
backingSignal.value = newValue
signalRef.contents.value = compute()
}

// Create observer using Core types, with backingSubs for dirty propagation
let observer = Core.makeObserver(observerId, #Computed(backingSignal.id), recompute, ~name?, ~backingSubs=backingSignal.subs)

// Initial computation under tracking (no clearDeps needed - observer is fresh)
let prev = Scheduler.currentObserver.contents
Scheduler.currentObserver := Some(observer)
observer.run()
Core.clearDirty(observer)
Scheduler.currentObserver := prev

// Level will be computed lazily on first retrack (starts at 0)
// Create combined subs (this IS the observer for the computed)
let subs = Core.makeComputedSubs(recompute)

// Initial computation under tracking to establish dependencies
let prev = Scheduler.currentComputedSubs.contents
Scheduler.currentComputedSubs := Some(subs)
let initialValue = compute()
Scheduler.currentComputedSubs := prev

// Create the signal with the initial value
let signal: Signal.t<'a> = {
id,
value: initialValue,
equals: (_, _) => false, // Computeds always check freshness via dirty flag
name,
subs,
}

// Register for lookup by signal ID (needed for ensureComputedFresh and dirty propagation)
Scheduler.registerComputed(backingSignal.id, observer, backingSignal.subs)
// Set the ref so recompute can access the signal
signalRef := signal
Core.clearSubsDirty(subs)

backingSignal
signal
}

let dispose = (signal: Signal.t<'a>): unit => {
Scheduler.unregisterComputed(signal.id, signal.subs)
Core.clearSubsDeps(signal.subs)
}
109 changes: 91 additions & 18 deletions src/signals/Signals__Core.res
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ let flag_pending = 2
let flag_running = 4

// Observer kind tag
type kind = [#Effect | #Computed(int)]
type kind = [#Effect | #Computed]

// Forward declare mutually recursive types
type rec link = {
Expand All @@ -24,35 +24,66 @@ type rec link = {
}

// Signal subscriber list (head/tail of linked list)
// For computeds, this same object also serves as the observer (combined structure)
and subs = {
mutable first: option<link>,
mutable last: option<link>,
mutable version: int, // signal version for freshness check
// For computed signals: direct reference to backing observer (avoids Map lookup)
mutable computedObserver: option<observer>,
mutable version: int,
// === Observer fields (only used for computeds) ===
// If compute is Some, this subs is a computed signal
mutable compute: option<unit => unit>,
mutable firstDep: option<link>,
mutable lastDep: option<link>,
mutable flags: int,
mutable level: int,
}

// Observer with dependency list
// Observer for effects only (computeds use subs directly)
and observer = {
id: int,
kind: kind,
run: unit => unit,
// Dependency linked list (replaces Set<int>)
mutable firstDep: option<link>,
mutable lastDep: option<link>,
// State flags (replaces dirty: bool)
mutable flags: int,
mutable level: int,
name: option<string>,
// For computed observers: direct reference to backing signal's subs (avoids Map lookup)
// For computed observers: direct reference to backing subs (the combined object)
mutable backingSubs: option<subs>,
}

// Create empty subscriber list
let makeSubs = (): subs => {first: None, last: None, version: 0, computedObserver: None}
// Create empty subscriber list (for plain signals)
let makeSubs = (): subs => {
first: None,
last: None,
version: 0,
compute: None,
firstDep: None,
lastDep: None,
flags: 0,
level: 0,
}

// Create subs for a computed (with compute function)
let makeComputedSubs = (compute: unit => unit): subs => {
first: None,
last: None,
version: 0,
compute: Some(compute),
firstDep: None,
lastDep: None,
flags: flag_dirty, // start dirty
level: 0,
}

// Create observer
let makeObserver = (id: int, kind: kind, run: unit => unit, ~name: option<string>=?, ~backingSubs: option<subs>=?): observer => {
let makeObserver = (
id: int,
kind: kind,
run: unit => unit,
~name: option<string>=?,
~backingSubs: option<subs>=?,
): observer => {
id,
kind,
run,
Expand All @@ -64,13 +95,28 @@ let makeObserver = (id: int, kind: kind, run: unit => unit, ~name: option<string
backingSubs,
}

// Flag operations (using Int.Bitwise module)
let isDirty = (o: observer): bool => Int.Bitwise.land(o.flags, flag_dirty) !== 0
let setDirty = (o: observer): unit => o.flags = Int.Bitwise.lor(o.flags, flag_dirty)
let clearDirty = (o: observer): unit => o.flags = Int.Bitwise.land(o.flags, Int.Bitwise.lnot(flag_dirty))
let isPending = (o: observer): bool => Int.Bitwise.land(o.flags, flag_pending) !== 0
let setPending = (o: observer): unit => o.flags = Int.Bitwise.lor(o.flags, flag_pending)
let clearPending = (o: observer): unit => o.flags = Int.Bitwise.land(o.flags, Int.Bitwise.lnot(flag_pending))
// Flag operations for observer (using Int.Bitwise module)
let isDirty = (o: observer): bool => Int.bitwiseAnd(o.flags, flag_dirty) !== 0
let setDirty = (o: observer): unit => o.flags = Int.bitwiseOr(o.flags, flag_dirty)
let clearDirty = (o: observer): unit =>
o.flags = Int.bitwiseAnd(o.flags, Int.bitwiseNot(flag_dirty))
let isPending = (o: observer): bool => Int.bitwiseAnd(o.flags, flag_pending) !== 0
let setPending = (o: observer): unit => o.flags = Int.bitwiseOr(o.flags, flag_pending)
let clearPending = (o: observer): unit =>
o.flags = Int.bitwiseAnd(o.flags, Int.bitwiseNot(flag_pending))

// Flag operations for subs (for computeds - subs IS the observer)
let isSubsDirty = (s: subs): bool => Int.bitwiseAnd(s.flags, flag_dirty) !== 0
let setSubsDirty = (s: subs): unit => s.flags = Int.bitwiseOr(s.flags, flag_dirty)
let clearSubsDirty = (s: subs): unit =>
s.flags = Int.bitwiseAnd(s.flags, Int.bitwiseNot(flag_dirty))
let isSubsPending = (s: subs): bool => Int.bitwiseAnd(s.flags, flag_pending) !== 0
let setSubsPending = (s: subs): unit => s.flags = Int.bitwiseOr(s.flags, flag_pending)
let clearSubsPending = (s: subs): unit =>
s.flags = Int.bitwiseAnd(s.flags, Int.bitwiseNot(flag_pending))

// Check if subs is a computed (has compute function)
let isComputed = (s: subs): bool => s.compute !== None

// Create a link node
let makeLink = (subs: subs, observer: observer): link => {
Expand Down Expand Up @@ -148,3 +194,30 @@ let clearDeps = (observer: observer): unit => {
observer.firstDep = None
observer.lastDep = None
}

// Clear all dependencies from subs (for computeds - subs IS the observer)
let clearSubsDeps = (s: subs): unit => {
let link = ref(s.firstDep)
while link.contents !== None {
switch link.contents {
| Some(l) =>
let next = l.nextDep
unlinkFromSubs(l)
link := next
| None => ()
}
}
s.firstDep = None
s.lastDep = None
}

// Add link to subs's dependency list (for computeds - subs IS the observer)
let linkToSubsDeps = (s: subs, link: link): unit => {
link.prevDep = s.lastDep
link.nextDep = None
switch s.lastDep {
| Some(last) => last.nextDep = Some(link)
| None => s.firstDep = Some(link)
}
s.lastDep = Some(link)
}
4 changes: 1 addition & 3 deletions src/signals/Signals__Effects.res
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,7 @@ let run = (fn: unit => option<unit => unit>, ~name: option<string>=?): disposer
// Create observer using Core types
let observer = Core.makeObserver(observerId, #Effect, runWithCleanup, ~name?)

// Initial run under tracking
Core.clearDeps(observer)

// Initial run under tracking (no need to clearDeps - observer is fresh)
let prev = Scheduler.currentObserver.contents
Scheduler.currentObserver := Some(observer)

Expand Down
Loading