Skip to content

Conversation

@d-torrance
Copy link
Member

This is an alternative to #4047 that may address @mahrud's concerns. Rather than touching try, we add a new keyword attempt that returns a pair (val, err) (much like Go), where val is the value of the code and err is null if no error occurred, and otherwise val is null and err is an instance of a new Error type with information about the error that occurred .

i1 : attempt 5

o1 = (5, )

o1 : Sequence

i2 : attempt 1/0

o2 = (, Error{"message" => division by zero})
              "position" => stdio:2:8-2:11

o2 : Sequence

@mahrud
Copy link
Member

mahrud commented Dec 4, 2025

I really like this solution!

@seangrate
Copy link
Contributor

This seems like a good approach. Hopefully at some point, we can also build in some sort of error types so that we can check for particular errors by their types as opposed to their messages.

@d-torrance
Copy link
Member Author

That sounds like a good idea!

@pzinn
Copy link
Contributor

pzinn commented Dec 5, 2025

what I don't understand is, the output of attempt is a pair, but either the first or the second element of the pair is null? what's the point? one might as well test if output is an instance of Error

@pzinn
Copy link
Contributor

pzinn commented Dec 5, 2025

the location of the error is different than in the usual error message. is that intentional?

i1 : 1/0
stdio:1:1:(3): error: division by zero

i2 : attempt(1/0)

o2 = (, Error{"message" => division by zero})
              "position" => stdio:2:8-2:11

o2 : Sequence

(it may not be obvious, but in the example the first error point to / whereas the second points to 1/0)

@pzinn
Copy link
Contributor

pzinn commented Dec 5, 2025

This location doesn't seem right either:

i1 : attempt(1/a)

o1 = (, Error{"message" => no method for binary operator / applied to objects:})
                                       1 (of class ZZ)
                                 /     a (of class Symbol)
              "position" => ../../Macaulay2/m2/robust.m2:101:25-101:66

o1 : Sequence

@d-torrance
Copy link
Member Author

what I don't understand is, the output of attempt is a pair, but either the first or the second element of the pair is null? what's the point? one might as well test if output is an instance of Error

That's a good point. I was basically just copying the Go syntax that Mahrud pointed out in the other PR. Also, in an earlier draft, the second element was a string, not an Error object, so it was necessary then.

@d-torrance
Copy link
Member Author

As for the file positions, as I recall, errors originally all start out with dummyPosition and then get updated in various places, right? I think attempt is grabbing it a little later than it would have gotten printed if printing wasn't suppressed by tryEval, so maybe the position was updated a bit more in the meantime?

@mahrud
Copy link
Member

mahrud commented Dec 5, 2025

what I don't understand is, the output of attempt is a pair, but either the first or the second element of the pair is null? what's the point? one might as well test if output is an instance of Error

Strictly speaking, in Go the return value is always of the expected type, so attempt 1/0 should return (0, Error{...}) or something like that, but currently there's just no output. But even for us, because of type consistency this is more readable:

