Skip to content

Commit

Permalink
pkg/eval: Implement special command "with".
Browse files Browse the repository at this point in the history
This addresses #1114.
  • Loading branch information
xiaq committed Jul 24, 2024
1 parent 6b1a88f commit 9597a25
Show file tree
Hide file tree
Showing 6 changed files with 272 additions and 74 deletions.
2 changes: 2 additions & 0 deletions 0.21.0-release-notes.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Notable new features

- A new `with` command for running a lambda with temporary assignments.

- A new `keep-if` command.

- The `os` module has gained the following new commands: `mkdir-all`,
Expand Down
127 changes: 104 additions & 23 deletions pkg/eval/builtin_special.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,12 @@ func (err PluginLoadError) Unwrap() error { return err.err }
func init() {
// Needed to avoid initialization loop
builtinSpecials = map[string]compileBuiltin{
"var": compileVar,
"set": compileSet,
"tmp": compileTmp,
"del": compileDel,
"fn": compileFn,
"var": compileVar,
"set": compileSet,
"tmp": compileTmp,
"with": compileWith,
"del": compileDel,
"fn": compileFn,

"use": compileUse,

Expand All @@ -86,10 +87,9 @@ func init() {
}
}

// VarForm = 'var' { VariablePrimary } [ '=' { Compound } ]
// VarForm = 'var' { LHS } [ '=' { Compound } ]
func compileVar(cp *compiler, fn *parse.Form) effectOp {
lhsArgs, rhs := compileLHSRHS(cp, fn)
lhs := cp.parseCompoundLValues(lhsArgs, newLValue)
lhs, rhs := compileLHSOptionalRHS(cp, fn.Args, fn.To, newLValue)
if rhs == nil {
// Just create new variables, nothing extra to do at runtime.
return nopOp{}
Expand All @@ -99,7 +99,7 @@ func compileVar(cp *compiler, fn *parse.Form) effectOp {

// SetForm = 'set' { LHS } '=' { Compound }
func compileSet(cp *compiler, fn *parse.Form) effectOp {
lhs, rhs := compileSetArgs(cp, fn)
lhs, rhs := compileLHSRHS(cp, fn.Args, fn.To, setLValue)
return &assignOp{fn.Range(), lhs, rhs, false}
}

Expand All @@ -108,32 +108,113 @@ func compileTmp(cp *compiler, fn *parse.Form) effectOp {
if len(cp.scopes) <= 1 {
cp.errorpf(fn, "tmp may only be used inside a function")
}
lhs, rhs := compileSetArgs(cp, fn)
lhs, rhs := compileLHSRHS(cp, fn.Args, fn.To, setLValue)
return &assignOp{fn.Range(), lhs, rhs, true}
}

func compileSetArgs(cp *compiler, fn *parse.Form) (lvaluesGroup, valuesOp) {
lhsArgs, rhs := compileLHSRHS(cp, fn)
func compileWith(cp *compiler, fn *parse.Form) effectOp {
if len(fn.Args) < 2 {
cp.errorpf(fn, "with requires at least two arguments")
return nopOp{}
}
lastArg := fn.Args[len(fn.Args)-1]
bodyNode, ok := cmpd.Lambda(lastArg)
if !ok {
cp.errorpf(lastArg, "last argument must be a lambda")
return nopOp{}
}

assignNodes := fn.Args[:len(fn.Args)-1]
firstNode, ok := cmpd.Primary(assignNodes[0])
if !ok {
cp.errorpf(assignNodes[0], "argument must not be compound expressions")
return nopOp{}
}

var assigns []withAssign
if firstNode.Type == parse.List {
assigns = make([]withAssign, len(assignNodes))
for i, assignNode := range assignNodes {
p, ok := cmpd.Primary(assignNode)
if !ok {
cp.errorpf(assignNode, "argument must not be compound expressions")
continue
}
if p.Type != parse.List {
cp.errorpf(assignNode, "argument must be a list")
continue
}
lhs, rhs := compileLHSRHS(cp, p.Elements, p.To, setLValue)
assigns[i] = withAssign{assignNode.Range(), lhs, rhs}
}
} else {
lhs, rhs := compileLHSRHS(cp, assignNodes, assignNodes[len(assignNodes)-1].To, setLValue)
assigns = []withAssign{{diag.MixedRanging(assignNodes[0], assignNodes[len(assignNodes)-1]), lhs, rhs}}
}
return &withOp{fn.Range(), assigns, cp.primaryOp(bodyNode)}
}

type withOp struct {
diag.Ranging
assigns []withAssign
bodyOp valuesOp
}

type withAssign struct {
diag.Ranging
lhs lvaluesGroup
rhs valuesOp
}

func (op *withOp) exec(fm *Frame) (opExc Exception) {
var restoreFuncs []func(*Frame) Exception
defer func() {
for i := len(restoreFuncs) - 1; i >= 0; i-- {
exc := restoreFuncs[i](fm)
if exc != nil && opExc == nil {
opExc = exc
}
}
}()
for _, assign := range op.assigns {
exc := doAssign(fm, assign, assign.lhs, assign.rhs, func(f func(*Frame) Exception) {
restoreFuncs = append(restoreFuncs, f)
})
if exc != nil {
return exc
}
}
body := execLambdaOp(fm, op.bodyOp)
return fm.errorp(op, body.Call(fm.Fork("with body"), NoArgs, NoOpts))
}

// Finds LHS and RHS, compiling the RHS. Syntax:
//
// { LHS } '=' { Compound }
func compileLHSRHS(cp *compiler, args []*parse.Compound, end int, lf lvalueFlag) (lvaluesGroup, valuesOp) {
lhs, rhs := compileLHSOptionalRHS(cp, args, end, lf)
if rhs == nil {
cp.errorpf(diag.PointRanging(fn.Range().To), "need = and right-hand-side")
cp.errorpf(diag.PointRanging(end), "need = and right-hand-side")
}
lhs := cp.parseCompoundLValues(lhsArgs, setLValue)
return lhs, rhs
}

func compileLHSRHS(cp *compiler, fn *parse.Form) ([]*parse.Compound, valuesOp) {
for i, cn := range fn.Args {
// Finds LHS and optional RHS, compiling RHS if it exists. Syntax:
//
// { LHS } [ '=' { Compound } ]
func compileLHSOptionalRHS(cp *compiler, args []*parse.Compound, end int, lf lvalueFlag) (lvaluesGroup, valuesOp) {
for i, cn := range args {
if parse.SourceText(cn) == "=" {
lhs := fn.Args[:i]
if i == len(fn.Args)-1 {
return lhs, nopValuesOp{diag.PointRanging(fn.Range().To)}
lhs := cp.compileCompoundLValues(args[:i], lf)
if i == len(args)-1 {
return lhs, nopValuesOp{diag.PointRanging(end)}
}
return lhs, seqValuesOp{
diag.MixedRanging(fn.Args[i+1], fn.Args[len(fn.Args)-1]),
cp.compoundOps(fn.Args[i+1:])}
diag.MixedRanging(args[i+1], args[len(args)-1]),
cp.compoundOps(args[i+1:])}
}
}
return fn.Args, nil
return cp.compileCompoundLValues(args, lf), nil
}

const delArgMsg = "arguments to del must be variable or variable elements"
Expand Down Expand Up @@ -836,7 +917,7 @@ func (cp *compiler) compileOneLValue(n *parse.Compound, f lvalueFlag) lvalue {
if len(n.Indexings) != 1 {
cp.errorpf(n, "must be valid lvalue")
}
lvalues := cp.parseIndexingLValue(n.Indexings[0], f)
lvalues := cp.compileIndexingLValue(n.Indexings[0], f)
if lvalues.rest != -1 {
cp.errorpf(lvalues.lvalues[lvalues.rest], "rest variable not allowed")
}
Expand Down
74 changes: 74 additions & 0 deletions pkg/eval/builtin_special_test.elvts
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,80 @@ Exception: restore variable: bad var
[tty]:1:7-9: { tmp bad = foo; put after }
[tty]:1:1-28: { tmp bad = foo; put after }

////////
# with #
////////

~> var x = old
with x = new { put $x }
put $x
▶ new
▶ old

## multiple assignments enclosed in lists ##
~> var x y = old-x old-y
with [x = new-x] [y = new-y] { put $x $y }
put $x $y
▶ new-x
▶ new-y
▶ old-x
▶ old-y

## variables are restored if body throws exception ##
~> var x = old
with [x = new] { fail foo }
Exception: foo
[tty]:2:18-26: with [x = new] { fail foo }
~> put $x
▶ old

## exception setting variable restores previously set variables ##
//add-bad-var bad 0
~> var x = old
with [x = new] [bad = new] { }
Exception: bad var
[tty]:2:17-19: with [x = new] [bad = new] { }
~> put $x
▶ old

## exception restoring variable is propagated and doesn't affect restoring other variables ##
//add-bad-var bad 1
~> var x = old
with [x = new] [bad = new] { }
Exception: restore variable: bad var
[tty]:2:17-19: with [x = new] [bad = new] { }
~> put $x
▶ old

## two few arguments ##
~> with
Compilation error: with requires at least two arguments
[tty]:1:1-4: with
~> with { }
Compilation error: with requires at least two arguments
[tty]:1:1-8: with { }

## last argument not lambda ##
~> var x
with x = val foobar
Compilation error: last argument must be a lambda
[tty]:2:14-19: with x = val foobar

## compound expressions ##
~> with a'x' = foo { }
Compilation error: argument must not be compound expressions
[tty]:1:6-9: with a'x' = foo { }
~> var x
with [x = a] a'y' { }
Compilation error: argument must not be compound expressions
[tty]:2:14-17: with [x = a] a'y' { }

## list followed by non-list ##
~> var x
with [x = a] y { }
Compilation error: argument must be a list
[tty]:2:14-14: with [x = a] y { }

///////
# del #
///////
Expand Down
4 changes: 2 additions & 2 deletions pkg/eval/compile_effect.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ func (cp *compiler) formOp(n *parse.Form) effectOp {
}
assignmentOps = cp.assignmentOps(n.Assignments)
for _, a := range n.Assignments {
lvalues := cp.parseIndexingLValue(a.Left, setLValue|newLValue)
lvalues := cp.compileIndexingLValue(a.Left, setLValue|newLValue)
tempLValues = append(tempLValues, lvalues.lvalues...)
}
logger.Println("temporary assignment of", len(n.Assignments), "pairs")
Expand Down Expand Up @@ -403,7 +403,7 @@ func allTrue(vs []any) bool {
}

func (cp *compiler) assignmentOp(n *parse.Assignment) effectOp {
lhs := cp.parseIndexingLValue(n.Left, setLValue|newLValue)
lhs := cp.compileIndexingLValue(n.Left, setLValue|newLValue)
rhs := cp.compoundOp(n.Right)
return &assignOp{n.Range(), lhs, rhs, false}
}
Expand Down
Loading

0 comments on commit 9597a25

Please sign in to comment.