diff --git a/docs/README.md b/docs/README.md index 3c520a58..de70a965 100644 --- a/docs/README.md +++ b/docs/README.md @@ -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]` diff --git a/rel/expr_binary.go b/rel/expr_binary.go index 25e4cace..78d8ac85 100644 --- a/rel/expr_binary.go +++ b/rel/expr_binary.go @@ -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) diff --git a/rel/expr_dot.go b/rel/expr_dot.go index c6743d02..f105bc9d 100644 --- a/rel/expr_dot.go +++ b/rel/expr_dot.go @@ -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 { @@ -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) { @@ -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() + "?" +} diff --git a/rel/expr_safe_tail.go b/rel/expr_safe_tail.go new file mode 100644 index 00000000..23e2a4e4 --- /dev/null +++ b/rel/expr_safe_tail.go @@ -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()) +} diff --git a/rel/value.go b/rel/value.go index 5dc48d2b..d3a6d42b 100644 --- a/rel/value.go +++ b/rel/value.go @@ -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() diff --git a/syntax/arrai.wbnf b/syntax/arrai.wbnf index e36a50a3..5231ace9 100644 --- a/syntax/arrai.wbnf +++ b/syntax/arrai.wbnf @@ -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* @@ -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?); diff --git a/syntax/compile.go b/syntax/compile.go index 5bbfe055..bb75bf31 100644 --- a/syntax/compile.go +++ b/syntax/compile.go @@ -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", ) @@ -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) @@ -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 { diff --git a/syntax/expr_safe_tail_test.go b/syntax/expr_safe_tail_test.go new file mode 100644 index 00000000..11a07ab8 --- /dev/null +++ b/syntax/expr_safe_tail_test.go @@ -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}`) +} diff --git a/syntax/parser.go b/syntax/parser.go index 406c4e59..3c1beb37 100644 --- a/syntax/parser.go +++ b/syntax/parser.go @@ -32,15 +32,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* @@ -66,6 +58,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?);