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.
6a4d922936d3d34c0802597362f4a1e71ecaa191d15bd5bbfcb62f713482f107
61a4a787d7fe05a7bd238d1724aeb998bfa11809c8c9412c0d8263fdc623104f
28 changes: 24 additions & 4 deletions pkg/rt/native_prims.go
Original file line number Diff line number Diff line change
Expand Up @@ -587,6 +587,14 @@ func Some(ec *vm.ExecContext, pred vm.Value, coll vm.Value) (vm.Value, error) {
if ls, ok := seq.(*vm.LazySeq); ok {
seq = ls.Resolve()
}
// Resolve a bytecode predicate once and reuse one frame for the whole
// walk: per-element ec.Invoke would pay resolution, frame-pool traffic,
// and frame init on every element. Non-bytecode/variadic predicates fall
// back to the generic invoke below.
pc := ec.PrepareCall(f, 1)
if pc != nil {
defer pc.Release()
}
fargs := []vm.Value{nil}
for seq != nil {
// Chunked fast path: walk whole chunks via Nth to avoid per-element
Expand All @@ -595,8 +603,14 @@ func Some(ec *vm.ExecContext, pred vm.Value, coll vm.Value) (vm.Value, error) {
c := cs.ChunkedFirst()
n := c.ChunkCount()
for i := 0; i < n; i++ {
fargs[0] = c.Nth(i)
v, err := ec.Invoke(f, fargs)
var v vm.Value
var err error
if pc != nil {
v, err = pc.Call1(c.Nth(i))
} else {
fargs[0] = c.Nth(i)
v, err = ec.Invoke(f, fargs)
}
if err != nil {
return vm.NIL, err
}
Expand All @@ -607,8 +621,14 @@ func Some(ec *vm.ExecContext, pred vm.Value, coll vm.Value) (vm.Value, error) {
seq = cs.ChunkedNext()
continue
}
fargs[0] = seq.First()
v, err := ec.Invoke(f, fargs)
var v vm.Value
var err error
if pc != nil {
v, err = pc.Call1(seq.First())
} else {
fargs[0] = seq.First()
v, err = ec.Invoke(f, fargs)
}
if err != nil {
return vm.NIL, err
}
Expand Down
73 changes: 73 additions & 0 deletions pkg/rt/some_prepared_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package rt

import (
"testing"

"github.com/nooga/let-go/pkg/vm"
)

// testGreaterThan5Pred builds a unary bytecode fn computing (< 5 x), so Some
// exercises the PreparedCall path with a real dispatch per element.
func testGreaterThan5Pred() *vm.Func {
consts := vm.NewConsts()
chunk := vm.NewCodeChunk(consts)
chunk.Append(vm.OP_LOAD_CONST)
chunk.Append32(consts.Intern(vm.Int(5)))
chunk.Append(vm.OP_LOAD_ARG)
chunk.Append32(0)
chunk.Append(vm.OP_LT)
chunk.Append(vm.OP_RETURN)
chunk.SetMaxStack(4)
return vm.MakeFunc(1, false, chunk)
}

func TestSomeBytecodePredOverChunkedAndLinearSeqs(t *testing.T) {
pred := testGreaterThan5Pred()

// Chunked source (Range) drives the chunk-walking arm.
v, err := Some(vm.RootExecContext, pred, vm.NewRange(vm.Int(0), vm.Int(10), vm.Int(1)))
if err != nil {
t.Fatal(err)
}
if v != vm.TRUE {
t.Fatalf("chunked walk: got %v", v)
}

// No match returns nil.
v, err = Some(vm.RootExecContext, pred, vm.NewRange(vm.Int(0), vm.Int(5), vm.Int(1)))
if err != nil {
t.Fatal(err)
}
if v != vm.NIL {
t.Fatalf("chunked no-match: got %v", v)
}

// Linear source (List) drives the First/Next arm.
list, err := vm.ListType.Box([]vm.Value{vm.Int(1), vm.Int(9), vm.Int(2)})
if err != nil {
t.Fatal(err)
}
v, err = Some(vm.RootExecContext, pred, list)
if err != nil {
t.Fatal(err)
}
if v != vm.TRUE {
t.Fatalf("linear walk: got %v", v)
}
}

func TestSomeNativePredStillFallsBack(t *testing.T) {
native, err := vm.NativeFnType.Wrap(func(args []vm.Value) (vm.Value, error) {
return vm.Boolean(args[0] == vm.Int(2)), nil
})
if err != nil {
t.Fatal(err)
}
v, err := Some(vm.RootExecContext, native, vm.NewRange(vm.Int(0), vm.Int(4), vm.Int(1)))
if err != nil {
t.Fatal(err)
}
if v != vm.TRUE {
t.Fatalf("native pred fallback: got %v", v)
}
}
98 changes: 98 additions & 0 deletions pkg/vm/prepared_call.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
* Copyright (c) 2021 Marcin Gasperowicz <xnooga@gmail.com>
* SPDX-License-Identifier: MIT
*/

package vm

// PreparedCall caches the resolution of a fixed-arity bytecode callable and
// reuses one frame across many calls from native code. It exists for
// per-element callback loops (reduce/some/filter and friends): resolution,
// frame-pool traffic, and frame initialization are per-call-site constants,
// so paying them once per seq operation instead of once per element removes
// most of the host->VM entry cost.
//
// A PreparedCall is single-owner: it must be used by one native call
// activation and never shared or called reentrantly. Nested uses of the same
// lg function each prepare their own.
type PreparedCall struct {
fn *Func
closedOvers []Value
ec *ExecContext
frame *Frame
args []Value
constsc int
stackLen int
}

// PrepareCall resolves fn for repeated arity-n invocation. It returns nil for
// targets that are not plain fixed-arity bytecode callables (variadic, native,
// protocol, ...), and for arities without a matching CallN method; callers
// fall back to ec.Invoke.
func (ec *ExecContext) PrepareCall(fn Fn, arity int) *PreparedCall {
// Only arities a CallN entry point can fully populate are prepared —
// Call1 is the only one today. Widen this as CallN methods land.
if arity != 1 {
return nil
}
args := make([]Value, arity)
target, direct, err := resolveBytecodeCall(fn, args)
if err != nil || !direct || target.fn.isVariadric {
return nil
}
ec = ec.orRoot()
f := NewFrame(target.fn.chunk, nil)
f.closedOvers = target.closedOvers
f.ec = ec
return &PreparedCall{
fn: target.fn,
closedOvers: target.closedOvers,
ec: ec,
frame: f,
args: args,
constsc: target.fn.chunk.consts.count(),
stackLen: max(target.fn.chunk.maxStack, 4),
}
}

// Call1 invokes the prepared unary callable.
func (p *PreparedCall) Call1(a Value) (Value, error) {
p.args[0] = a
return p.call()
}

// call resets the owned frame and runs it. The reset must cover code/consts/
// closedOvers, not just args/ip/sp: a tail call in the callee body rebinds the
// frame to the tail target via installBytecodeCall, and an error unwind can
// leave stale handlers.
func (p *PreparedCall) call() (Value, error) {
f := p.frame
f.args = p.args
f.argc = len(p.args)
f.closedOvers = p.closedOvers
f.code = p.fn.chunk
f.consts = f.code.consts
f.constsc = p.constsc
f.ip = 0
f.sp = 0
f.debug = false
f.parent = nil
f.stack = f.stack[:p.stackLen]
if len(f.handlers) > 0 {
f.handlers = f.handlers[:0]
}
state := frameRunState{root: f, current: f}
if allocAttrEnabled {
return runChainProtected(&state)
}
return runChain(&state)
}

// Release returns the owned frame to the pool. The PreparedCall must not be
// used afterwards.
func (p *PreparedCall) Release() {
if p.frame != nil {
ReleaseFrame(p.frame)
p.frame = nil
}
}
Loading
Loading