github.com/vugu/vugu@v0.3.6-0.20240430171613-3f6f402e014b/mod-check-default.go (about) 1 //go:build !tinygo 2 // +build !tinygo 3 4 package vugu 5 6 import ( 7 "bytes" 8 "errors" 9 "fmt" 10 "reflect" 11 "strings" 12 "unsafe" 13 14 "github.com/vugu/xxhash" 15 ) 16 17 // type ModCheckedString string 18 // func (s ModCheckedString) ModCheck(mt *ModTracker, oldData interface{}) (isModified bool, newData interface{}) { 19 // } 20 21 // ModTracker tracks modifications and maintains the appropriate state for this. 22 type ModTracker struct { 23 old map[interface{}]mtResult 24 cur map[interface{}]mtResult 25 } 26 27 // TrackNext moves the "current" information to the "old" position - starting a new round of change tracking. 28 // Calls to ModCheckAll after calling TrackNext will compare current values to the values from before the call to TrackNext. 29 // It is generally called once at the start of each build and render cycle. This method must be called at least 30 // once before doing modification checks. 31 func (mt *ModTracker) TrackNext() { 32 33 // lazy initialize 34 if mt.old == nil { 35 mt.old = make(map[interface{}]mtResult) 36 } 37 if mt.cur == nil { 38 mt.cur = make(map[interface{}]mtResult) 39 } 40 41 // remove all elements from old map 42 for k := range mt.old { 43 delete(mt.old, k) 44 } 45 46 // and swap them 47 mt.old, mt.cur = mt.cur, mt.old 48 49 } 50 51 //nolint:golint,unused 52 func (mt *ModTracker) dump() []byte { 53 var buf bytes.Buffer 54 fmt.Fprintf(&buf, "-- cur (len=%d): --\n", len(mt.cur)) 55 for k, v := range mt.cur { 56 fmt.Fprintf(&buf, " %#v = %#v\n", k, v) 57 } 58 fmt.Fprintf(&buf, "-- old (len=%d): --\n", len(mt.old)) 59 for k, v := range mt.old { 60 fmt.Fprintf(&buf, " %#v = %#v\n", k, v) 61 } 62 return buf.Bytes() 63 } 64 65 // Otherwise pointers to some built-in types are supported including all primitive single-value types - 66 // bool, int/uint and all variations, both float types, both complex types, string. 67 // Pointers to supported types are supported. 68 // Arrays, slices and maps(nope!) using supported types are supported. 69 // Pointers to structs will be checked by 70 // checking each field with a struct tag like `vugu:"modcheck"`. Slices and arrays of 71 // structs are okay, since their members have a stable position in memory and a pointer 72 // can be taken. Maps using structs however must use pointers to them 73 // (restriction applies to both keys and values) to be supported. 74 75 // ModCheckAll performs a modification check on the values provided. 76 // For values implementing the ModChecker interface, the ModCheck method will be called. 77 // All values passed should be pointers to the types described below. 78 // Single-value primitive types are supported. Structs are supported and 79 // and are traversed by calling ModCheckAll on each with the tag `vugu:"modcheck"`. 80 // Arrays and slices of supported types are supported, their length is compared as well 81 // as a pointer to each member. 82 // As a special case []byte is treated like a string. 83 // Maps are not supported at this time. 84 // Other weird and wonderful things like channels and funcs are not supported. 85 // Passing an unsupported type will result in a panic. 86 func (mt *ModTracker) ModCheckAll(values ...interface{}) (ret bool) { 87 88 for _, v := range values { 89 90 // check if we've already done a mod check on v 91 curres, ok := mt.cur[v] 92 if ok { 93 ret = ret || curres.modified 94 continue 95 } 96 97 // look up the old data 98 oldres := mt.old[v] 99 100 // the result of the mod check on v goes here 101 var mod bool 102 var newdata interface{} 103 104 { 105 // see if it implements the ModChecker interface 106 mc, ok := v.(ModChecker) 107 if ok { 108 mod, newdata = mc.ModCheck(mt, oldres.data) 109 goto handleData 110 } 111 112 // support for certain built-in types 113 switch vt := v.(type) { 114 115 case *string: 116 oldval, ok := oldres.data.(string) 117 mod = !ok || oldval != *vt 118 newdata = *vt 119 goto handleData 120 121 case *[]byte: // special case of []byte, handled like string not slice 122 oldval, ok := oldres.data.(string) 123 vts := string(*vt) 124 mod = !ok || oldval != vts 125 newdata = vts 126 goto handleData 127 128 case *bool: 129 oldval, ok := oldres.data.(bool) 130 mod = !ok || oldval != *vt 131 newdata = *vt 132 goto handleData 133 134 case *int: 135 oldval, ok := oldres.data.(int) 136 mod = !ok || oldval != *vt 137 newdata = *vt 138 goto handleData 139 140 case *int8: 141 oldval, ok := oldres.data.(int8) 142 mod = !ok || oldval != *vt 143 newdata = *vt 144 goto handleData 145 146 case *int16: 147 oldval, ok := oldres.data.(int16) 148 mod = !ok || oldval != *vt 149 newdata = *vt 150 goto handleData 151 152 case *int32: 153 oldval, ok := oldres.data.(int32) 154 mod = !ok || oldval != *vt 155 newdata = *vt 156 goto handleData 157 158 case *int64: 159 oldval, ok := oldres.data.(int64) 160 mod = !ok || oldval != *vt 161 newdata = *vt 162 goto handleData 163 164 case *uint: 165 oldval, ok := oldres.data.(uint) 166 mod = !ok || oldval != *vt 167 newdata = *vt 168 goto handleData 169 170 case *uint8: 171 oldval, ok := oldres.data.(uint8) 172 mod = !ok || oldval != *vt 173 newdata = *vt 174 goto handleData 175 176 case *uint16: 177 oldval, ok := oldres.data.(uint16) 178 mod = !ok || oldval != *vt 179 newdata = *vt 180 goto handleData 181 182 case *uint32: 183 oldval, ok := oldres.data.(uint32) 184 mod = !ok || oldval != *vt 185 newdata = *vt 186 goto handleData 187 188 case *uint64: 189 oldval, ok := oldres.data.(uint64) 190 mod = !ok || oldval != *vt 191 newdata = *vt 192 goto handleData 193 194 case *float32: 195 oldval, ok := oldres.data.(float32) 196 mod = !ok || oldval != *vt 197 newdata = *vt 198 goto handleData 199 200 case *float64: 201 oldval, ok := oldres.data.(float64) 202 mod = !ok || oldval != *vt 203 newdata = *vt 204 goto handleData 205 206 case *complex64: 207 oldval, ok := oldres.data.(complex64) 208 mod = !ok || oldval != *vt 209 newdata = *vt 210 goto handleData 211 212 case *complex128: 213 oldval, ok := oldres.data.(complex128) 214 mod = !ok || oldval != *vt 215 newdata = *vt 216 goto handleData 217 218 } 219 220 // when the scalpel (type switch) doesn't do it, 221 // gotta use the bonesaw (reflection) 222 223 rv := reflect.ValueOf(v) 224 225 // check pointer and deref 226 if rv.Kind() != reflect.Ptr { 227 panic(errors.New("type not implemented (pointer required): " + rv.String())) 228 } 229 rvv := rv.Elem() 230 231 // slice and array are treated the same 232 if rvv.Kind() == reflect.Slice || rvv.Kind() == reflect.Array { 233 234 l := rvv.Len() 235 236 // for slices and arrays we compute the hash of the raw contents, 237 // takes care of length and sequence changes 238 ha := xxhash.New() 239 240 var el0t reflect.Type 241 242 if l > 0 { 243 // use the unsafe package to make a byte slice corresponding to the raw slice contents 244 var bs []byte 245 // TODO: need to be fix later 246 bsh := (*reflect.SliceHeader)(unsafe.Pointer(&bs)) //nolint:staticcheck 247 248 el0 := rvv.Index(0) 249 el0t = el0.Type() 250 251 bsh.Data = el0.Addr().Pointer() // point to first element of slice 252 bsh.Len = l * int(el0t.Size()) 253 bsh.Cap = bsh.Len 254 255 // hash it 256 _, err := ha.Write(bs) 257 if err != nil { 258 panic(err) 259 } 260 } 261 262 hashval := ha.Sum64() 263 264 // use hashval as our data, and mark as modified if different 265 oldval, ok := oldres.data.(uint64) 266 mod = !ok || oldval != hashval 267 newdata = hashval 268 269 // for types that by definition have already been checked with the hash above, we're done 270 if el0t != nil { 271 switch el0t.Kind() { 272 case reflect.Bool, 273 reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, 274 reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, 275 reflect.Uintptr, 276 reflect.Float32, reflect.Float64, reflect.Complex64, reflect.Complex128, 277 reflect.String: 278 goto handleData 279 } 280 } 281 282 // recurse into each element and check, update mod as we go 283 // NOTE: for "deep" element types that can have changes within them, 284 // it's important to recurse into children even if mod is already 285 // true, otherwise we'll never call ModCheckAll on these children and 286 // never get an unmodified response 287 288 for i := 0; i < l; i++ { 289 290 // get pointer to the individual element and recurse into it 291 elv := rvv.Index(i).Addr().Interface() 292 mod = mt.ModCheckAll(elv) || mod 293 } 294 295 goto handleData 296 297 } 298 299 // for structs we iterate over the fields looked for tagged ones 300 if rvv.Kind() == reflect.Struct { 301 302 // just use bool(true) as the data value for a struct - this way it will always be modified the first time, 303 // but every subsequent check will solely depend on the checks on it's fields 304 oldval, ok := oldres.data.(bool) 305 mod = !ok || !oldval 306 newdata = true 307 308 rvvt := rvv.Type() 309 for i := 0; i < rvvt.NumField(); i++ { 310 // skip untagged fields 311 if !hasTagPart(rvvt.Field(i).Tag.Get("vugu"), "data") { 312 continue 313 } 314 315 // call ModCheckAll on pointer to field 316 mod = mt.ModCheckAll( 317 rvv.Field(i).Addr().Interface(), 318 ) || mod 319 320 } 321 322 goto handleData 323 } 324 325 // random stream of conciousness: should we check for a ModCheck implementation here and call it? 326 // We might want to follow these pointers or rather recurse into them (if not nil?) 327 // with a ModCheckAll. Also look at how this ties into maps (if at all), since theoretically 328 // we could compare a map by storing its length and basically doing for k, v := range m { ...ModCheckAll(&k,&v)... } 329 // actually no that won't work because k and v will have different locations each time - but still could 330 // potentially make some mapKeyValue struct that does what we need - but not vital to solve right now. 331 // Pointers to pointers will probably come up first, and is likely why you are reading this comment. 332 333 // pointer (meaning we were originally passed a pointer to a pointer) 334 if rvv.Kind() == reflect.Ptr { 335 336 // use pointer value as data... 337 vv := rvv.Pointer() 338 oldval, ok := oldres.data.(uintptr) 339 mod = !ok || oldval != vv 340 newdata = vv 341 342 // ...but also recurse and call ModChecker with one level of pointer dereferencing, if not nil 343 if !rvv.IsNil() { 344 mod = mt.ModCheckAll( 345 rvv.Interface(), 346 ) || mod 347 } 348 349 goto handleData 350 } 351 352 panic(errors.New("type not implemented: " + reflect.TypeOf(v).String())) 353 354 } 355 handleData: 356 mt.cur[v] = mtResult{modified: mod, data: newdata} 357 ret = ret || mod 358 359 } 360 361 return ret 362 } 363 364 // ModCheck(oldData interface{}) (isModified bool, newData interface{}) 365 366 // hm, this may not work - what happens if we call ModCheck twice in a row?? we need a clear 367 // way to demark the "old" and "new" versions of data - it might be that the comparison and 368 // the gather of the new value need to be separate methods? Or can ModTracker somehow 369 // prevent ModCheck from being call twice in the same pass (or prevent that from being an issue) 370 371 // actually that might work - if there is a call on ModTracker that moves "new" to "old", it 372 // woudl be pretty clear - and then calling this would only update the "new" value. Also the 373 // presence of something in the "new" data would mean it's already been called in this pass 374 // and so can be deduplicated. 375 376 func hasTagPart(tagstr, part string) bool { 377 for _, p := range strings.Split(tagstr, ",") { 378 if p == part { 379 return true 380 } 381 } 382 return false 383 }