(inv, err) := attempt inverse n
if err =!= null then error("oops: ", err#"message")

than a situation where the return value has different types in each branch:

ret := attempt inverse n
if not instance(ret, Error) then inv := ret else error("oops: ", ret#"message")

Also, what if you're writing an error-handling related function that needs to return an error? You need to separate the legitimate output Error object from the error that may have occurred in the process :)

@pzinn
Copy link
Contributor

pzinn commented Dec 7, 2025

I stand by my original point.

Copy link
Contributor

@antonleykin antonleykin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The two ideas are good.
The second (proposed by @seangrate) to have an Error type.
The original idea to "capture message of error" (due to @d-torrance) via attempt (returning it, not storing, proposed by @mahrud).

Returning one thing---either "result" or Error---would be more natural, since our language is not statically typed (e.g., the type of the "result" is unknown anyway).

else (
when r
is err:Error do seq(nullE, toExpr(err))
else seq(r, nullE))); -- shouldn't happen
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shouldn't happen, unless the code fabricates it's own Error. I don't think we should let attempt output user-fabricated stuff.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. I've replaced this with an error.

attempt c
Description
Text
The code @VAR "c"@ is evaluated and a sequence containing two elements
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Emphasize that this is a keywork in the language, not a method. One could mentally parse (attempt 1)/0.
Perhaps give more examples: what would attempt 1/2; 1/0 give?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Documentation expanded

@d-torrance d-torrance force-pushed the attempt branch 2 times, most recently from 8be50f4 to 85964c6 Compare December 8, 2025 13:07
@d-torrance
Copy link
Member Author

@antonleykin - I think I've addressed your comments. attempt now returns a single element, either the value of the evaluated code or an Error object.

@mahrud
Copy link
Member

mahrud commented Dec 9, 2025

I would encourage you to reevaluate this decision. Here are a few dangers of this paradigm:

  1. If a helper function constructMyError legitimately returns an Error object (we have LOTS of these helpers in Core), it's impossible to detect from err := attempt constructMyError(...) if err is a legitimate output or if constructMyError itself failed. We need Core to be robust, so I wouldn't use attempt there with this implementation.
  2. Since error checking is less explicit, it's easy for users to forget to check for errors and only later end up with confusing type errors like dim myFunc() giving error: no method found for applying dim to: argument : Error {...} (of class Error)
  3. Even worse, since Error inherits from hash tables, a caller can accidentally treat the output as input to a function that accepts a hash table and consequently mask the error. Here is an explicit example of this:
f := M -> attempt apply(codim M, i -> i^2);
# f(ZZ^100)      -- gives #{}         which is 0
# f((ZZ[x])^100) -- gives #Error{...} which is 2

If you're going through the trouble of adding a new keyword, I'd like it to be robust and future proof, especially for MPI or RPC type applications.

@pzinn
Copy link
Contributor

pzinn commented Dec 9, 2025

I would encourage you to reevaluate this decision. Here are a few dangers of this paradigm:

  1. If a helper function constructMyError legitimately returns an Error object (we have LOTS of these helpers in Core), it's impossible to detect from err := attempt constructMyError(...) if err is a legitimate output or if constructMyError itself failed. We need Core to be robust, so I wouldn't use attempt there with this implementation.

this I don't understand at all. There seems to be a basic confusion here. These Error types are produced by the interpreter itself or at the level M2 level by calling error, not by creating an error manually. If we were to do what you suggest that would be a totally different usage which would require a separate discussion -- and a complete rewrite of this PR anyway.

  1. Even worse, since Error inherits from hash tables, a caller can accidentally treat the output as input to a function that accepts a hash table and consequently mask the error. Here is an explicit example of this:
f := M -> attempt apply(codim M, i -> i^2);
# f(ZZ^100)      -- gives #{}         which is 0
# f((ZZ[x])^100) -- gives #Error{...} which is 2

well, if you're going to write attempt, that means you're expecting an error, so you should test for it, otherwise you probably shouldn't use attempt in the first place.
That being said, I agree that Error probably shouldn't inherit from HashTable; instead, just like Dictionary, it should be its own type.

@mahrud
Copy link
Member

mahrud commented Dec 9, 2025

There seems to be a basic confusion here. These Error types are produced by the interpreter itself or at the level M2 level by calling error, not by creating an error manually. If we were to do what you suggest that would be a totally different usage which would require a separate discussion

Why shouldn't error objects be produced at the top-level? Is something preventing it in this proposal? There is a huge upside, as @seangrate suggested above, in allowing custom subtypes inheriting from Error so that error checking looks more like if instance(err, RingError) ... rather than if err#"message" == "incorrect ring" .... Otherwise why bother with a new type at all?

We currently don't have an Error type, but once it exists I guarantee this PR isn't going to be the end of it. Soon we'll have extensions like Python error types and Flint error types and use it with hooks and threads and loads more that we can't imagine now. (Side note: @d-torrance if err is an instance of Error then probably error err should produce an error with that location and message)

well, if you're going to write attempt, that means you're expecting an error, so you should test for it, otherwise you probably shouldn't use attempt in the first place.

Mistakes like this will sneak in, either by people treating attempt like try or just mistakes in the programming where there are multiple branches and you forget to check if the output is an error in one branch.

More generally, I'm just surprised that we're going with a syntax like this:

c := attempt codim M;
if instance(c, Error) then
if c#"message" == "codim: expected an affine ring (consider Generic=>true to work over QQ"
then c = planZZ(M)
else error "unexpected module"

@d-torrance
Copy link
Member Author

I'm convinced by @mahrud's arguments and prefer the Go-style two-element output.

@pzinn
Copy link
Contributor

pzinn commented Dec 9, 2025

Why shouldn't error objects be produced at the top-level? Is something preventing it in this proposal? There is a huge upside, as @seangrate suggested above, in allowing custom subtypes inheriting from Error so that error checking looks more like if instance(err, RingError) ... rather than if err#"message" == "incorrect ring" .... Otherwise why bother with a new type at all?

because that simply wouldn't work in the current setting. what would happen if you called your function that outputs an error your way without attempt? in fact, what's the role of attempt then? As I said, if you want to go this route, then we should scrap this PR and start from scratch with a completely different design.

There absolutely should be subtypes, but this is irrelevant to the point I'm making.
I'm simply suggesting Error be its own "root" type.

@d-torrance
Copy link
Member Author

I went back to the Go-style output and also added some basic support for subclassing of Error:

i1 : MyError = new SelfInitializingType of Error;

i2 : MyError "foo"

o2 = MyError{"message" => foo}

o2 : MyError

i3 : error o2
stdio:3:5:(3): error: foo

i4 : attempt error o2

o4 = (, MyError{"message" => foo                            })
                "position" => ../m2/debugging.m2:21:22-21:75

o4 : Sequence

@mahrud
Copy link
Member

mahrud commented Dec 10, 2025

Looks like the error

"position" => ../m2/debugging.m2:21:22-21:75

is pointing to this line:

error Error := err -> olderror new class err from processArgs err#"message"

No obligation to fix this in this PR, but obviously the location should point somewhere more appropriate. There are probably multiple ways to fix it (e.g. maybe moving error(Error) to the interpreter?), but one of my hopes from the attempt keyword is to fix these spurious error layers: #2568.

@pzinn
Copy link
Contributor

pzinn commented Dec 10, 2025

I understand less and less the logic. Now one can create Error objects at the M2 level, which are then fed to error, which at the d level proceeds to discard that Error object, only keeping track of the message, and creates a new Error object out of it... ??

@mahrud
Copy link
Member

mahrud commented Dec 10, 2025

The point of object oriented error handling is that instead of interrupting the computation, an error object is created to help the subsequent code resolve it and proceed without interruption. But there may be errors objects which can't be handled, so there should be a way to force an interrupt so that the user can intervene. This is where error(Error) comes in, to simplify this:

(c, err) := attempt codim M;
if instance(err, Error) then
if instance(err, RingError)
then c = planZZ M
else error("unexpected error: " | err#"message" | " at " | err#"position")

into this:

(c, err) := attempt codim M;
if instance(err, Error) then
if instance(err, RingError)
then c = planZZ M
else error(err) -- the original message and position are returned

Also bump to version 0.2 and start a changelog
Now takes a single input (the object to print) and leaves the
formatting of the message to the caller.

Also renamed to RobustPrintNetMethod so we can also have
RobustPrintSringMethod (which we'll use for arbitrary errors)
@mahrud
Copy link
Member

mahrud commented Dec 23, 2025

I presume this is just a rebase?

@d-torrance
Copy link
Member Author

Yup -- fixing the conflicct

@mahrud
Copy link
Member

mahrud commented Dec 24, 2025

@antonleykin @pzinn @mikestillman any objections to merging this for now, with the expectation that there may be future changes as Paul indicated? (And, of course, the possibility of reverting one or both new features before the release if we discover major problems)

@pzinn
Copy link
Contributor

pzinn commented Dec 25, 2025

fine with me

@mikestillman
Copy link
Member

I am looking at the final PR, and will review it too, and I will have comments (already do, but going back over the long discussion to make sure I don't re-ask too many questions. I think I like this version now, but I want to make sure it doesn't slow things down, and that the doc matches the actual behavior (e.g. doc mentions try code except code else code but this isn't allowed?)

@d-torrance
Copy link
Member Author

Oops -- try x except y else z was definitely a typo! I've fixed it, and also documented the try/then/except/do case that Mahrud added.

@mikestillman
Copy link
Member

@d-torrance Thanks! Perhaps we should also add to the "error handling" node "stopIfError" (a mutable variable), and a sentence about trap?

It seems like other things that I thought were a problem have somehow been fixed since I tried this yesterday? Not sure... (I needed to do new SelfInitializingType of Error rather than new Type of Error. Both seem to work now...)

@d-torrance
Copy link
Member Author

Sounds good! I've updated "error handling" as requested.

The only difference between SelfInializingType and Type, I think, is that the former lets you do MyError "foo" but the latter needs new MyError from "foo".

Mention "trap" in the description, and also add a link to stopIfError.
@mikestillman
Copy link
Member

It seems like we have 5 different forms for try blocks. Perhaps we should deprecate a couple versions, or give guidance about which should be used? Which ones are currently used? (That question is for myself, I'll take a look!)

I do like the trap function/keyword though!

@mikestillman
Copy link
Member

I found the problem: The extended markdown package (well, one of 3 different packages I just disabled) was grabbing shift-enter... Disabling them, and restarting makes it work.

@d-torrance
Copy link
Member Author

There are six forms:

try x
try x else z
try x then y
try x then y else z
try x except err do z
try x then y except err do z

They're all documented in this PR. I don't see a problem with having this many -- it doesn't hold a candle to all the different forms of for statements, for example!

@mahrud
Copy link
Member

mahrud commented Jan 6, 2026

I agree with Doug about different forms of try.

The extended markdown package

There's a markdown package?!

@mahrud
Copy link
Member

mahrud commented Jan 12, 2026

@mikestillman is this good to merge?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants