Skip to content

Commit

Permalink
Merge branch 'refactor-str-seq' of https://github.com/arr-ai/arrai in…
Browse files Browse the repository at this point in the history
…to refactor-str-seq
  • Loading branch information
ericzhang6222 committed May 25, 2020
2 parents d1c3334 + 2416761 commit 1b14e39
Show file tree
Hide file tree
Showing 9 changed files with 279 additions and 43 deletions.
17 changes: 16 additions & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,22 @@ Arr.ai supports operations on numbers.
3. Function call:
1. `[2, 4, 6, 8](2) = 6`, `"hello"(1) = 101`
2. `{"red": 0.3, "green": 0.5, "blue", 0.2}("green") = 0.5`
4. Function slice:
4. Conditional Accessor Syntax: This feature allows failures in accessing a `Tuple` attribute
or a `Set` call and replacing it with a provided expression in case of failure. Any call or
attribute access that ends with `?` are allowed to fail.
1. `(a: 1).b?:42 = 42`
2. `(a: 1).a?:42 = 1`
3. `{"a": 1}("b")?:42 = 42`
4. `{"a": 1}("a")?:42 = 1`

It also allows appending access expressions.
1. `(a: {"b": (c: 2)}).a?("b").c?:42 = 2`
2. `(a: {"b": (c: 2)}).a?("b").d?:42 = 42`

Not all access failures are allowed. Only missing attributes of a `Tuple` or a `Set` call
does not return exactly 1 value.
1. `(a: (b: 1)).a?.b.c?:42` will fail as it will try to evaluate `1.c?:42`.
5. Function slice:
1. `[1, 1, 2, 3, 5, 8](2:5) = [2, 3, 5]`
2. `[1, 2, 3, 4, 5, 6](1:5:2) = [2, 4]`

Expand Down
12 changes: 12 additions & 0 deletions rel/expr_binary.go
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,18 @@ func NewCallExpr(scanner parser.Scanner, a, b Expr) Expr {
return newBinExpr(scanner, a, b, "call", "«%s»(%s)", Call)
}

func SafeCall(a, b Value, local Scope) (Value, error) {
if x, ok := a.(Set); ok {
return x.CallAll(b), nil
}
return nil, errors.Errorf(
"call lhs must be a function, not %T", a)
}

func NewSafeCallExpr(scanner parser.Scanner, a, b Expr) Expr {
return newBinExpr(scanner, a, b, safeCallOp, "«%s»(%s)?", SafeCall)
}

func NewCallExprCurry(scanner parser.Scanner, f Expr, args ...Expr) Expr {
for _, arg := range args {
f = NewCallExpr(scanner, f, arg)
Expand Down
30 changes: 29 additions & 1 deletion rel/expr_dot.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ import (
"github.com/go-errors/errors"
)

type missingAttrError struct {
ctxErr error
}

func (m missingAttrError) Error() string {
return m.ctxErr.Error()
}

// DotExpr returns the tuple or set with a single field replaced by an
// expression.
type DotExpr struct {
Expand Down Expand Up @@ -63,7 +71,7 @@ func (x *DotExpr) Eval(local Scope) (_ Value, err error) {
}
}
}
return nil, wrapContext(errors.Errorf("Missing attr %s", x.attr), x)
return nil, missingAttrError{wrapContext(errors.Errorf("Missing attr %s", x.attr), x)}
}

switch t := a.(type) {
Expand All @@ -88,3 +96,23 @@ func (x *DotExpr) Eval(local Scope) (_ Value, err error) {
"(%s).%s: lhs must be a Tuple, not %T", x.lhs, x.attr, a), x)
}
}

type SafeDotExpr struct {
d *DotExpr
}

func NewSafeDotExpr(scanner parser.Scanner, lhs Expr, attr string) Expr {
return &SafeDotExpr{NewDotExpr(scanner, lhs, attr).(*DotExpr)}
}

func (sd *SafeDotExpr) Eval(local Scope) (Value, error) {
return sd.d.Eval(local)
}

