1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
//! HARN-TYP-026 — the `throws E` declared-exception-channel check.
//!
//! Deterministic, in-process typechecker assertions on the diagnostic CODE
//! (never prose/timing): a callable whose `throw` sites all conform to its
//! declared `throws` set checks clean; a `throw` of a type the set does not
//! cover raises TYP-026; and a callable with no `throws` clause is never
//! constrained (the annotation is additive). The final group covers
//! catch-exhaustiveness: a `try`/`catch` subtracts the errors it handles from
//! the declared channel and adds those its catch/finally bodies raise, so a
//! caught error does not count and an uncaught one does — matching the VM's
//! type-selective (enum-only) `catch` dispatch.
use super::*;
use crate::diagnostic_codes::Code;
fn throws_mismatches(source: &str) -> Vec<String> {
check_source(source)
.into_iter()
.filter(|d| d.code == Code::ThrowsTypeMismatch)
.map(|d| d.message)
.collect()
}
#[test]
fn throwing_the_declared_type_checks_clean() {
let diags = throws_mismatches(
r#"fn parse(s: string) throws string {
throw "bad input"
}"#,
);
assert!(
diags.is_empty(),
"throwing the declared type must not raise TYP-026: {diags:?}"
);
}
#[test]
fn throwing_an_undeclared_type_errors() {
let diags = throws_mismatches(
r"fn parse(s: string) throws string {
throw 42
}",
);
assert_eq!(
diags.len(),
1,
"throwing a type outside the declared set must raise exactly one TYP-026: {diags:?}"
);
}
#[test]
fn union_throws_set_covers_each_member() {
let diags = throws_mismatches(
r#"fn parse(s: string) throws string | int {
if s == "" {
throw 42
}
throw "bad"
}"#,
);
assert!(
diags.is_empty(),
"each member of a union throws set must be allowed: {diags:?}"
);
}
#[test]
fn missing_throws_clause_is_unconstrained() {
// No `throws` clause → the historical unconstrained behavior; the throw of
// an int must not be flagged. This is what keeps the feature additive.
let diags = throws_mismatches(
r"fn parse(s: string) {
throw 42
}",
);
assert!(
diags.is_empty(),
"an unannotated callable must never be throws-checked: {diags:?}"
);
}
#[test]
fn pipeline_throws_clause_is_enforced() {
let diags = throws_mismatches(
r"pipeline run(task) throws string {
throw 42
}",
);
assert_eq!(
diags.len(),
1,
"a pipeline that throws outside its declared set must raise TYP-026: {diags:?}"
);
}
// --- catch-exhaustiveness: try/catch subtracts handled errors -------------
#[test]
fn caught_error_is_excluded_from_the_declared_channel() {
// The body throws `Parse.Bad`, which the `catch (e: Parse)` handles, so it
// does NOT count against the declared channel. The catch body then throws
// `Net.Down`, which IS declared — so the callable checks clean. If the
// caught `Parse` error leaked into the thrown set this would raise TYP-026
// (Parse is not <: Net).
let diags = throws_mismatches(
r"enum Parse { Bad }
enum Net { Down }
fn f() throws Net {
try {
throw Parse.Bad
} catch (e: Parse) {
throw Net.Down
}
}",
);
assert!(
diags.is_empty(),
"an error handled by its catch clause must not count against `throws`: {diags:?}"
);
}
#[test]
fn uncaught_error_escapes_and_must_be_declared() {
// The body throws `Net.Down` but the catch only handles `Parse`, so
// `Net.Down` is rethrown and escapes. The declared channel is `Parse`,
// which does not cover it → exactly one TYP-026.
let diags = throws_mismatches(
r"enum Parse { Bad }
enum Net { Down }
fn f() throws Parse {
try {
throw Net.Down
} catch (e: Parse) {
}
}",
);
assert_eq!(
diags.len(),
1,
"an error the catch does not cover must escape and raise TYP-026: {diags:?}"
);
}
#[test]
fn catch_all_absorbs_every_body_error() {
// An untyped `catch` is a catch-all: every error the body throws is
// handled, so nothing escapes and the (over-broad) declared channel is not
// violated.
let diags = throws_mismatches(
r"enum Net { Down }
fn f() throws Net {
try {
throw Net.Down
} catch {
}
}",
);
assert!(
diags.is_empty(),
"a catch-all must absorb every body error: {diags:?}"
);
}
#[test]
fn finally_body_throw_escapes() {
// A `throw` in the `finally` block always escapes, regardless of the catch.
// Here it is `Net.Down` against a declared `Parse` channel → TYP-026.
let diags = throws_mismatches(
r"enum Parse { Bad }
enum Net { Down }
fn f() throws Parse {
try {
throw Parse.Bad
} catch (e: Parse) {
} finally {
throw Net.Down
}
}",
);
assert_eq!(
diags.len(),
1,
"a throw in `finally` must escape and be checked against `throws`: {diags:?}"
);
}