Skip to content

Structured exception handling

Wang Renxin edited this page May 17, 2017 · 15 revisions

The retro ON ERROR GOTO error handling works fine with line number based BASIC, but it's bad for a language with modern syntax. Error handling in MY-BASIC is simple, it just prompts an error message, then terminates execution flow. However it's possible to fork an isolated execution environment, with which code runs as if in a sub environment. A forked environment 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 an error occurred.

The mb_fork function is used to fork an environment from an exist one.

To implement a TRY statement for structured exception handling, add a 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);

		/* 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);
		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);
		}

		/* Close forked */
		mb_close_forked(&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 replace them; they are responsible for ensuring three of the 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:

  1. It invokes the first "try" invokable argument
  2. Invokes the second "catch" routine by passing the error text, if any error occurred in 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
  5. A TRY statement returns the value of the returned one from "try" routine

For example:

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

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

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

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