Skip to content

Commit

Permalink
Merge pull request PaesslerAG#54 from PaesslerAG/sublanguage
Browse files Browse the repository at this point in the history
Sublanguage
  • Loading branch information
generikvault committed Dec 14, 2020
2 parents c11e94e + e3d61a8 commit bd491ef
Show file tree
Hide file tree
Showing 8 changed files with 303 additions and 21 deletions.
4 changes: 2 additions & 2 deletions evaluable.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,10 @@ func constant(value interface{}) Evaluable {
// slices and
// map with int or string key.
func (p *Parser) Var(path ...Evaluable) Evaluable {
if p.Language.selector == nil {
if p.selector == nil {
return variable(path)
}
return p.Language.selector(path)
return p.selector(path)
}

// Evaluables is a slice of Evaluable.
Expand Down
28 changes: 27 additions & 1 deletion example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,7 @@ func (s *exampleCustomSelector) SelectGVal(ctx context.Context, k string) (inter
return nil, nil
}

func ExampleCustomSelector() {
func ExampleSelector() {
lang := gval.Base()
value, err := lang.Evaluate(
"myStruct.hidden",
Expand All @@ -422,3 +422,29 @@ func ExampleCustomSelector() {
// Output:
// hello world
}

func parseSub(ctx context.Context, p *gval.Parser) (gval.Evaluable, error) {
return p.ParseSublanguage(ctx, subLang)
}

var (
superLang = gval.NewLanguage(
gval.PrefixExtension('$', parseSub),
)
subLang = gval.NewLanguage(
gval.Init(func(ctx context.Context, p *gval.Parser) (gval.Evaluable, error) { return p.Const("hello world"), nil }),
)
)

func ExampleParser_ParseSublanguage() {
value, err := superLang.Evaluate("$", nil)

if err != nil {
fmt.Println(err)
}

fmt.Println(value)

// Output:
// hello world
}
132 changes: 132 additions & 0 deletions gval_noparameter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"fmt"
"testing"
"text/scanner"
)

func TestNoParameter(t *testing.T) {
Expand Down Expand Up @@ -675,6 +676,137 @@ func TestNoParameter(t *testing.T) {
}),
want: "§4",
},
{
name: "Tabs as non-whitespace",
expression: "4\t5\t6",
extension: NewLanguage(
Init(func(ctx context.Context, p *Parser) (Evaluable, error) {
p.SetWhitespace('\n', '\r', ' ')
return p.ParseExpression(ctx)
}),
InfixNumberOperator("\t", func(a, b float64) (interface{}, error) {
return a * b, nil
}),
),
want: 120.0,
},
{
name: "Handle all other prefixes",
expression: "^foo + $bar + &baz",
extension: DefaultExtension(func(ctx context.Context, p *Parser) (Evaluable, error) {
var mul int
switch p.TokenText() {
case "^":
mul = 1
case "$":
mul = 2
case "&":
mul = 3
}

switch p.Scan() {
case scanner.Ident:
return p.Const(mul * len(p.TokenText())), nil
default:
return nil, p.Expected("length multiplier", scanner.Ident)
}
}),
want: 18.0,
},
{
name: "Embed languages",
expression: "left { 5 + 5 } right",
extension: func() Language {
step := func(ctx context.Context, p *Parser, cur Evaluable) (Evaluable, error) {
next, err := p.ParseExpression(ctx)
if err != nil {
return nil, err
}

return func(ctx context.Context, parameter interface{}) (interface{}, error) {
us, err := cur.EvalString(ctx, parameter)
if err != nil {
return nil, err
}

them, err := next.EvalString(ctx, parameter)
if err != nil {
return nil, err
}

return us + them, nil
}, nil
}

return NewLanguage(
Init(func(ctx context.Context, p *Parser) (Evaluable, error) {
p.SetWhitespace()
p.SetMode(0)

return p.ParseExpression(ctx)
}),
DefaultExtension(func(ctx context.Context, p *Parser) (Evaluable, error) {
return step(ctx, p, p.Const(p.TokenText()))
}),
PrefixExtension(scanner.EOF, func(ctx context.Context, p *Parser) (Evaluable, error) {
return p.Const(""), nil
}),
PrefixExtension('{', func(ctx context.Context, p *Parser) (Evaluable, error) {
eval, err := p.ParseSublanguage(ctx, Full())
if err != nil {
return nil, err
}

switch p.Scan() {
case '}':
default:
return nil, p.Expected("embedded", '}')
}

return step(ctx, p, eval)
}),
)
}(),
want: "left 10 right",
},
{
name: "Late binding",
expression: "5 * [ 10 * { 20 / [ 10 ] } ]",
extension: func() Language {
var inner, outer Language

parseCurly := func(ctx context.Context, p *Parser) (Evaluable, error) {
eval, err := p.ParseSublanguage(ctx, outer)
if err != nil {
return nil, err
}

if p.Scan() != '}' {
return nil, p.Expected("end", '}')
}

return eval, nil
}

parseSquare := func(ctx context.Context, p *Parser) (Evaluable, error) {
eval, err := p.ParseSublanguage(ctx, inner)
if err != nil {
return nil, err
}

if p.Scan() != ']' {
return nil, p.Expected("end", ']')
}

return eval, nil
}

inner = Full(PrefixExtension('{', parseCurly))
outer = Full(PrefixExtension('[', parseSquare))
return outer
}(),
want: 100.0,
},
},
t,
)
Expand Down
37 changes: 32 additions & 5 deletions language.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ import (

// Language is an expression language
type Language struct {
prefixes map[interface{}]prefix
prefixes map[interface{}]extension
operators map[string]operator
operatorSymbols map[rune]struct{}
init extension
def extension
selector func(Evaluables) Evaluable
}

Expand All @@ -29,6 +31,12 @@ func NewLanguage(bases ...Language) Language {
for i := range base.operatorSymbols {
l.operatorSymbols[i] = struct{}{}
}
if base.init != nil {
l.init = base.init
}
if base.def != nil {
l.def = base.def
}
if base.selector != nil {
l.selector = base.selector
}
Expand All @@ -38,7 +46,7 @@ func NewLanguage(bases ...Language) Language {

func newLanguage() Language {
return Language{
prefixes: map[interface{}]prefix{},
prefixes: map[interface{}]extension{},
operators: map[string]operator{},
operatorSymbols: map[rune]struct{}{},
}
Expand All @@ -48,16 +56,15 @@ func newLanguage() Language {
func (l Language) NewEvaluable(expression string) (Evaluable, error) {
p := newParser(expression, l)

eval, err := p.ParseExpression(context.Background())

eval, err := p.parse(context.Background())
if err == nil && p.isCamouflaged() && p.lastScan != scanner.EOF {
err = p.camouflage
}

if err != nil {
pos := p.scanner.Pos()
return nil, fmt.Errorf("parsing error: %s - %d:%d %s", p.scanner.Position, pos.Line, pos.Column, err)
}

return eval, nil
}

Expand Down Expand Up @@ -116,6 +123,26 @@ func PrefixExtension(r rune, ext func(context.Context, *Parser) (Evaluable, erro
return l
}

// Init is a language that does no parsing, but invokes the given function when
// parsing starts. It is incumbent upon the function to call ParseExpression to
// continue parsing.
//
// This function can be used to customize the parser settings, such as
// whitespace or ident behavior.
func Init(ext func(context.Context, *Parser) (Evaluable, error)) Language {
l := newLanguage()
l.init = ext
return l
}

// DefaultExtension is a language that runs the given function if no other
// prefix matches.
func DefaultExtension(ext func(context.Context, *Parser) (Evaluable, error)) Language {
l := newLanguage()
l.def = ext
return l
}

// PrefixMetaPrefix chooses a Prefix to be executed
func PrefixMetaPrefix(r rune, ext func(context.Context, *Parser) (call string, alternative func() (Evaluable, error), err error)) Language {
l := newLanguage()
Expand Down
12 changes: 6 additions & 6 deletions operator.go
Original file line number Diff line number Diff line change
Expand Up @@ -255,19 +255,19 @@ type infix struct {
func (op infix) merge(op2 operator) operator {
switch op2 := op2.(type) {
case *infix:
if op2.number != nil {
if op.number == nil {
op.number = op2.number
}
if op2.boolean != nil {
if op.boolean == nil {
op.boolean = op2.boolean
}
if op2.text != nil {
if op.text == nil {
op.text = op2.text
}
if op2.arbitrary != nil {
if op.arbitrary == nil {
op.arbitrary = op2.arbitrary
}
if op2.shortCircuit != nil {
if op.shortCircuit == nil {
op.shortCircuit = op2.shortCircuit
}
}
Expand All @@ -293,7 +293,7 @@ func (op directInfix) merge(op2 operator) operator {
return op
}

type prefix func(context.Context, *Parser) (Evaluable, error)
type extension func(context.Context, *Parser) (Evaluable, error)

type postfix struct {
operatorPrecedence
Expand Down
35 changes: 35 additions & 0 deletions parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,46 @@ func (p *Parser) ParseNextExpression(c context.Context) (eval Evaluable, err err
scan := p.Scan()
ex, ok := p.prefixes[scan]
if !ok {
if scan != scanner.EOF && p.def != nil {
return p.def(c, p)
}
return nil, p.Expected("extensions")
}
return ex(c, p)
}

// ParseSublanguage sets the next language for this parser to parse and calls
// its initialization function, usually ParseExpression.
func (p *Parser) ParseSublanguage(c context.Context, l Language) (Evaluable, error) {
if p.isCamouflaged() {
panic("can not ParseSublanguage() on camouflaged Parser")
}
curLang := p.Language
curWhitespace := p.scanner.Whitespace
curMode := p.scanner.Mode
curIsIdentRune := p.scanner.IsIdentRune

p.Language = l
p.resetScannerProperties()

defer func() {
p.Language = curLang
p.scanner.Whitespace = curWhitespace
p.scanner.Mode = curMode
p.scanner.IsIdentRune = curIsIdentRune
}()

return p.parse(c)
}

func (p *Parser) parse(c context.Context) (Evaluable, error) {
if p.init != nil {
return p.init(c, p)
}

return p.ParseExpression(c)
}

func parseString(c context.Context, p *Parser) (Evaluable, error) {
s, err := strconv.Unquote(p.TokenText())
if err != nil {
Expand Down
Loading

0 comments on commit bd491ef

Please sign in to comment.