func (sd *SafeDotExpr) Source() parser.Scanner {
return sd.d.Src
}

func (sd *SafeDotExpr) String() string {
return sd.d.String() + "?"
}
76 changes: 76 additions & 0 deletions rel/expr_safe_tail.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package rel

import (
"fmt"

"github.com/arr-ai/wbnf/parser"
)

const safeCallOp = "safe_call"

type SafeTailExpr struct {
ExprScanner
fallbackValue, base Expr
tailExprs []func(Expr) Expr
}

func NewSafeTailExpr(
scanner parser.Scanner,
fallback, base Expr,
tailExprs []func(Expr) Expr,
) Expr {
if len(tailExprs) == 0 {
panic("exprs cannot be empty")
}
return &SafeTailExpr{ExprScanner{scanner}, fallback, base, tailExprs}
}

func (s *SafeTailExpr) Eval(local Scope) (value Value, err error) {
value, err = s.base.Eval(local)
if err != nil {
return nil, wrapContext(err, s)
}
for _, t := range s.tailExprs {
expr := t(value)
if call, isCall := expr.(*BinExpr); isCall && call.op == "safe_call" {
value, err = call.Eval(local)
if err != nil {
return nil, wrapContext(err, s)
}

for e, i := value.(Set).Enumerator(), 1; e.MoveNext(); i++ {
if i > 1 {
return s.fallbackValue.Eval(local)
}
}
if !value.IsTrue() {
return s.fallbackValue.Eval(local)
}
value = SetAny(value.(Set))
} else if safeDot, isSafeDot := expr.(*SafeDotExpr); isSafeDot {
value, err = safeDot.Eval(local)
if err != nil {
if _, isMissingAttr := err.(missingAttrError); isMissingAttr {
return s.fallbackValue.Eval(local)
}
return nil, wrapContext(err, s)
}
} else {
value, err = expr.Eval(local)
if err != nil {
return nil, wrapContext(err, s)
}
}
}
return value, err
}

func (s *SafeTailExpr) String() string {
finalExpr := s.tailExprs[0](s.base)
if len(s.tailExprs) > 1 {
for _, e := range s.tailExprs[1:] {
finalExpr = e(finalExpr)
}
}
return finalExpr.String() + fmt.Sprintf(":%s", s.fallbackValue.String())
}
4 changes: 4 additions & 0 deletions rel/value.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,10 @@ func SetCall(s Set, arg Value) Value {
return SetAny(result)
}

func SafeSetCall(s Set, arg Value) Value {
return s.CallAll(arg)
}

