ivy icon indicating copy to clipboard operation
ivy copied to clipboard

ivy: some way to recover from panics?

Open rsc opened this issue 4 months ago • 3 comments

Food for thought. Not suggesting a specific change.

I was using Ivy last night to understand some other math code. I wanted to reduce a homogenous linear equation problem Mx = 0, where M has rho N (N+1) and x has rho N, to a heterogeneous one Ax=B, where A has rho N N and x and B have rho N, and then solve with mdiv.

The way to do this seems to be trial and error: force an output to 1, derive an A, B from M assuming the chosen variable is 1, and if Ax=B can't be solved with mdiv, try other outputs until you find one that works. The only problem is that if Ax=B can't be solved with mdiv, ivy panics and aborts the entire computation, so I can't try something else (at least in a program; by hand I can).

Because I wanted to focus on solving my actual problem, I added the form 'x else y' to Ivy which means evaluate x, or if that panics then evaluate to y. That let me write:

op col x = ((rho x), 1) rho x

op x solve m =
	n = 1 drop rho m
	A = -m[;x]
	B = m[;(x!=iota n) sel iota n]
	print 'Solve' x 'A' (col A) 'B' B
	((x == iota n) + (x != iota n) fill A mdiv B) else (x+1) solve m

op solve m = 1 solve m

solve mix (1 1 1 1 -1) (1 2 4 8 -2) (1 3 9 27 0) (1 4 16 64 -4)

solve mix (1 1 1 -1 -1) (1 2 4 -2 -4) (1 3 9 0 0) (1 4 16 -4 -16)

This seems to work, at least for those cases:

(Solve) 1 A (-1| B ( 1  1  1 -1|
            |-1|   | 2  4  8 -2|
            |-1|   | 3  9 27  0|
            |-1)   | 4 16 64 -4)

1 -11/6 7/8 -1/8 -1/12

(Solve) 1 A (-1| B (  1   1  -1  -1|
            |-1|   |  2   4  -2  -4|
            |-1|   |  3   9   0   0|
            |-1)   |  4  16  -4 -16)
(Solve) 2 A (-1| B (  1   1  -1  -1|
            |-2|   |  1   4  -2  -4|
            |-3|   |  1   9   0   0|
            |-4)   |  1  16  -4 -16)

0 1 -1/3 1 -1/3

I don't particularly like 'x else y' - maybe I should have used 'x recover y' - and I'm not sure it makes a ton of sense with respect to right-to-left evaluation. Maybe it should be 'y recover x' or 'y ifnot x'. Probably it shouldn't be done at all.

Perhaps mdiv is the only operation where it is difficult to predict whether it will panic. That could be fixed by adding a determinant operator or maybe defining mdiv on underdetermined problems.

But perhaps there are other good uses for recovering from a failed computation. It seemed at least worth writing down.

% git diff .
diff --git a/parse/function.go b/parse/function.go
index 5c1a957..6d936a9 100644
--- a/parse/function.go
+++ b/parse/function.go
@@ -264,6 +264,9 @@ func walk(expr value.Expr, assign bool, f func(value.Expr, bool)) {
 	case *value.BinaryExpr:
 		walk(e.Right, false, f)
 		walk(e.Left, e.Op == "=", f)
+	case *value.ElseExpr:
+		walk(e.Right, false, f)
+		walk(e.Left, false, f)
 	case *value.IndexExpr:
 		for i := len(e.Right) - 1; i >= 0; i-- {
 			x := e.Right[i]
diff --git a/parse/parse.go b/parse/parse.go
index 827b126..0698977 100644
--- a/parse/parse.go
+++ b/parse/parse.go
@@ -266,6 +266,13 @@ func (p *Parser) expr() value.Expr {
 	case scan.EOF, scan.RightParen, scan.RightBrack, scan.Semicolon, scan.Colon:
 		return expr
 	case scan.Identifier:
+		if tok.Text == "else" {
+			p.next()
+			return &value.ElseExpr{
+				Left:  expr,
+				Right: p.expr(),
+			}
+		}
 		if p.context.DefinedBinary(tok.Text) {
 			p.next()
 			return &value.BinaryExpr{
@@ -470,7 +477,7 @@ func (p *Parser) numberOrVector(tok scan.Token) value.Expr {
 			case scan.LeftParen:
 				fallthrough
 			case scan.Identifier:
-				if p.context.DefinedOp(tok.Text) {
+				if p.context.DefinedOp(tok.Text) || tok.Text == "else" {
 					break Loop
 				}
 				fallthrough
diff --git a/value/expr.go b/value/expr.go
index 153436c..403456d 100644
--- a/value/expr.go
+++ b/value/expr.go
@@ -77,6 +77,35 @@ func (c *CondExpr) Operands() (left, right Expr) {
 	return c.Cond.Left, c.Cond.Right
 }
 
+// ElseExpr is an ElseExpr executor: expression "else" expression
+type ElseExpr struct {
+	Left  Expr
+	Right Expr
+}
+
+func (x *ElseExpr) ProgString() string {
+	return (&BinaryExpr{Left: x.Left, Op: "else", Right: x.Right}).ProgString()
+}
+
+func (x *ElseExpr) Eval(context Context) (ret Value) {
+	defer func() {
+		if recover() != nil {
+			ret = x.Right.Eval(context)
+		}
+	}()
+	return x.Left.Eval(context)
+}
+
+var _ = Decomposable(&ElseExpr{})
+
+func (x *ElseExpr) Operator() string {
+	return "else"
+}
+
+func (x *ElseExpr) Operands() (left, right Expr) {
+	return x.Left, x.Right
+}
+
 // VectorExpr holds a syntactic vector to be verified and evaluated.
 type VectorExpr []Expr
 
% 

rsc avatar Oct 10 '25 16:10 rsc

Leaving aside your other questions, the right-to-left evaluation doesn't feel right here because we always evaluate the failure value. It would be nice if we could avoid that, but I don't see how right now, at least not without a control structure of some kind.

If I could buckle down and succeed at last in getting operators to be first class values, perhaps this could be cleaner if we could could use a functor, kinda like recover.

There's also the Sawzall approach, at least under option, of eliding (or perhaps substituting) failed computations. That might work well.

I might create a list of the places failure can happen and see if the issue is more general.

robpike avatar Oct 12 '25 07:10 robpike

The diff does make it a real control structure rather than a binary operator: 'x else y' does not evaluate y unless x panics. But the code handling variable ordering and what local variables are in scope may be wrong since when the construct does run x and y, y happens only after (some of) x.

On the other hand 'x: y' has the same left-to-right evaluation so maybe that can still be made to work.

rsc avatar Oct 12 '25 18:10 rsc

I'm starting to think that a 'recover' model might a better plan. The expression

recover value

(the spelling is not important now) in an op could tuck away value, or its expression tree for later evaluation, and then a "panic" would cause the op to return value instead. It could return 0 when executed, value when triggered, if that matters for the pathological op containing only a recover expression.

For me this has the advantage that the unusual expression would stand out, as one would usually see it as the first line of the op, signaling possible trouble ahead, and that it would not need to decorate or be evaluated at the failure location itself.

robpike avatar Oct 12 '25 20:10 robpike