Skip to content

Structured exception handling

Wang Renxin edited this page May 31, 2022 · 15 revisions

The retro ON ERROR GOTO error handling works fine with line number based BASIC, but it's bad for a language with structured syntax. The ON ERROR RESUME NEXT is even worse, that covers up any error instead of exposing it, which is often supposed to be fixed, until the whole program cannot continue any more. Error handling in MY-BASIC is simple, it just prompts an error message, then terminates the execution flow. However it's possible to fork an isolated execution environment, with which code runs as if in an isolated context. A forked instance shares the same parsed AST, scope chain, GC context, etc. but separates some states such as error state. So we can check execution state without breaking the main execution flow when error occurs.

Use the mb_fork function to fork an instance from an existing one.

To implement a TRY statement for structured exception handling, add a C function first:

#define _mb_check_mark(__expr, __result, __exit) do { __result = (__expr); if(__result != MB_FUNC_OK) goto __exit; } while(0)

static int _try_catch_finally(struct mb_interpreter_t* s, void** l) {
	int result = MB_FUNC_OK;
	struct mb_interpreter_t* forked = 0;
	mb_value_t try_routine;
	mb_value_t catch_routine;
	mb_value_t finally_routine;
	mb_value_t ret;
	bool_t gc = false;

	mb_assert(s && l);

	mb_make_nil(try_routine);
	mb_make_nil(catch_routine);
	mb_make_nil(finally_routine);
	mb_make_nil(ret);

	/* Begin of reserving routines */
	begin_reserving(...);

	/* Get arguments */
	_mb_check_mark(mb_attempt_open_bracket(s, l), result, _exit);

	_mb_check_mark(mb_pop_value(s, l, &try_routine), result, _exit);
	if(mb_has_arg(s, l)) {
		_mb_check_mark(mb_pop_value(s, l, &catch_routine), result, _exit);
	}
	if(mb_has_arg(s, l)) {
		_mb_check_mark(mb_pop_value(s, l, &finally_routine), result, _exit);
	}

	_mb_check_mark(mb_attempt_close_bracket(s, l), result, _exit);

	do {
		void* ast = *l;
		mb_value_t args[1];
		mb_make_nil(args[0]);
		mb_error_e err = SE_NO_ERR;
		int ecode = MB_FUNC_OK;

		/* Fork an isolated environment */
		mb_fork(&forked, s, false);

		/* Evaluate try routine with forked */
		ecode = mb_eval_routine(forked, &ast, try_routine, args, 0, &ret);
		/* Evaluate catch routine if error occurred */
		err = mb_get_last_error(forked, 0, 0, 0, 0);
		if(err != SE_NO_ERR) {
			const char* errmsg = mb_get_error_desc(err);
			if(catch_routine.type == MB_DT_ROUTINE) {
				mb_value_t eargs[1];
				mb_make_string(eargs[0], (char*)errmsg);
				err = SE_NO_ERR;
				mb_eval_routine(s, l, catch_routine, eargs, 1, 0);
			}
		}
		/* Evaluate finally routine */
		if(finally_routine.type == MB_DT_ROUTINE) {
			mb_eval_routine(s, l, finally_routine, args, 0, 0);
		}
		/* Raise the error if it's not caught */
		if(err != SE_NO_ERR) {
			result = mb_raise_error(s, l, err, ecode);
		}

		/* Join forked */
		mb_join(&forked);
	} while(0);

_exit:
	/* End of reserving routines */
	end_reserving(...);

	/* Clean up routines */
	gc = mb_get_gc_enabled(s);
	mb_set_gc_enabled(s, false);
	if(try_routine.type == MB_DT_ROUTINE) {
		mb_check(mb_unref_value(s, l, try_routine));
	}
	if(catch_routine.type == MB_DT_ROUTINE) {
		mb_check(mb_unref_value(s, l, catch_routine));
	}
	if(finally_routine.type == MB_DT_ROUTINE) {
		mb_check(mb_unref_value(s, l, finally_routine));
	}
	mb_set_gc_enabled(s, gc);

	/* Return the value of the returned one from try routine */
	mb_check(mb_push_value(s, l, ret));

	return result;
}

Then register it:

mb_register_func(bas, "TRY", _try_catch_finally);

GC may be triggered when evaluating a routine within mb_eval_routine. Please be aware that begin_reserving and end_reserving are placeholder functions, there are different ways to implement them; they are responsible for ensuring all of the three routines will not be collected before evaluation done. A simple way is to use mb_set_gc_enabled to pause GC earlier, at where begin_reserving is. A properer way is to use mb_set_alive_checker to let the collector know that the routines are still alive; a specific implementation depends on how you've organized your program to use the global mb_set_gc_enabled.

The TRY statement accepts three arguments. And works as follow:

  1. It invokes the first "try" invokable argument
  2. Invokes the second "catch" routine by passing the error text, if any error occurred during the "try" routine
  3. The third "finally" routine is always invoked no matter error occurred or not
  4. It raises an occurred error outter if it's not caught yet by current TRY
  5. A TRY statement returns the value of the returned result from a "try" routine

For example:

ret = try
(
	lambda ()
	(
		print "Try.";

		return 42
	),
	lambda (_)
	(
		print "Catch: ", _, ".";
	),
	lambda ()
	(
		print "Finally.";
	)
)
print ret;

Read the "short identifier" section in Lambda abstraction to know how to add a short alias of the LAMBDA keyword.

The "catch" and "finally" routines are optional, it's a "try" only call as follow:

try
(
	lambda ()
	(
		print "Try.";
	)
)

And a test with error:

try
(
	lambda ()
	(
		print "Try.";

		raise(0)
	),
	lambda (_)
	(
		print "Catch: ", _, ".";
	),
	lambda ()
	(
		print "Finally.";
	)
)

It's also possible to use the mb_set_error_handler function to redirect error handler of a forked environment, otherwise it uses the same handler of the base environment.

Clone this wiki locally