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
2 changes: 1 addition & 1 deletion pkg/rt/generated.sums
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
# Content digest of all .lg + lgbgen sources that feed the .lgb
# bundle and the lowered Go tree. The genmanifest staleness test
# fails if this no longer matches the sources on disk.
0d12d62ce4179aafd6ab53cced499234576c4356b6106e1ac8ba79bc43a50b3a
4f9d9ff0405bc1290f5650975c260dd73d4a854e0210191102006333ff635d40
161 changes: 3 additions & 158 deletions pkg/rt/lang.go
Original file line number Diff line number Diff line change
Expand Up @@ -2422,163 +2422,6 @@ func installLangNS() {
return vec.(vm.Fn).Invoke([]vm.Value{v})
})

reduce := vm.NewCtxNativeFn("reduce", func(ec *vm.ExecContext, vs []vm.Value) (vm.Value, error) {
if len(vs) < 2 || len(vs) > 3 {
return vm.NIL, fmt.Errorf("wrong number of arguments %d", len(vs))
}
// 3-arg form with nil coll: return init regardless of fn
if len(vs) == 3 && vs[2] == vm.NIL {
return vs[1], nil
}
mfn, ok := vm.AsFn(vs[0])
if !ok {
return vm.NIL, fmt.Errorf("reduce expected Fn")
}
sidx := 1
if len(vs) == 3 {
sidx = 2
}
// Handle nil and empty collections
if vs[sidx] == vm.NIL {
if len(vs) == 3 {
return vs[1], nil
}
return ec.Invoke(mfn, nil)
}
// Check for empty collection first (skip for lazy/cons — RawCount forces realization)
switch vs[sidx].(type) {
case *vm.LazySeq, *vm.Cons:
// don't call RawCount — could be infinite
default:
if coll, ok := vs[sidx].(vm.Collection); ok {
if coll.RawCount() == 0 {
if len(vs) == 3 {
return vs[1], nil
}
return ec.Invoke(mfn, nil)
}
}
}
// ArrayVector fast path: flat slice with O(1) indexing — reduce by
// direct index with zero seq/chunk allocation instead of via seqOf.
if av, ok := vs[sidx].(vm.ArrayVector); ok {
var acc vm.Value
i := 0
if len(vs) == 3 {
acc = vs[1]
} else {
acc = av[0]
i = 1
}
fargs := []vm.Value{nil, nil}
for ; i < len(av); i++ {
fargs[0] = acc
fargs[1] = av[i]
res, err := ec.Invoke(mfn, fargs)
if err != nil {
return vm.NIL, err
}
if r, ok := res.(*vm.Reduced); ok {
return r.Deref(), nil
}
acc = res
}
return acc, nil
}
// Range fast path: a range is (start, end, step) arithmetic — reduce
// by direct iteration with zero seq/chunk allocation. Mirrors the
// ArrayVector fast path above.
if rng, ok := vs[sidx].(*vm.Range); ok {
start, end, step := rng.Bounds()
var acc vm.Value
i := start
if len(vs) == 3 {
acc = vs[1]
} else {
acc = vm.Int(i)
i += step
}
fargs := []vm.Value{nil, nil}
for (step > 0 && i < end) || (step < 0 && i > end) {
fargs[0] = acc
fargs[1] = vm.Int(i)
res, err := ec.Invoke(mfn, fargs)
if err != nil {
return vm.NIL, err
}
if r, ok := res.(*vm.Reduced); ok {
return r.Deref(), nil
}
acc = res
i += step
}
return acc, nil
}
seq, err := seqOf(vs[sidx])
if err != nil {
return vm.NIL, fmt.Errorf("reduce expected Seq")
}
// seqOf returns LazySeq objects without resolving them; an
// unresolved-empty LazySeq is non-nil but yields First()=NIL.
// Resolve here so empty inputs hit the early-return path
// instead of spuriously iterating once with a NIL element.
if ls, ok := seq.(*vm.LazySeq); ok {
seq = ls.Resolve()
}
if seq == nil {
if len(vs) == 3 {
return vs[1], nil
}
return ec.Invoke(mfn, nil)
}
var acc vm.Value
if len(vs) == 3 {
acc = vs[1]
} else {
acc = seq.First()
seq = seq.Next()
}
// Reused two-arg buffer (same pattern as `some`'s fargs): avoids a
// fresh []vm.Value per element — the single largest allocation site
// in seq-based reduce.
fargs := []vm.Value{nil, nil}
for seq != nil {
// Chunked fast path: when the source exposes a chunk, walk via
// Nth in a tight inner loop and advance one chunk at a time. This
// avoids the per-element LazySeq/Cons allocation churn that
// dominates plain Next()-based reduce on chunked sources.
if cs, ok := vm.AsChunkedSeq(seq); ok {
c := cs.ChunkedFirst()
n := c.ChunkCount()
for i := 0; i < n; i++ {
fargs[0] = acc
fargs[1] = c.Nth(i)
acc, err = ec.Invoke(mfn, fargs)
if err != nil {
return vm.NIL, err
}
if r, ok := acc.(*vm.Reduced); ok {
return r.Deref(), nil
}
}
seq = cs.ChunkedNext()
continue
}
fargs[0] = acc
fargs[1] = seq.First()
acc, err = ec.Invoke(mfn, fargs)
if err != nil {
return vm.NIL, err
}
if r, ok := acc.(*vm.Reduced); ok {
return r.Deref(), nil
}
seq = seq.Next()
}

return acc, nil
})

some := vm.NewCtxNativeFn("some", func(ec *vm.ExecContext, vs []vm.Value) (vm.Value, error) {
if len(vs) != 2 {
return vm.NIL, fmt.Errorf("wrong number of arguments %d", len(vs))
Expand Down Expand Up @@ -3946,7 +3789,9 @@ func installLangNS() {
ns.Def("mapv", mapv)
parMapV := vm.NewCtxNativeFn("pmapv", parallelMapV)
ns.Def("pmapv", parMapV)
ns.Def("reduce", reduce)
// reduce is registered by RegisterGeneratedPrimitives from the //lg:native
// Reduce/Reduce3 decls; a second hand-registration here would be silently
// overwritten by it (the generated registrar drains last).
ns.Def("some", some)

ns.Def("println", printlnf)
Expand Down
65 changes: 63 additions & 2 deletions pkg/rt/native_prims.go
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,63 @@ func reduceColl(ec *vm.ExecContext, mfn vm.Fn, coll vm.Value, hasInit bool, init
}
}
}
// Reused two-arg buffer, shared by every path below: ec.Invoke does not
// retain its argument slice, so one buffer per reduce beats one per
// element — the single largest allocation site in a reduce loop.
fargs := []vm.Value{nil, nil}
// ArrayVector fast path: flat slice with O(1) indexing — reduce by
// direct index with zero seq/chunk allocation instead of via seqOf.
if av, ok := coll.(vm.ArrayVector); ok {
var acc vm.Value
i := 0
if hasInit {
acc = initVal
} else {
acc = av[0]
i = 1
}
for ; i < len(av); i++ {
fargs[0] = acc
fargs[1] = av[i]
res, err := ec.Invoke(mfn, fargs)
if err != nil {
return vm.NIL, err
}
if r, ok := res.(*vm.Reduced); ok {
return r.Deref(), nil
}
acc = res
}
return acc, nil
}
// Range fast path: a range is (start, end, step) arithmetic — reduce
// by direct iteration with zero seq/chunk allocation. Mirrors the
// ArrayVector fast path above.
if rng, ok := coll.(*vm.Range); ok {
start, end, step := rng.Bounds()
var acc vm.Value
i := start
if hasInit {
acc = initVal
} else {
acc = vm.Int(i)
i += step
}
for (step > 0 && i < end) || (step < 0 && i > end) {
fargs[0] = acc
fargs[1] = vm.Int(i)
res, err := ec.Invoke(mfn, fargs)
if err != nil {
return vm.NIL, err
}
if r, ok := res.(*vm.Reduced); ok {
return r.Deref(), nil
}
acc = res
i += step
}
return acc, nil
}
seq, err := seqOf(coll)
if err != nil {
return vm.NIL, fmt.Errorf("reduce expected Seq")
Expand Down Expand Up @@ -415,7 +472,9 @@ func reduceColl(ec *vm.ExecContext, mfn vm.Fn, coll vm.Value, hasInit bool, init
c := cs.ChunkedFirst()
n := c.ChunkCount()
for i := 0; i < n; i++ {
acc, err = ec.Invoke(mfn, []vm.Value{acc, c.Nth(i)})
fargs[0] = acc
fargs[1] = c.Nth(i)
acc, err = ec.Invoke(mfn, fargs)
if err != nil {
return vm.NIL, err
}
Expand All @@ -426,7 +485,9 @@ func reduceColl(ec *vm.ExecContext, mfn vm.Fn, coll vm.Value, hasInit bool, init
seq = cs.ChunkedNext()
continue
}
acc, err = ec.Invoke(mfn, []vm.Value{acc, seq.First()})
fargs[0] = acc
fargs[1] = seq.First()
acc, err = ec.Invoke(mfn, fargs)
if err != nil {
return vm.NIL, err
}
Expand Down
52 changes: 52 additions & 0 deletions pkg/rt/native_prims_lifecycle.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ func setPrimitiveRoot(ns *vm.Namespace, name string, v vm.Value) *vm.Var {
// the binding so it can be reapplied after the namespace's source loads.
// Called only from the generated RegisterGeneratedPrimitives.
func defGeneratedPrimitive(ns *vm.Namespace, nsName, name string, v vm.Value) {
recordShadowedHandRegistration(ns, nsName, name)
// GuardRoot marks the adapter as the var's canonical root: lowered
// direct-call sites stay on the native fast path only while the root is
// untouched (vm.GuardedRootsIntact), so with-redefs/alter-var-root in a
Expand All @@ -151,6 +152,57 @@ func defGeneratedPrimitive(ns *vm.Namespace, nsName, name string, v vm.Value) {
genPrimMu.Unlock()
}

// Shadowed hand registrations.
//
// installLangNS Defs its closures first; the generated registrar drains last
// and takes the var root, so a name registered in BOTH places runs the
// generated //lg:native body and the hand-written closure is silently
// discarded. That is invisible when the two bodies agree and a regression when
// they don't: `reduce` had accumulated ArrayVector/Range fast paths only in the
// closure, so the day the generated registration started resolving (the ns
// alias fix in #639) reduce got 1.75x slower with every test still green.
// Recorded at initial registration so a test can ratchet the set.
var (
shadowedMu sync.Mutex
shadowedHandRegs = map[string]bool{} // "<requested-ns>/<name>"
)

// recordShadowedHandRegistration notes that a generated primitive is about to
// take a var some other registration already interned. It runs from
// defGeneratedPrimitive BEFORE the binding lands in genPrimBindings, so a name
// already present there was interned by a PREVIOUS generated registration, not
// by hand. Without that check any re-registration (a second registrar pass, a
// test binding the same ns/name twice in one process) records its own
// predecessor as a hand shadow, making the set invocation-count dependent.
func recordShadowedHandRegistration(ns *vm.Namespace, nsName, name string) {
if ns == nil || ns.LookupLocal(vm.Symbol(name)) == nil {
return
}
genPrimMu.RLock()
_, alreadyGenerated := genPrimBindings[resolveNSAlias(nsName)][name]
genPrimMu.RUnlock()
if alreadyGenerated {
return
}
shadowedMu.Lock()
shadowedHandRegs[nsName+"/"+name] = true
shadowedMu.Unlock()
}

// ShadowedHandRegistrations lists "<ns>/<name>" for every generated primitive
// that landed on top of an existing hand-written registration. Diagnostic:
// each entry is a duplicate implementation where only the generated one runs.
func ShadowedHandRegistrations() []string {
shadowedMu.Lock()
out := make([]string, 0, len(shadowedHandRegs))
for k := range shadowedHandRegs {
out = append(out, k)
}
shadowedMu.Unlock()
sort.Strings(out)
return out
}

// BindGeneratedPrimitive is the exported binding entry point for own-mode
// registrars generated into OTHER packages (which cannot call the unexported
// defGeneratedPrimitive). It resolves/creates the target namespace and binds
Expand Down
Loading
Loading