Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Error stack class/object #95

Open
zbeekman opened this issue Jan 6, 2020 · 6 comments
Open

Error stack class/object #95

zbeekman opened this issue Jan 6, 2020 · 6 comments
Labels
topic: utilities containers, strings, files, OS/environment integration, unit testing, assertions, logging, ...

Comments

@zbeekman
Copy link
Member

zbeekman commented Jan 6, 2020

I would like to implement an error stack class/object. This will be helpful for addressing #76 (message for errors inside of stdlib). The reasons why this is useful are that:

  1. You can raise/signal errors deep within the call stack, even in pure (or maybe elemental too, I'll have to think more carefully about that) procedures, and then handle them at a higher level
  2. You can maintain a call stack with the object and optionally include the file name, procedure name, and line number along with an error message and error type/severity
  3. IO for warnings, errors, statuses, etc. can be performed once outside performance critical and/or pure/elemental procedures
  4. You could hook up some logging mechanism too, so that the file IO happens on return from pure procedures

Perhaps this should be part of #72 (standard assert and macros) or the existing stdlib_experimental_error.f90 or perhaps it should be its own module.

I'm waiting, in part, for #69 (string handling routines) to get to a point where there's some consensus, to take a crack at implementing that before proposing an implementation for this. String handling will be important for the manipulations required.

Sketch interface prototype

pure subroutine push(this, file_name, line, procedure_name)
    !! caller calls this on line before call to callee
    !! `file_name` can be set to a compile-time constant via CMake definition
    !! or can use `__FILE__` macro, but this may overrun the line length
    !! `line` can be passed `__LINE__` macro.
    class(error_stack_t), intent(inout) :: this
    character(len=*), intent(in) :: file_name
    integer, intent(in) :: line
    character(len=*), intent(in), optional :: procedure_name
end subroutine push

I like to let CMake ad a per-source-file definition, something like THIS_FILE that holds the relative path to the file in the source directory. It could fallback to __FILE__ when using manual makefiles. The benefit over __FILE__ is two fold:

  1. Relative paths are shorter than absolute paths and less likely to overrun the line length limit, and CMake can check the length and insert a newline w/ continuation if needed
  2. Embedding absolute paths prevents builds from being reproducible and can make package managers thing that the software is not relocatable when it actually is.

Also, one or more arguments could be made optional. It would be wonderful if there was a way to query the name of the current scope in Fortran and if the programmer didn't need to make two separate calls, one to the callee and one push info onto the error stack. You could add file name, line number and error_stack arguments to all procedures and then the callee could push the data onto the stack as an alternative to the caller doing it.

Feedback, advice and suggestions welcome here.

pure subroutine pop(this)
    !! Called before/on return to caller
    !! only removes entries from the stack if no errors are signaling,
    !! otherwise preserve the call stack to the error, but decrement the depth
    !! so that when the error is caught the stack can be cleaned up appropriately
    class(error_stack_t), intent(inout) :: this
end subroutine pop
pure subroutine signal(this, message, severity, error_type) ! Or raise?
    !! Callee can signal or raise an exception and return control to caller
    !! (or further up the call stack) for handling
    class(error_stack_t), intent(inout) :: this
    character(len=*), intent(in) :: message
    integer, intent(in) :: severity
    integer, intent(in), optional :: error_type
end subroutine signal
subroutine catch(this, pop) ! maybe impure elemental?
    !! Do IO to tty or log as is necessary and handle calls to `error stop` 
    !! or other actions based on severity
    class(error_stack_t), intent(inout) :: this
    logical, optional :: pop !! pop stack up to this callers depth
end subroutine catch

The API needs some further thought, which is why I'd like feedback from others. In an ideal world, it would be awesome to have this act as a decorator and not have to call the push method before the caller calls the callee or not have to pass the file name and line number if the callee does the push.

Preprocessor macro expansion could automatically expand multiple actual arguments on calls to other stdlib procedure to pass in the stack object, file name and line number but seems a bit too magic for my liking.

@certik
Copy link
Member

certik commented Jan 6, 2020

Thanks for the proposal. Can you give an example how would this be used in practice? Does one have to call pop and push manually (or with a macro) in every function/subroutine in a project?

Shouldn't better debugging and stacktraces capability be rather built into compilers themselves?

@zbeekman
Copy link
Member Author

zbeekman commented Jan 6, 2020

Thanks for the proposal. Can you give an example how would this be used in practice?

I'll update the original post with an example

Does one have to call pop and push manually (or with a macro) in every function/subroutine in a project?

Unless someone can think of a better way to do this, then, "yes": Either the caller or callee must call pop and push starting at the depth where one wishes to catch and report errors and down into any procedures who will be raising/signalling errors.

Upon further consideration, the design with the lease typing that does not require an object oriented decorator is to pass 3 extra arguments to each procedure:

  1. The error_stack_t object
  2. The __LINE__ macro as an integer literal actual argument (this will hold the line the procedure was called on)
  3. The caller filename (either __FILE__ or a CMake defined relative path)

Then the procedure can call push and pop on entry/exit and provide its own name as an optional argument. I'm going to update my original post to reflect this.

For other classes & objects that take an OOD/OOP approach you may be able to implement a decorator pattern where the classes being implemented use the error_stack_t class, probably via composition. I need to think on this particular aspect a bit more and look at other examples, as I do not have experience with decorator patterns in Fortran. The hard part is getting the file name and the line number where the procedure was called. With pre-processor macros this needs to happen in the same file/scope that the call is made.

Shouldn't better debugging and stacktraces capability be rather built into compilers themselves?

Absolutely! But I'm tired of waiting 😄. I should open a new issue over in the other J3 repo. In particular the following would be helpful in letting users write their own error handling, if we cannot get error handling into the language itself:

  • file_name(): Function to return the name of a source code file (effectively standardize the __FILE__ macro without it being a macro)
  • line_number(): Function to return an integer of the line number of the file it was called
  • scope_name(): Function to return (even the mangled, namespaced) symbol definition. Or as an alternative you could have:
  • function_name(): Function to return the name of the current function
  • subroutine_name(): Function to return the name of the current subroutine

Or, better still for error handling:

  • called_from_file(): Function to return filename where the current procedure was called
  • called_on_line(): Function to return the line number where the current procedure was called

@certik
Copy link
Member

certik commented Jan 6, 2020

Shouldn't better debugging and stacktraces capability be rather built into compilers themselves?

Absolutely! But I'm tired of waiting

I am doing what I can. I could have been a lot further along, but I felt putting my time into stdlib and the J3 repo and getting us organized was worth getting a bit delayed.

In the meantime, here is how to get nice stacktraces from user code:

https://github.com/trilinos/Trilinos/blob/4ddae567dc74b55dfa2fb7eb27c04a1808ba2475/packages/teuchos/core/src/Teuchos_stacktrace.hpp
https://github.com/trilinos/Trilinos/blob/master/packages/teuchos/core/src/Teuchos_stacktrace.cpp

I implemented that 10 years ago. Since then, there seem to be a few other libraries that can do that:

I think they all follow basically the same approach.

I have not tested that with Fortran, only C and C++, but I think it would work.

@zbeekman
Copy link
Member Author

zbeekman commented Jan 6, 2020

I am doing what I can. I could have been a lot further along, but I felt putting my time into stdlib and the J3 repo and getting us organized was worth getting a bit delayed.

I completely agree! Thanks for the hard work!

@zbeekman
Copy link
Member Author

zbeekman commented Jan 6, 2020

I was editing my original post but lost the edits upon navigating to another link. I'll try to refine my ideas and add a usage example tonight.

@everythingfunctional
Copy link
Member

I have an implementation of something that does pretty much what you're asking for here. https://gitlab.com/everythingfunctional/erloff

It uses a functional programming approach, and has some conveniences for callers to deal with errors/messages that come back. It enables subroutines to remain pure as well.

Here are a couple projects I've made use of it as well:

Let me know if you have any questions about it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
topic: utilities containers, strings, files, OS/environment integration, unit testing, assertions, logging, ...
Projects
None yet
Development

No branches or pull requests

4 participants