Skip to content

Commit e751d27

Browse files
committed
Enforce Error type in throw statements
Throw statements now require the thrown value to be of type Error or a subclass. Added a diagnostic message for invalid throw types, updated the abort implementation to throw Error when exception handling is enabled, and added tests for invalid throw types.
1 parent b42652b commit e751d27

File tree

9 files changed

+4031
-1843
lines changed

9 files changed

+4031
-1843
lines changed

src/compiler.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3024,6 +3024,19 @@ export class Compiler extends DiagnosticEmitter {
30243024
if (this.options.hasFeature(Feature.ExceptionHandling)) {
30253025
// Compile the thrown value - should be Error or subclass
30263026
let valueExpr = this.compileExpression(statement.value, Type.auto);
3027+
let valueType = this.currentType;
3028+
3029+
// Verify that the thrown type is Error or a subclass
3030+
let errorInstance = this.program.errorInstance;
3031+
let classReference = valueType.getClass();
3032+
if (!classReference || !classReference.isAssignableTo(errorInstance)) {
3033+
this.error(
3034+
DiagnosticCode.Only_Error_or_its_subclasses_can_be_thrown_but_found_type_0,
3035+
statement.value.range,
3036+
valueType.toString()
3037+
);
3038+
return module.unreachable();
3039+
}
30273040

30283041
// Ensure exception tag exists
30293042
let tagName = this.ensureExceptionTag();

src/diagnosticMessages.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"Initializer, definitive assignment or nullable type expected.": 238,
5353
"Definitive assignment has no effect on local variables.": 239,
5454
"Ambiguous operator overload '{0}' (conflicting overloads '{1}' and '{2}').": 240,
55+
"Only Error or its subclasses can be thrown, but found type '{0}'.": 241,
5556

5657
"Importing the table disables some indirect call optimizations.": 901,
5758
"Exporting the table disables some indirect call optimizations.": 902,

src/program.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -636,6 +636,14 @@ export class Program extends DiagnosticEmitter {
636636
}
637637
private _stringInstance: Class | null = null;
638638

639+
/** Gets the standard `Error` instance. */
640+
get errorInstance(): Class {
641+
let cached = this._errorInstance;
642+
if (!cached) this._errorInstance = cached = this.requireClass(CommonNames.Error);
643+
return cached;
644+
}
645+
private _errorInstance: Class | null = null;
646+
639647
/** Gets the standard `RegExp` instance. */
640648
get regexpInstance(): Class {
641649
let cached = this._regexpInstance;

std/assembly/builtins.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2603,13 +2603,32 @@ export abstract class i31 { // FIXME: usage of 'new' requires a class :(
26032603
// @ts-ignore: decorator
26042604
@external("env", "abort")
26052605
@external.js("throw Error(`${message} in ${fileName}:${lineNumber}:${columnNumber}`);")
2606-
declare function abort(
2606+
declare function __abort_impl(
26072607
message?: string | null,
26082608
fileName?: string | null,
26092609
lineNumber?: u32,
26102610
columnNumber?: u32
26112611
): void;
26122612

2613+
// When exception-handling is enabled, abort throws an Error that can be caught.
2614+
// When disabled, it calls the external abort function (host implementation).
2615+
function abort(
2616+
message: string | null = null,
2617+
fileName: string | null = null,
2618+
lineNumber: u32 = 0,
2619+
columnNumber: u32 = 0
2620+
): void {
2621+
if (isDefined(ASC_FEATURE_EXCEPTION_HANDLING)) {
2622+
let fullMessage = message ? message : "abort";
2623+
if (fileName) {
2624+
fullMessage += " in " + fileName + ":" + lineNumber.toString() + ":" + columnNumber.toString();
2625+
}
2626+
throw new Error(fullMessage);
2627+
} else {
2628+
__abort_impl(message, fileName, lineNumber, columnNumber);
2629+
}
2630+
}
2631+
26132632
// @ts-ignore: decorator
26142633
@external("env", "trace")
26152634
@external.js("console.log(message, ...[a0, a1, a2, a3, a4].slice(0, n));")

tests/compiler/exceptions.debug.wat

Lines changed: 1955 additions & 729 deletions
Large diffs are not rendered by default.

tests/compiler/exceptions.release.wat

Lines changed: 1942 additions & 1113 deletions
Large diffs are not rendered by default.

tests/compiler/exceptions.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -499,3 +499,80 @@ function testReturnInFinallySuppressesException(): i32 {
499499
}
500500
assert(testReturnInFinallySuppressesException() == 42);
501501
assert(finallyReturnSuppressedExceptionRan);
502+
503+
// ============================================================
504+
// Tests for catching abort() and runtime errors
505+
// ============================================================
506+
507+
// Test catching abort()
508+
function testCatchAbort(): bool {
509+
let caught = false;
510+
try {
511+
abort("this should be catchable");
512+
} catch (e) {
513+
caught = true;
514+
// Verify we got an Error with the abort message
515+
assert(e.message.includes("this should be catchable"));
516+
}
517+
return caught;
518+
}
519+
assert(testCatchAbort());
520+
521+
// Test catching runtime errors from __new (allocation too large)
522+
function testCatchRuntimeError(): bool {
523+
let caught = false;
524+
try {
525+
// Try to allocate an impossibly large object
526+
// This should trigger an allocation error in the runtime
527+
__new(usize.MAX_VALUE, idof<ArrayBuffer>());
528+
} catch (e) {
529+
caught = true;
530+
// Should have caught an allocation error
531+
assert(e.message.length > 0);
532+
}
533+
return caught;
534+
}
535+
assert(testCatchRuntimeError());
536+
537+
// Test that abort in a function can be caught by the caller
538+
function functionThatAborts(): void {
539+
abort("abort from function");
540+
}
541+
542+
function testCatchAbortFromFunction(): bool {
543+
let caught = false;
544+
try {
545+
functionThatAborts();
546+
} catch (e) {
547+
caught = true;
548+
assert(e.message.includes("abort from function"));
549+
}
550+
return caught;
551+
}
552+
assert(testCatchAbortFromFunction());
553+
554+
// Test catch variable is properly typed as Error
555+
function testCatchVariableType(): bool {
556+
try {
557+
throw new Error("type test");
558+
} catch (e) {
559+
// e should be typed as Error, so we can access message directly
560+
let msg: string = e.message;
561+
return msg == "type test";
562+
}
563+
return false;
564+
}
565+
assert(testCatchVariableType());
566+
567+
// Test catching custom Error subclass (use existing CustomError class)
568+
function testCatchCustomError2(): bool {
569+
try {
570+
throw new CustomError("custom error 2", 99);
571+
} catch (e) {
572+
// e is typed as Error, need to cast to access code
573+
let custom = e as CustomError;
574+
return custom.message == "custom error 2" && custom.code == 99;
575+
}
576+
return false;
577+
}
578+
assert(testCatchCustomError2());
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"asc_flags": [
3+
"--enable", "exception-handling"
4+
],
5+
"stderr": [
6+
"AS241: Only Error or its subclasses can be thrown, but found type '~lib/string/String'.",
7+
"EOF"
8+
]
9+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// Test that throwing non-Error types results in a compile error
2+
3+
// This should fail - throwing a string is not allowed
4+
throw "string error";
5+
6+
ERROR("EOF");

0 commit comments

Comments
 (0)