Documentation
¶
Overview ¶
Package fuzzfp watches the arguments a fuzz target hands to a builtin and reports the two mutations that no builtin may ever perform: rewriting a value's source location, and reaching inside an LNative to change the Go value a host handed in.
Why only those two ¶
A blanket "the arguments came back unchanged" assertion is not available here, because several callables mutate an argument BY DESIGN -- `append!` grows a vector in place, `assoc!` writes a sorted-map, and MacroCall writes to the nodes a macro returns. A guard that fired on those would be turned off within a week. So this package asserts only properties that hold for every callable in the library, and says so in one place rather than leaving each target to decide.
Source ¶
LVal.Source documents itself as shared: "Programs should not modify the contents of Source as the reference may be shared by multiple LVals." Every value the interpreter builds without a parser behind it points at ONE process-wide token.Location (lisp.nativeSource), so a builtin that writes through a Source pointer relocates a large fraction of every value alive in the process.
One write to Source is legitimate, and it is the reason the tracker (issue #318) recorded Source as unwatchable: stampMacroExpansion replaces a SYNTHETIC location with the macro call site. Its guard is exactly
if v.Source == nil || v.Source.Pos < 0 { v.Source = callSite }
so the legitimate transition is precisely "nil-or-synthetic pointer replaced by a real one".
That is worth stating as the interpreter's invariant rather than as a macro special case, because it is not one site. LEnv.errorSource (lisp/env.go) stamps an error's location under the SAME condition -- "All objects are given a source which may be a nativeSource() value which does not correspond to a file and has an invalid position (-1)", says the comment there -- and those are the only two places in lisp/ that write Source onto a value they did not just allocate. Neither writes over a real location.
Guard permits that transition and watches everything else:
- the pointer is unchanged, so the Location VALUE must be unchanged. This is the case that matters most, because it is the shared one: an in-place edit of lisp.nativeSource's Location is invisible to lisp.SingletonSnapshot (which compares Source by pointer) and would otherwise be reported by nothing at all.
- a REAL location (Pos >= 0) is never replaced, in any direction.
And nothing at all is asserted about what a nil-or-synthetic Source becomes. That is deliberately as permissive as the interpreter, not one step stricter: LEnv.errorSource assigns env.Loc, env.Loc is assigned from an evaluated node's Source, and a hand-built LVal may have a nil Source -- so a synthetic location legitimately becoming nil is reachable. A rule that reported it would fire on correct code at a rate low enough to look like an intermittent defect, and a gate that does that gets switched off. The cost is that a builtin which nils a synthetic Source is not reported; the two rules above are the ones with teeth.
LNative ¶
The tracker's stated reason for recording only an LNative's dynamic type was that formatting an arbitrary Go value can traverse a randomised map. That is true of fmt, and it is a property of fmt rather than of the values: a walk that sorts map entries by their own fingerprint is deterministic over exactly the same inputs. [fingerprintGo] is that walk. It reads unexported fields through reflect without ever calling Value.Interface (which would panic on them), so it sees inside time.Time and *regexp.Regexp, and it breaks cycles on pointer identity so a self-referential native terminates.
Deterministic here means "twice in one process gives the same answer", which is what a before/after comparison needs and is also the strong form: Go randomises map iteration per range statement per run, so two ranges in one process already disagree. TestFingerprintIsDeterministic exercises that directly, and TestGuardIsDeterministicOverGeneratedValues does it over the whole generated corpus.
What is deliberately NOT watched ¶
- Cells, Str, Int, Float, Quoted, Spliced. Legitimately mutated; see above.
- The Native of an LFun (an *LFunData, holding an *LEnv), an LError (a call stack) or an LBytes (a *[]byte). Only nodes whose Type is LNative have their Native fingerprinted, which is what keeps the walk away from an entire environment and from the deliberately-mutable byte slice.
- Meta. Only populated in format-preserving mode, which no builtin target runs in.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Fingerprint ¶
Fingerprint returns the structural fingerprint of a Go value. Exported for the determinism tests; the guard itself uses it internally.
Types ¶
type Guard ¶
type Guard struct {
// contains filtered or unexported fields
}
Guard is a set of watched nodes. Create one with Watch before the call and call Guard.Check after it.
func Watch ¶
Watch records every LVal reachable from v.
Nodes are recorded BY POINTER, not by position, so a builtin that legitimately reshapes the tree -- appends a cell, replaces one, sorts in place -- does not knock the before and after records out of alignment and produce a spurious report. A node that is dropped from the tree entirely is still watched: whoever dropped it may still be holding it.
func (*Guard) Check ¶
Check re-derives every watched property and reports the first violation, or "" when there is none.
One violation rather than all of them: a fuzz failure is read by a human looking for the smallest reproducer, and a builtin that relocates one node has almost always relocated a thousand.