func SetAny(s Set) Value {
for e := s.Enumerator(); e.MoveNext(); {
return e.Current()
Expand Down
18 changes: 9 additions & 9 deletions syntax/arrai.wbnf
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,7 @@ expr -> C* amp="&"* @ C* arrow=(
> C* unop=/{:>|=>|>>|[-+!*^]}* @ C*
> C* @:binop=">>>" C*
> C* @ postfix=/{count|single}? C* touch? C*
> C* (get | @) tail=(
get
| call=("("
arg=(
expr (":" end=expr? (":" step=expr)?)?
| ":" end=expr (":" step=expr)?
):",",
")")
)* C*
> C* (get | @) tail_op=(safe_tail | tail)* C*
> %!patternterms(expr)
| C* cond=("cond" "(" (key=@ ":" value=@):",",? ("*" ":" f=expr ","?)? ")") C*
| C* "{:" C* embed=(grammar=@ ":" subgrammar=%%ast) ":}" C*
Expand All @@ -52,6 +44,14 @@ sexpr -> "${"
C* expr C*
control=/{ (?: : [-+#*\.\_0-9a-z]* (?: : (?: \\. | [^\\:}] )* ){0,2} )? }
close=/{\}\s*};
tail -> get
| call=("("
arg=(
expr (":" end=expr? (":" step=expr)?)?
| ":" end=expr (":" step=expr)?
):",",
")");
safe_tail -> first_safe=(tail "?") ops=(safe=(tail "?") | tail)* ":" fall=expr;
pattern -> extra | %!patternterms(pattern|expr) | IDENT | NUM;
extra -> ("..." ident=IDENT?);

Expand Down
102 changes: 79 additions & 23 deletions syntax/compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ func (pc ParseContext) CompileExpr(b ast.Branch) rel.Expr {
// Note: please make sure if it is necessary to add new syntax name before `expr`.
name, c := which(b,
"amp", "arrow", "let", "unop", "binop", "compare", "rbinop", "if", "get",
"tail", "postfix", "touch", "get", "rel", "set", "dict", "array",
"tail_op", "postfix", "touch", "get", "rel", "set", "dict", "array",
"embed", "op", "fn", "pkg", "tuple", "xstr", "IDENT", "STR", "NUM", "cond",
"expr",
)
Expand All @@ -99,7 +99,7 @@ func (pc ParseContext) CompileExpr(b ast.Branch) rel.Expr {
return pc.compileCond(b, c)
case "postfix", "touch":
return pc.compilePostfixAndTouch(b, c)
case "get", "tail":
case "get", "tail_op":
return pc.compileCallGet(b)
case "rel":
return pc.compileRelation(c)
Expand Down Expand Up @@ -432,41 +432,97 @@ func (pc ParseContext) compilePostfixAndTouch(b ast.Branch, c ast.Children) rel.

func (pc ParseContext) compileCallGet(b ast.Branch) rel.Expr {
var result rel.Expr

get := func(get ast.Node) {
if get != nil {
if ident := get.One("IDENT"); ident != nil {
scanner := ident.One("").(ast.Leaf).Scanner()
result = rel.NewDotExpr(scanner, result, scanner.String())
}
if str := get.One("STR"); str != nil {
s := str.One("").Scanner()
result = rel.NewDotExpr(s, result, parseArraiString(s.String()))
}
}
}

if expr := b.One("expr"); expr != nil {
result = pc.CompileExpr(expr.(ast.Branch))
} else {
result = rel.DotIdent
get(b.One("get"))
result = pc.compileGet(rel.DotIdent, b.One("get"), false)
}
for _, part := range b.Many("tail_op") {
if safe := part.One("safe_tail"); safe != nil {
result = pc.compileSafeTails(result, part.One("safe_tail"))
} else {
result = pc.compileTail(result, part.One("tail"), false)
}
}
return result
}

for _, part := range b.Many("tail") {
if call := part.One("call"); call != nil {
func (pc ParseContext) compileTail(base rel.Expr, tail ast.Node, safe bool) rel.Expr {
createExpr := rel.NewCallExpr
if safe {
createExpr = rel.NewSafeCallExpr
}
if tail != nil {
if call := tail.One("call"); call != nil {
args := call.Many("arg")
exprs := make([]ast.Node, 0, len(args))
for _, arg := range args {
exprs = append(exprs, arg.One("expr"))
}
for _, arg := range pc.compileExprs(exprs...) {
result = rel.NewCallExpr(call.Scanner(), result, arg)
base = createExpr(call.Scanner(), base, arg)
}
}
get(part.One("get"))
base = pc.compileGet(base, tail.One("get"), safe)
}
return result
return base
}

func (pc ParseContext) compileGet(base rel.Expr, get ast.Node, safe bool) rel.Expr {
createExpr := rel.NewDotExpr
if safe {
createExpr = rel.NewSafeDotExpr
}
if get != nil {
if ident := get.One("IDENT"); ident != nil {
scanner := ident.One("").(ast.Leaf).Scanner()
base = createExpr(scanner, base, scanner.String())
}
if str := get.One("STR"); str != nil {
s := str.One("").Scanner()
base = createExpr(s, base, parseArraiString(s.String()))
}
}
return base
}

func (pc ParseContext) compileSafeTails(base rel.Expr, tail ast.Node) rel.Expr {
if tail != nil {
firstSafe := tail.One("first_safe").One("tail")
exprStates := []func(rel.Expr) rel.Expr{
func(expr rel.Expr) rel.Expr {
return pc.compileTail(base, firstSafe, true)
},
}

fallback := pc.CompileExpr(tail.One("fall").(ast.Branch))

for _, o := range tail.Many("ops") {
if safeTail := o.One("safe"); safeTail != nil {
exprStates = append(exprStates,
func(expr rel.Expr) rel.Expr {
pctx := pc
safe := safeTail.One("tail")
return pctx.compileTail(expr, safe, true)
},
)
} else if tail := o.One("tail"); tail != nil {
exprStates = append(exprStates,
func(expr rel.Expr) rel.Expr {
pctx := pc
unsafeTail := tail
return pctx.compileTail(expr, unsafeTail, false)
},
)
} else {
panic("wat")
}
}

return rel.NewSafeTailExpr(tail.Scanner(), fallback, base, exprStates)
}
//TODO: panic?
return base
}

func (pc ParseContext) compileRelation(c ast.Children) rel.Expr {
Expand Down
45 changes: 45 additions & 0 deletions syntax/expr_safe_tail_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package syntax

import "testing"

func TestSafeTail(t *testing.T) {
t.Parallel()

AssertCodesEvalToSameValue(t, `1 `, `(a: 1).a?:42 `)
AssertCodesEvalToSameValue(t, `42`, `(a: 1).b?:42 `)
AssertCodesEvalToSameValue(t, `1 `, `{"a": 1}("a")?:42 `)
AssertCodesEvalToSameValue(t, `42`, `{"a": 1}("b")?:42 `)
AssertCodesEvalToSameValue(t, `1 `, `(a: (b: 1)).a?.b:42 `)
AssertCodesEvalToSameValue(t, `1 `, `{"a": {"b": 1}}("a")?("b"):42 `)
AssertCodesEvalToSameValue(t, `1 `, `let a = (b: (c: (d: (e: 1)))); a.b.c?.d.e?:42`)
AssertCodesEvalToSameValue(t, `1 `, `let a = (b: (c: (d: (e: 1)))); a.b.c?.d?.e:42`)
AssertCodesEvalToSameValue(t, `42`, `let a = (b: (c: (d: (e: 1)))); a.b.c?.d.f?:42`)
AssertCodesEvalToSameValue(t, `42`, `let a = (b: (c: (d: (e: 1)))); a.b.c?.f?.e:42`)
AssertCodesEvalToSameValue(t,
`1`,
`let a = {"b": {"c": {"d": {"e": 1}}}}; a("b")("c")?("d")("e")?:42 `)
AssertCodesEvalToSameValue(t,
`1`,
`let a = {"b": {"c": {"d": {"e": 1}}}}; a("b")("c")?("d")?("e"):42 `)
AssertCodesEvalToSameValue(t,
`42`,
`let a = {"b": {"c": {"d": {"e": 1}}}}; a("b")("c")?("d")("f")?:42 `)
AssertCodesEvalToSameValue(t,
`42`,
`let a = {"b": {"c": {"d": {"e": 1}}}}; a("b")("c")?("f")?("e"):42 `)
AssertCodesEvalToSameValue(t,
`1`,
`let a = {"b": (c: (d: {"e": 1}))}; a("b").c?.d("e")?:42 `)
AssertCodesEvalToSameValue(t,
`42`,
`let a = {"b": (c: (d: {"e": 1}))}; a("b").c?.d("f")?:42 `)
AssertCodesEvalToSameValue(t,
`42`,
`let a = {"b": (c: (d: {"e": 1}))}; a("b").c?.e?("f")?:42 `)

AssertCodeErrors(t, `(a: 1).a?.c:42 `, `(1).c: lhs must be a Tuple, not rel.Number`)
AssertCodeErrors(t, `(a: (b: 1)).a?.c:42 `, `Missing attr c`)
AssertCodeErrors(t,
`{"a": {"b": 1}}("a")?("c"):42`,
`unexpected panic: Call: no return values from set {b: 1}`)
}
Loading

0 comments on commit 1b14e39

Please sign in to comment.