Move all variable creations to start of lexical scope.

Commit 8c71635ca3 moved the creation to the start
of pipelines; the approach works for simplistic cases like "x = 1 | nop", but
fails in more complex cases, such as "nop (x = 1) | nop".

The correct place to hoist variable creations is the lexical scope, and this
commit implements this approach.

This fixes #623.
This commit is contained in:
Qi Xiao 2020-04-26 19:04:49 +01:00
parent 3e9d5e9e60
commit c211679f0c
4 changed files with 52 additions and 30 deletions

View File

@ -16,6 +16,35 @@ import (
"github.com/xiaq/persistent/hashmap" "github.com/xiaq/persistent/hashmap"
) )
// An effectOpBody that creates all variables in a scope before executing the
// body.
type scopeOp struct {
inner effectOpBody
locals []string
}
func wrapScopeOp(op effectOp, locals []string) effectOp {
return effectOp{scopeOp{op.body, locals}, op.Ranging}
}
func (op scopeOp) invoke(fm *Frame) error {
for _, name := range op.locals {
var variable vars.Var
if strings.HasSuffix(name, FnSuffix) {
val := Callable(nil)
variable = vars.FromPtr(&val)
} else if strings.HasSuffix(name, NsSuffix) {
val := Ns(nil)
variable = vars.FromPtr(&val)
} else {
variable = vars.FromInit(nil)
}
fm.local[name] = variable
}
return op.inner.invoke(fm)
}
func (cp *compiler) chunkOp(n *parse.Chunk) effectOp { func (cp *compiler) chunkOp(n *parse.Chunk) effectOp {
return makeEffectOp(n, chunkOp{cp.pipelineOps(n.Pipelines)}) return makeEffectOp(n, chunkOp{cp.pipelineOps(n.Pipelines)})
} }
@ -41,16 +70,10 @@ func (op chunkOp) invoke(fm *Frame) error {
} }
func (cp *compiler) pipelineOp(n *parse.Pipeline) effectOp { func (cp *compiler) pipelineOp(n *parse.Pipeline) effectOp {
saveNewLocals := cp.newLocals
cp.newLocals = nil
formOps := cp.formOps(n.Forms) formOps := cp.formOps(n.Forms)
newLocals := cp.newLocals
cp.newLocals = saveNewLocals
return makeEffectOp(n, return makeEffectOp(n,
&pipelineOp{n.Background, parse.SourceText(n), formOps, newLocals}) &pipelineOp{n.Background, parse.SourceText(n), formOps})
} }
func (cp *compiler) pipelineOps(ns []*parse.Pipeline) []effectOp { func (cp *compiler) pipelineOps(ns []*parse.Pipeline) []effectOp {
@ -62,10 +85,9 @@ func (cp *compiler) pipelineOps(ns []*parse.Pipeline) []effectOp {
} }
type pipelineOp struct { type pipelineOp struct {
bg bool bg bool
source string source string
subops []effectOp subops []effectOp
newLocals []string
} }
const pipelineChanBufferSize = 32 const pipelineChanBufferSize = 32
@ -75,20 +97,6 @@ func (op *pipelineOp) invoke(fm *Frame) error {
return ErrInterrupted return ErrInterrupted
} }
for _, name := range op.newLocals {
var variable vars.Var
if strings.HasSuffix(name, FnSuffix) {
val := Callable(nil)
variable = vars.FromPtr(&val)
} else if strings.HasSuffix(name, NsSuffix) {
val := Ns(nil)
variable = vars.FromPtr(&val)
} else {
variable = vars.FromInit(nil)
}
fm.local[name] = variable
}
if op.bg { if op.bg {
fm = fm.fork("background job" + op.source) fm = fm.fork("background job" + op.source)
fm.intCh = nil fm.intCh = nil

View File

@ -108,6 +108,7 @@ func TestCompileEffect(t *testing.T) {
// Concurrently creating a new variable and accessing existing variable. // Concurrently creating a new variable and accessing existing variable.
// Run with "go test -race". // Run with "go test -race".
That("x = 1", "put $x | y = (all)").DoesNothing(), That("x = 1", "put $x | y = (all)").DoesNothing(),
That("nop (x = 1) | nop").DoesNothing(),
// Assignment errors when the RHS errors. // Assignment errors when the RHS errors.
That("x = [][1]").Throws(errWithType{errs.OutOfRange{}}, "[][1]"), That("x = [][1]").Throws(errWithType{errs.OutOfRange{}}, "[][1]"),

View File

@ -471,8 +471,10 @@ func (cp *compiler) lambda(n *parse.Primary) valuesOpBody {
for _, optName := range optNames { for _, optName := range optNames {
thisScope.set(optName) thisScope.set(optName)
} }
savedLocals := cp.pushNewLocals()
subop := cp.chunkOp(n.Chunk) chunkOp := cp.chunkOp(n.Chunk)
scopeOp := wrapScopeOp(chunkOp, cp.newLocals)
cp.newLocals = savedLocals
// XXX The fiddlings with cp.capture is error-prone. // XXX The fiddlings with cp.capture is error-prone.
capture := cp.capture capture := cp.capture
@ -483,7 +485,7 @@ func (cp *compiler) lambda(n *parse.Primary) valuesOpBody {
cp.registerVariableGet(name, nil) cp.registerVariableGet(name, nil)
} }
return &lambdaOp{argNames, restArgName, optNames, optDefaultOps, capture, subop, cp.srcMeta, n.Range().From, n.Range().To} return &lambdaOp{argNames, restArgName, optNames, optDefaultOps, capture, scopeOp, cp.srcMeta, n.Range().From, n.Range().To}
} }
type lambdaOp struct { type lambdaOp struct {

View File

@ -18,7 +18,7 @@ type compiler struct {
scopes []staticNs scopes []staticNs
// Variables captured from outer scopes. // Variables captured from outer scopes.
capture staticNs capture staticNs
// New variables created within a pipeline. // New variables created in a lexical scope.
newLocals []string newLocals []string
// Destination of warning messages. This is currently only used for // Destination of warning messages. This is currently only used for
// deprecation messages. // deprecation messages.
@ -45,7 +45,18 @@ func compile(b, g staticNs, tree parse.Tree, w io.Writer) (op Op, err error) {
panic(r) panic(r)
} }
}() }()
return Op{cp.chunkOp(tree.Root), tree.Source}, nil savedLocals := cp.pushNewLocals()
chunkOp := cp.chunkOp(tree.Root)
scopeOp := wrapScopeOp(chunkOp, cp.newLocals)
cp.newLocals = savedLocals
return Op{scopeOp, tree.Source}, nil
}
func (cp *compiler) pushNewLocals() []string {
saved := cp.newLocals
cp.newLocals = nil
return saved
} }
func (cp *compiler) errorpf(r diag.Ranger, format string, args ...interface{}) { func (cp *compiler) errorpf(r diag.Ranger, format string, args ...interface{}) {