@@ -6,51 +6,73 @@ let flag_dirty = 1
66let flag_pending = 2
77let flag_running = 4
88
9+ // Global tracking version
10+ let trackingVersion : ref <int > = ref (0 )
11+
912// Observer kind tag
1013type kind = [#Effect | #Computed ]
1114
12- // Forward declare mutually recursive types
13- type rec link = {
14- // Direct reference to signal's subscriber list (type-erased)
15- mutable subs : subs ,
16- // Direct reference to observer
17- mutable observer : observer ,
18- // Links in the observer's dependency chain
19- mutable nextDep : option <link >,
20- mutable prevDep : option <link >,
21- // Links in the signal's subscriber chain
22- mutable nextSub : option <link >,
23- mutable prevSub : option <link >,
24- }
15+ // Mutually recursive types using recursive modules
16+ module rec Link : {
17+ type t = {
18+ // Direct reference to signal's subscriber list (type-erased)
19+ mutable subs : Subs .t ,
20+ // Direct reference to observer
21+ mutable observer : Observer .t ,
22+ // Links in the observer's dependency chain
23+ mutable nextDep : option <Link .t >,
24+ mutable prevDep : option <Link .t >,
25+ // Links in the signal's subscriber chain
26+ mutable nextSub : option <Link .t >,
27+ mutable prevSub : option <Link .t >,
28+ // Version stamp for duplicate detection within a compute cycle
29+ mutable lastTrackedVersion : int ,
30+ }
31+ } = Link
2532
2633// Signal subscriber list (head/tail of linked list)
2734// For computeds, this same object also serves as the observer (combined structure)
28- and subs = {
29- mutable first : option <link >,
30- mutable last : option <link >,
31- mutable version : int ,
32- // === Observer fields (only used for computeds) ===
33- // If compute is Some, this subs is a computed signal
34- mutable compute : option <unit => unit >,
35- mutable firstDep : option <link >,
36- mutable lastDep : option <link >,
37- mutable flags : int ,
38- mutable level : int ,
39- }
35+ and Subs : {
36+ type t = {
37+ mutable first : option <Link .t >,
38+ mutable last : option <Link .t >,
39+ mutable version : int ,
40+ // === Observer fields (only used for computeds) ===
41+ // If compute is Some, this subs is a computed signal
42+ mutable compute : option <unit => unit >,
43+ mutable firstDep : option <Link .t >,
44+ mutable lastDep : option <Link .t >,
45+ mutable flags : int ,
46+ mutable level : int ,
47+ // Current tracking version for this compute cycle (for link reuse)
48+ mutable currentTrackingVersion : int ,
49+ // Whether the last recompute changed the value (for equality short-circuit)
50+ mutable valueChanged : bool ,
51+ }
52+ } = Subs
4053
4154// Observer for effects only (computeds use subs directly)
42- and observer = {
43- id : int ,
44- kind : kind ,
45- run : unit => unit ,
46- mutable firstDep : option <link >,
47- mutable lastDep : option <link >,
48- mutable flags : int ,
49- mutable level : int ,
50- name : option <string >,
51- // For computed observers: direct reference to backing subs (the combined object)
52- mutable backingSubs : option <subs >,
53- }
55+ and Observer : {
56+ type t = {
57+ id : int ,
58+ kind : kind ,
59+ run : unit => unit ,
60+ mutable firstDep : option <Link .t >,
61+ mutable lastDep : option <Link .t >,
62+ mutable flags : int ,
63+ mutable level : int ,
64+ name : option <string >,
65+ // For computed observers: direct reference to backing subs (the combined object)
66+ mutable backingSubs : option <Subs .t >,
67+ // Current tracking version for this run cycle (for link reuse)
68+ mutable currentTrackingVersion : int ,
69+ }
70+ } = Observer
71+
72+ // Type aliases for convenience
73+ type link = Link .t
74+ type subs = Subs .t
75+ type observer = Observer .t
5476
5577// Create empty subscriber list (for plain signals)
5678let makeSubs = (): subs => {
@@ -62,6 +84,8 @@ let makeSubs = (): subs => {
6284 lastDep : None ,
6385 flags : 0 ,
6486 level : 0 ,
87+ currentTrackingVersion : 0 ,
88+ valueChanged : true , // Plain signals always "changed"
6589}
6690
6791// Create subs for a computed (with compute function)
@@ -74,6 +98,8 @@ let makeComputedSubs = (compute: unit => unit): subs => {
7498 lastDep : None ,
7599 flags : flag_dirty , // start dirty
76100 level : 0 ,
101+ currentTrackingVersion : 0 ,
102+ valueChanged : true , // Start as changed for initial computation
77103}
78104
79105// Create observer
@@ -93,9 +119,10 @@ let makeObserver = (
93119 level : 0 ,
94120 name ,
95121 backingSubs ,
122+ currentTrackingVersion : 0 ,
96123}
97124
98- // Flag operations for observer (using Int.Bitwise module)
125+ // Flag operations for observer
99126let isDirty = (o : observer ): bool => Int .bitwiseAnd (o .flags , flag_dirty ) !== 0
100127let setDirty = (o : observer ): unit => o .flags = Int .bitwiseOr (o .flags , flag_dirty )
101128let clearDirty = (o : observer ): unit =>
@@ -105,7 +132,7 @@ let setPending = (o: observer): unit => o.flags = Int.bitwiseOr(o.flags, flag_pe
105132let clearPending = (o : observer ): unit =>
106133 o .flags = Int .bitwiseAnd (o .flags , Int .bitwiseNot (flag_pending ))
107134
108- // Flag operations for subs (for computeds - subs IS the observer)
135+ // Flag operations for subs
109136let isSubsDirty = (s : subs ): bool => Int .bitwiseAnd (s .flags , flag_dirty ) !== 0
110137let setSubsDirty = (s : subs ): unit => s .flags = Int .bitwiseOr (s .flags , flag_dirty )
111138let clearSubsDirty = (s : subs ): unit =>
@@ -115,7 +142,7 @@ let setSubsPending = (s: subs): unit => s.flags = Int.bitwiseOr(s.flags, flag_pe
115142let clearSubsPending = (s : subs ): unit =>
116143 s .flags = Int .bitwiseAnd (s .flags , Int .bitwiseNot (flag_pending ))
117144
118- // Check if subs is a computed (has compute function)
145+ // Check if subs is a computed
119146let isComputed = (s : subs ): bool => s .compute !== None
120147
121148// Create a link node
@@ -126,6 +153,7 @@ let makeLink = (subs: subs, observer: observer): link => {
126153 prevDep : None ,
127154 nextSub : None ,
128155 prevSub : None ,
156+ lastTrackedVersion : 0 ,
129157}
130158
131159// Add link to signal's subscriber list
@@ -179,6 +207,20 @@ let unlinkFromDeps = (observer: observer, link: link): unit => {
179207 link .nextDep = None
180208}
181209
210+ // Remove link from subs's dependency list (for computeds - subs IS the observer)
211+ let unlinkFromSubsDeps = (s : subs , link : link ): unit => {
212+ switch link .prevDep {
213+ | Some (prev ) => prev .nextDep = link .nextDep
214+ | None => s .firstDep = link .nextDep
215+ }
216+ switch link .nextDep {
217+ | Some (next ) => next .prevDep = link .prevDep
218+ | None => s .lastDep = link .prevDep
219+ }
220+ link .prevDep = None
221+ link .nextDep = None
222+ }
223+
182224// Clear all dependencies from observer (unlinks from all signals)
183225let clearDeps = (observer : observer ): unit => {
184226 let link = ref (observer .firstDep )
0 commit comments