mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-05 04:27:09 +00:00
feat(verifier): named() and cross-extractor read guard in goja
This commit is contained in:
1 parent
fe86e91aff
commit
973ada4983
2 files changed
+54
-10
No files matched your search
@@ -10,8 +10,10 @@ import (
|
|||||||
|
|
||||||
type extractorState struct {
|
type extractorState struct {
|
||||||
getter goja.Callable
|
getter goja.Callable
|
||||||
handle *goja.Object
|
|
||||||
name string
|
name string
|
||||||
|
// currentValue/previousValue back the handle's current/previous accessors.
|
||||||
|
currentValue goja.Value
|
||||||
|
previousValue goja.Value
|
||||||
// prev/curr cache the JSON-encoded extractor values from the prior and
|
// prev/curr cache the JSON-encoded extractor values from the prior and
|
||||||
// current PushSnapshot, used by ChangedExtractors to surface per-step
|
// current PushSnapshot, used by ChangedExtractors to surface per-step
|
||||||
// diffs in the trace.
|
// diffs in the trace.
|
||||||
@@ -170,12 +172,41 @@ func (v *Verifier) bindExtract(call goja.FunctionCall) goja.Value {
|
|||||||
name = fmt.Sprintf("extractor_%d", len(v.extractors))
|
name = fmt.Sprintf("extractor_%d", len(v.extractors))
|
||||||
}
|
}
|
||||||
|
|
||||||
handle := v.runtime.NewObject()
|
state := &extractorState{
|
||||||
_ = handle.Set("current", goja.Undefined())
|
getter: getter,
|
||||||
_ = handle.Set("previous", goja.Undefined())
|
name: name,
|
||||||
|
currentValue: goja.Undefined(),
|
||||||
|
previousValue: goja.Undefined(),
|
||||||
|
}
|
||||||
|
|
||||||
v.extractors = append(v.extractors, &extractorState{getter: getter, handle: handle, name: name})
|
handle := v.runtime.NewObject()
|
||||||
|
_ = handle.DefineAccessorProperty("current", v.runtime.ToValue(func(goja.FunctionCall) goja.Value {
|
||||||
|
v.checkNotExtracting("current")
|
||||||
|
return state.currentValue
|
||||||
|
}), nil, goja.FLAG_FALSE, goja.FLAG_TRUE)
|
||||||
|
_ = handle.DefineAccessorProperty("previous", v.runtime.ToValue(func(goja.FunctionCall) goja.Value {
|
||||||
|
v.checkNotExtracting("previous")
|
||||||
|
return state.previousValue
|
||||||
|
}), nil, goja.FLAG_FALSE, goja.FLAG_TRUE)
|
||||||
|
_ = handle.Set("named", func(call goja.FunctionCall) goja.Value {
|
||||||
|
state.name = call.Argument(0).String()
|
||||||
return handle
|
return handle
|
||||||
|
})
|
||||||
|
|
||||||
|
v.extractors = append(v.extractors, state)
|
||||||
|
return handle
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkNotExtracting panics with a JS error when an extractor getter tries to
|
||||||
|
// read another extractor handle's current/previous. The message is identical to
|
||||||
|
// the web runtime's so authors see one diagnostic across engines.
|
||||||
|
func (v *Verifier) checkNotExtracting(slot string) {
|
||||||
|
if v.extracting {
|
||||||
|
panic(v.runtime.NewGoError(fmt.Errorf(
|
||||||
|
"reading .%s of an extractor inside another extractor is not allowed; extractor getters may read only from the state argument",
|
||||||
|
slot,
|
||||||
|
)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// bindAlways accepts either a predicate function (legacy shape) or a formula
|
// bindAlways accepts either a predicate function (legacy shape) or a formula
|
||||||
|
|||||||
@@ -49,6 +49,11 @@ type Verifier struct {
|
|||||||
// order, so the runner can surface them in the run report.
|
// order, so the runner can surface them in the run report.
|
||||||
unsupported []string
|
unsupported []string
|
||||||
unsupportedSeen map[string]bool
|
unsupportedSeen map[string]bool
|
||||||
|
|
||||||
|
// extracting is true only while an extractor getter is running. The handle's
|
||||||
|
// current/previous accessors consult it so a getter that reaches into
|
||||||
|
// another extractor's handle throws instead of reading a stale value.
|
||||||
|
extracting bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnsupportedVerbs returns the verbs the picker requested that this platform
|
// UnsupportedVerbs returns the verbs the picker requested that this platform
|
||||||
@@ -276,19 +281,27 @@ func (v *Verifier) PushSnapshot(input SnapshotInput) error {
|
|||||||
// Predicate thunks read these slots but never trigger advancement, so
|
// Predicate thunks read these slots but never trigger advancement, so
|
||||||
// invoking a thunk multiple times between snapshots is value-stable.
|
// invoking a thunk multiple times between snapshots is value-stable.
|
||||||
for index, extractor := range v.extractors {
|
for index, extractor := range v.extractors {
|
||||||
previous := extractor.handle.Get("current")
|
extractor.previousValue = extractor.currentValue
|
||||||
_ = extractor.handle.Set("previous", previous)
|
newValue, err := v.runExtractor(extractor, state)
|
||||||
newValue, err := extractor.getter(goja.Undefined(), state)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("extractor %d: %w", index, err)
|
return fmt.Errorf("extractor %d: %w", index, err)
|
||||||
}
|
}
|
||||||
_ = extractor.handle.Set("current", newValue)
|
extractor.currentValue = newValue
|
||||||
extractor.prev = extractor.curr
|
extractor.prev = extractor.curr
|
||||||
extractor.curr = encodeExtractorValue(newValue)
|
extractor.curr = encodeExtractorValue(newValue)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// runExtractor invokes an extractor's getter with the extracting flag set, so a
|
||||||
|
// getter that reads another extractor's current/previous throws. The flag is
|
||||||
|
// cleared even if the getter panics.
|
||||||
|
func (v *Verifier) runExtractor(extractor *extractorState, state goja.Value) (goja.Value, error) {
|
||||||
|
v.extracting = true
|
||||||
|
defer func() { v.extracting = false }()
|
||||||
|
return extractor.getter(goja.Undefined(), state)
|
||||||
|
}
|
||||||
|
|
||||||
// encodeExtractorValue produces a stable JSON encoding of an extractor's
|
// encodeExtractorValue produces a stable JSON encoding of an extractor's
|
||||||
// current value for diff comparison. goja values that don't survive Export
|
// current value for diff comparison. goja values that don't survive Export
|
||||||
// (e.g. wrapped host functions) yield nil; callers treat nil as "unknown" and
|
// (e.g. wrapped host functions) yield nil; callers treat nil as "unknown" and
|
||||||
@@ -358,7 +371,7 @@ func (v *Verifier) OverrideExtractorValues(overrides map[int]json.RawMessage) (s
|
|||||||
if conversionErr != nil {
|
if conversionErr != nil {
|
||||||
return skipped, fmt.Errorf("extractor override %d: %w", index, conversionErr)
|
return skipped, fmt.Errorf("extractor override %d: %w", index, conversionErr)
|
||||||
}
|
}
|
||||||
_ = v.extractors[index].handle.Set("current", value)
|
v.extractors[index].currentValue = value
|
||||||
}
|
}
|
||||||
return skipped, nil
|
return skipped, nil
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user