Skip to content

Introduce try/catch control structure #152

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jun 30, 2020
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
12 changes: 10 additions & 2 deletions constructors.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,14 +130,22 @@ func (t *Template) newYield(pos Pos, line int, name string, bplist *BlockParamet
return &YieldNode{NodeBase: NodeBase{TemplateName: t.Name, NodeType: NodeYield, Pos: pos, Line: line}, Name: name, Parameters: bplist, Expression: pipe, Content: content, IsContent: isContent}
}

func (t *Template) newInclude(pos Pos, line int, name, pipe Expression) *IncludeNode {
return &IncludeNode{NodeBase: NodeBase{TemplateName: t.Name, NodeType: NodeInclude, Pos: pos, Line: line}, Name: name, Expression: pipe}
func (t *Template) newInclude(pos Pos, line int, name, context Expression) *IncludeNode {
return &IncludeNode{NodeBase: NodeBase{TemplateName: t.Name, NodeType: NodeInclude, Pos: pos, Line: line}, Name: name, Context: context}
}

func (t *Template) newReturn(pos Pos, line int, pipe Expression) *ReturnNode {
return &ReturnNode{NodeBase: NodeBase{TemplateName: t.Name, NodeType: NodeReturn, Pos: pos, Line: line}, Value: pipe}
}

func (t *Template) newTry(pos Pos, line int, list *ListNode, catch *catchNode) *TryNode {
return &TryNode{NodeBase: NodeBase{TemplateName: t.Name, NodeType: NodeTry, Pos: pos, Line: line}, List: list, Catch: catch}
}

func (t *Template) newCatch(pos Pos, line int, errVar *IdentifierNode, list *ListNode) *catchNode {
return &catchNode{NodeBase: NodeBase{TemplateName: t.Name, NodeType: nodeCatch, Pos: pos, Line: line}, Err: errVar, List: list}
}

func (t *Template) newNumber(pos Pos, text string, typ itemType) (*NumberNode, error) {
n := &NumberNode{NodeBase: NodeBase{TemplateName: t.Name, NodeType: NodeNumber, Pos: pos}, Text: text}
// todo: optimize
Expand Down
148 changes: 101 additions & 47 deletions eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package jet

import (
"bytes"
"fmt"
"io"
"reflect"
Expand Down Expand Up @@ -130,78 +131,94 @@ func (st *scope) getBlock(name string) (block *BlockNode, has bool) {
return
}

// Set sets variable ${name} in the current template scope
func (state *Runtime) Set(name string, val interface{}) {
state.setValue(name, reflect.ValueOf(val))
func (state *Runtime) setValue(name string, val reflect.Value) error {
// try changing existing variable in current or parent scope
sc := state.scope
for sc != nil {
if _, ok := sc.variables[name]; ok {
sc.variables[name] = val
return nil
}
sc = sc.parent
}

return fmt.Errorf("could not assign %q = %v because variable %q is uninitialised", name, val, name)
}

func (state *Runtime) setValue(name string, val reflect.Value) bool {
// SetGlobal sets variable ${name} in the top-most template scope
func (state *Runtime) SetGlobal(name string, val interface{}) {
sc := state.scope
initial := sc

// try to resolve variables in the current scope
_, ok := sc.variables[name]

// if not found walks parent scopes
for !ok && sc.parent != nil {
// walk up to top-most valid scope
for sc.parent != nil && sc.parent.variables != nil {
sc = sc.parent
_, ok = sc.variables[name]
}

if ok {
sc.variables[name] = val
return false
}
sc.variables[name] = reflect.ValueOf(val)
}

for initial.variables == nil && initial.parent != nil {
initial = initial.parent
}
// Set sets the existing variable ${name} in the template scope it lives in.
func (state *Runtime) Set(name string, val interface{}) error {
return state.setValue(name, reflect.ValueOf(val))
}

if initial.variables != nil {
sc.variables[name] = val
return false
}
return true
// Let creates a variable ${name} in the current template scope (possibly shadowing an existing variable of the same name in a parent scope).
func (state *Runtime) Let(name string, val interface{}) {
state.scope.variables[name] = reflect.ValueOf(val)
}

// Resolve resolves a value from the execution context
func (state *Runtime) Resolve(name string) (reflect.Value, error) {
v, ok := reflect.Value{}, false
defer func() { v = indirectEface(v) }()
// SetOrLet calls Set() (if a variable with the given name is visible from the current scope) or Let() (if there is no variable with the given name in the current or any parent scope).
func (state *Runtime) SetOrLet(name string, val interface{}) {
_, err := state.resolve(name)
if err != nil {
state.Let(name, val)
} else {
state.Set(name, val)
}
}

// Resolve resolves a value from the execution context.
func (state *Runtime) resolve(name string) (reflect.Value, error) {
if name == "." {
return state.context, nil // indirectEface not needed
return state.context, nil
}

// try current, then parent variable scopes
sc := state.scope
for sc != nil {
v, ok = sc.variables[name]
v, ok := sc.variables[name]
if ok {
return v, nil
return indirectEface(v), nil
}
sc = sc.parent
}

// try globals
state.set.gmx.RLock()
v, ok = state.set.globals[name]
v, ok := state.set.globals[name]
state.set.gmx.RUnlock()
if ok {
return v, nil
return indirectEface(v), nil
}

// try default variables
v, ok = defaultVariables[name]
if ok {
return v, nil
return indirectEface(v), nil
}

return reflect.Value{}, fmt.Errorf("identifier '%q' not available in current (%+v) or parent scope, global, or default variables", name, state.scope.variables)
return reflect.Value{}, fmt.Errorf("identifier %q not available in current (%+v) or parent scope, global, or default variables", name, state.scope.variables)
}

// Resolve calls resolve() and ignores any errors, meaning it may return a zero reflect.Value.
func (state *Runtime) Resolve(name string) reflect.Value {
v, _ := state.resolve(name)
return v
}

// Resolve calls resolve() and panics if there is an error.
func (state *Runtime) MustResolve(name string) reflect.Value {
v, err := state.Resolve(name)
v, err := state.resolve(name)
if err != nil {
panic(err)
}
Expand All @@ -214,12 +231,12 @@ func (st *Runtime) recover(err *error) {
st.context = reflect.Value{}
pool_State.Put(st)
if recovered := recover(); recovered != nil {
var is bool
if _, is = recovered.(runtime.Error); is {
var ok bool
if _, ok = recovered.(runtime.Error); ok {
panic(recovered)
}
*err, is = recovered.(error)
if !is {
*err, ok = recovered.(error)
if !ok {
panic(recovered)
}
}
Expand All @@ -228,7 +245,10 @@ func (st *Runtime) recover(err *error) {
func (st *Runtime) executeSet(left Expression, right reflect.Value) {
typ := left.Type()
if typ == NodeIdentifier {
st.setValue(left.(*IdentifierNode).Ident, right)
err := st.setValue(left.(*IdentifierNode).Ident, right)
if err != nil {
left.error(err)
}
return
}
var value reflect.Value
Expand Down Expand Up @@ -481,6 +501,9 @@ func (st *Runtime) executeList(list *ListNode) (returnValue reflect.Value) {
if isLet {
st.releaseScope()
}
case NodeTry:
node := node.(*TryNode)
returnValue = st.executeTry(node)
case NodeYield:
node := node.(*YieldNode)
if node.IsContent {
Expand Down Expand Up @@ -513,6 +536,39 @@ func (st *Runtime) executeList(list *ListNode) (returnValue reflect.Value) {
return returnValue
}

func (st *Runtime) executeTry(try *TryNode) (returnValue reflect.Value) {
writer := st.Writer
buf := new(bytes.Buffer)

defer func() {
r := recover()

// copy buffered render output to writer only if no panic occured
if r == nil {
io.Copy(writer, buf)
} else {
// st.Writer is already set to its original value since the later defer ran first
if try.Catch != nil {
if try.Catch.Err != nil {
st.newScope()
st.scope.variables[try.Catch.Err.Ident] = reflect.ValueOf(r)
}
if try.Catch.List != nil {
returnValue = st.executeList(try.Catch.List)
}
if try.Catch.Err != nil {
st.releaseScope()
}
}
}
}()

st.Writer = buf
defer func() { st.Writer = writer }()

return st.executeList(try.List)
}

func (st *Runtime) executeInclude(node *IncludeNode) (returnValue reflect.Value) {
var templateName string
name := st.evalPrimaryExpressionGroup(node.Name)
Expand All @@ -536,10 +592,10 @@ func (st *Runtime) executeInclude(node *IncludeNode) (returnValue reflect.Value)
st.blocks = t.processedBlocks

var context reflect.Value
if node.Expression != nil {
if node.Context != nil {
context = st.context
defer func() { st.context = context }()
st.context = st.evalPrimaryExpressionGroup(node.Expression)
st.context = st.evalPrimaryExpressionGroup(node.Context)
}

Root := t.Root
Expand All @@ -548,9 +604,7 @@ func (st *Runtime) executeInclude(node *IncludeNode) (returnValue reflect.Value)
Root = t.Root
}

returnValue = st.executeList(Root)

return returnValue
return st.executeList(Root)
}

var (
Expand Down Expand Up @@ -662,7 +716,7 @@ func (st *Runtime) isSet(node Node) (ok bool) {
resolved, err := resolveIndex(base, index)
return err == nil && notNil(resolved)
case NodeIdentifier:
value, err := st.Resolve(node.String())
value, err := st.resolve(node.String())
return err == nil && notNil(value)
case NodeField:
node := node.(*FieldNode)
Expand Down Expand Up @@ -1023,7 +1077,7 @@ func (st *Runtime) evalBaseExpressionGroup(node Node) reflect.Value {
case NodeString:
return reflect.ValueOf(&node.(*StringNode).Text).Elem()
case NodeIdentifier:
resolved, err := st.Resolve(node.(*IdentifierNode).Ident)
resolved, err := st.resolve(node.(*IdentifierNode).Ident)
if err != nil {
node.error(err)
}
Expand Down
8 changes: 8 additions & 0 deletions eval_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -634,6 +634,14 @@ func TestExecReturn(t *testing.T) {
RunJetTestWithSet(t, set, nil, nil, "test_in_include", "", "from inside included template\n")
}

func TestTryCatch(t *testing.T) {
set := NewHTMLSet("./testData/tryCatch")
RunJetTestWithSet(t, set, nil, nil, "try", "", "before try without panic ...\n\nsome content\n\nfoo\n\nafter try without panic ...\nbefore panic ...\n\nafter panic ...")
RunJetTestWithSet(t, set, nil, nil, "try_catch", "", "before panic ...\n\nan error occured!\n\nafter panic ...")
RunJetTestWithSet(t, set, nil, nil, "try_catch_err", "", "before panic ...\n\nan error occured: Jet Runtime Error ("try_catch_err.jet":3): identifier "undefined_identifier_that_causes_panic" not available in current (map[]) or parent scope, global, or default variables\n\nafter panic ...")
RunJetTestWithSet(t, set, nil, nil, "try_include", "", "before broken include ...\n\nafter broken include ...")
}

func BenchmarkSimpleAction(b *testing.B) {
t, _ := JetTestingSet.GetTemplate("actionNode_dummy")
for i := 0; i < b.N; i++ {
Expand Down
40 changes: 24 additions & 16 deletions lex.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,20 +85,22 @@ const (
// Keywords appear after all the rest.
itemKeyword // used only to delimit the keywords
itemExtends
itemImport
itemInclude
itemBlock
itemEnd
itemYield
itemReturn
itemContent
itemInclude
itemElse
itemEnd
itemIf
itemNil
itemElse
itemRange
itemImport
itemTry
itemCatch
itemReturn
itemAnd
itemOr
itemNot
itemNil
itemMSG
itemTrans
)
Expand All @@ -109,22 +111,28 @@ var key = map[string]itemType{

"include": itemInclude,
"block": itemBlock,
"end": itemEnd,
"yield": itemYield,
"return": itemReturn,
"content": itemContent,

"else": itemElse,
"end": itemEnd,
"if": itemIf,
"else": itemElse,

"range": itemRange,
"nil": itemNil,
"and": itemAnd,
"or": itemOr,
"not": itemNot,

"content": itemContent,
"msg": itemMSG,
"trans": itemTrans,
"try": itemTry,
"catch": itemCatch,

"return": itemReturn,

"and": itemAnd,
"or": itemOr,
"not": itemNot,

"nil": itemNil,

"msg": itemMSG,
"trans": itemTrans,
}

const eof = -1
Expand Down
Loading