1pub struct PureCase {
2 pub id: &'static str,
3 pub source: &'static str,
4 pub entry: &'static str,
5 pub input_json: &'static str,
6 pub expected_json: &'static str,
7}
8
9const REGEX_ERROR_SOURCE: &str = r#"
10 fn inspect(input: string) {
11 const matched = try { regex_match(input, "x"); false } catch (_) { true }
12 const replaced = try { regex_replace(input, "y", "x"); false } catch (_) { true }
13 const captured = try { regex_captures(input, "x"); false } catch (_) { true }
14 const split = try { regex_split("x", input); false } catch (_) { true }
15 return [
16 matched,
17 replaced,
18 captured,
19 split,
20 ]
21 }
22"#;
23
24pub const PURE_CASES: &[PureCase] = &[
25 PureCase {
26 id: "hex-input-errors-are-catchable",
27 source: "fn inspect(input: string) { return try { hex_decode(input); false } catch (_) { true } }",
28 entry: "inspect",
29 input_json: r#""not-hex""#,
30 expected_json: "true",
31 },
32 PureCase {
33 id: "regex-input-errors-are-catchable",
34 source: REGEX_ERROR_SOURCE,
35 entry: "inspect",
36 input_json: r#""[""#,
37 expected_json: "[true,true,true,true]",
38 },
39 PureCase {
40 id: "valid-regex-inputs-do-not-throw",
41 source: REGEX_ERROR_SOURCE,
42 entry: "inspect",
43 input_json: r#""x""#,
44 expected_json: "[false,false,false,false]",
45 },
46 PureCase {
47 id: "hashed-secret-scan-refuses-an-empty-policy",
48 source: r#"
49 fn scan(input: string) {
50 try { secret_scan(input, []) }
51 catch (error) { return to_string(error).contains("hashes must be a nonempty list") }
52 return false
53 }
54 "#,
55 entry: "scan",
56 input_json: r#""clean""#,
57 expected_json: "true",
58 },
59 PureCase {
60 id: "hashed-secret-scan-keeps-locations-without-disclosing-tokens",
61 source: r#"
62 fn scan(input: string) {
63 const findings = secret_scan(input, [sha256("synthetic.private")])
64 let output = []
65 for finding in findings {
66 output = output + [{
67 line: finding.line,
68 column: finding.column_start,
69 end: finding.column_end,
70 redacted: finding.redacted,
71 correct_digest: finding.fingerprint == sha256("synthetic.private"),
72 }]
73 }
74 return output
75 }
76 "#,
77 entry: "scan",
78 input_json: r#""λ SYNTHETIC.PRIVATE\nsynthetic.private.invalid synthetic.private""#,
79 expected_json: r#"[{"line":1,"column":3,"end":20,"redacted":"[hash match]","correct_digest":true},{"line":2,"column":27,"end":44,"redacted":"[hash match]","correct_digest":true}]"#,
80 },
81 PureCase {
82 id: "truthiness-empty-bytes",
83 source: r"
84 fn truth(input: bytes) {
85 if input { return [!input, input ? true : false] }
86 return [!input, false]
87 }
88 ",
89 entry: "truth",
90 input_json: r#"{"$bytes":[]}"#,
91 expected_json: "[true,false]",
92 },
93 PureCase {
94 id: "truthiness-nonempty-bytes-with-zero-byte",
95 source: r"
96 fn truth(input: bytes) {
97 if input { return [!input, input ? true : false] }
98 return [!input, false]
99 }
100 ",
101 entry: "truth",
102 input_json: r#"{"$bytes":[0]}"#,
103 expected_json: "[false,true]",
104 },
105 PureCase {
106 id: "truthiness-nan-folding-and-runtime-branches",
107 source: r"
108 fn truth(input: float) {
109 let nan = input / input
110 let branch = false
111 if nan { branch = true }
112 return [!(0.0 / 0.0), !nan, (0.0 / 0.0) ? true : false,
113 nan ? true : false, (0.0 / 0.0) && true, nan && true,
114 (0.0 / 0.0) || false, nan || false, branch, !input]
115 }
116 ",
117 entry: "truth",
118 input_json: "0.0",
119 expected_json: "[false,false,true,true,true,true,true,true,true,true]",
120 },
121 PureCase {
122 id: "truthiness-duration-and-empty-values",
123 source: r"
124 fn truth(input: {zero: int, one: int, text: string, items: list<int>, record: dict<string, int>}) {
125 return [!0ms, !1ms, !input.zero, !input.one, !input.text, !input.items, !input.record,
126 !nil, !false, !true, ![], !({}), !0.0, !1.0]
127 }
128 ",
129 entry: "truth",
130 input_json: r#"{"zero":0,"one":1,"text":"","items":[],"record":{}}"#,
131 expected_json:
132 "[true,false,true,false,true,true,true,true,true,false,true,true,true,false]",
133 },
134 PureCase {
135 id: "runtime-string-repetition",
136 source: r#"
137 fn repeat(input: {text: string, count: int}) {
138 return [input.text * input.count, input.count * input.text, input.text * 0, input.text * -1, "" * input.count]
139 }
140 "#,
141 entry: "repeat",
142 input_json: r#"{"text":"λx","count":3}"#,
143 expected_json: r#"["λxλxλx","λxλxλx","","",""]"#,
144 },
145 PureCase {
146 id: "runtime-empty-string-repetition-with-huge-count",
147 source: r"fn repeat(input: {text: string, count: int}) { return input.text * input.count }",
148 entry: "repeat",
149 input_json: r#"{"text":"","count":9223372036854775807}"#,
150 expected_json: r#""""#,
151 },
152 PureCase {
153 id: "runtime-integer-exponent-promotes-beyond-u32",
154 source: r"fn power(input: int) { return [1 ** input, (-1) ** input, 2 ** -input] }",
155 entry: "power",
156 input_json: "4294967296",
157 expected_json: "[1.0,1.0,0.0]",
158 },
159 PureCase {
160 id: "runtime-float-integer-exponent-matches-folded-result",
161 source: r"
162 fn power(input: {base: float, exponent: int}) {
163 return [input.base ** input.exponent == 1.1 ** 7, input.base ** -input.exponent == 1.1 ** -7]
164 }
165 ",
166 entry: "power",
167 input_json: r#"{"base":1.1,"exponent":7}"#,
168 expected_json: "[true,true]",
169 },
170 PureCase {
171 id: "runtime-numeric-modulo-with-nonzero-divisors",
172 source: r"
173 fn modulo(input: {integer: int, floating: float}) {
174 return [input.integer % 3, input.integer % 2.5, input.floating % 3, input.floating % 2.5]
175 }
176 ",
177 entry: "modulo",
178 input_json: r#"{"integer":7,"floating":7.5}"#,
179 expected_json: "[1,2.0,1.5,0.0]",
180 },
181 PureCase {
182 id: "typed-record-pick-preserves-nil-and-copies-values",
183 source: r#"
184 type Input = {name: string, age: int, absent: nil, nested: {count: int}}
185 fn project(input: Input) -> {name: string, absent: nil, original: int, copied: int} {
186 let selected = pick(input, ["name", "absent", "nested", "name"])
187 selected.nested.count = 99
188 return {name: selected.name, absent: selected.absent, original: input.nested.count, copied: selected.nested.count}
189 }
190 "#,
191 entry: "project",
192 input_json: r#"{"name":"Ada","age":37,"absent":null,"nested":{"count":1}}"#,
193 expected_json: r#"{"name":"Ada","absent":null,"original":1,"copied":99}"#,
194 },
195 PureCase {
196 id: "typed-record-pick-runtime-keys-and-missing-values",
197 source: r"
198 fn project(input: {data: dict<string, int>, keys: list<string>}) -> dict<string, int> {
199 return pick(input.data, input.keys)
200 }
201 ",
202 entry: "project",
203 input_json: r#"{"data":{"a":1,"b":2},"keys":["b","missing","b"]}"#,
204 expected_json: r#"{"b":2}"#,
205 },
206 PureCase {
207 id: "typed-record-pick-empty",
208 source: r"fn project(input: {name: string}) -> {} { return pick(input, []) }",
209 entry: "project",
210 input_json: r#"{"name":"Ada"}"#,
211 expected_json: "{}",
212 },
213 PureCase {
214 id: "named-record-reducer",
215 source: r#"
216 type State = {count: int, history: list<int>}
217 type Event = {kind: string, amount: int}
218 type Input = {state: State, event: Event}
219
220 fn reduce(input: Input) -> State {
221 if input.event.kind == "reset" {
222 return {count: 0, history: input.state.history + [0]}
223 }
224 const next = input.state.count + input.event.amount
225 return {count: next, history: input.state.history + [next]}
226 }
227 "#,
228 entry: "reduce",
229 input_json: r#"{"state":{"count":2,"history":[1,2]},"event":{"kind":"increment","amount":3}}"#,
230 expected_json: r#"{"count":5,"history":[1,2,5]}"#,
231 },
232 PureCase {
233 id: "closure-capture-and-control-flow",
234 source: r"
235 type TransformInput = {value: int, offset: int, enabled: bool}
236 type TransformResult = {value: int, enabled: bool}
237 fn transform(input: TransformInput) -> TransformResult {
238 const offset = input.offset
239 const add_offset = {value -> value + offset}
240 if input.enabled {
241 return {value: add_offset(input.value), enabled: true}
242 }
243 return {value: input.value, enabled: false}
244 }
245 ",
246 entry: "transform",
247 input_json: r#"{"value":7,"offset":4,"enabled":true}"#,
248 expected_json: r#"{"enabled":true,"value":11}"#,
249 },
250 PureCase {
251 id: "recursive-and-sibling-functions",
252 source: r"
253 fn fib(n: int) -> int {
254 if n <= 1 { return n }
255 return fib(n - 1) + fib(n - 2)
256 }
257 fn solve(input: int) -> int { return fib(input) }
258 ",
259 entry: "solve",
260 input_json: "7",
261 expected_json: "13",
262 },
263 PureCase {
264 id: "forward-declared-capturing-function",
265 source: r"
266 fn reduce(input: int) -> int {
267 const offset = 3
268 return add_offset(input)
269 fn add_offset(value: int) -> int { return value + offset }
270 }
271 ",
272 entry: "reduce",
273 input_json: "4",
274 expected_json: "7",
275 },
276 PureCase {
277 id: "typed-rest-and-sibling-call",
278 source: r"
279 fn collect(...values: int) -> list<int> { return values }
280 fn reduce(input: int) -> list<int> { return collect(input, 2) }
281 ",
282 entry: "reduce",
283 input_json: "7",
284 expected_json: "[7,2]",
285 },
286 PureCase {
287 id: "mutating-methods-rewrite-their-place",
288 source: r#"
289 type MutateInput = {items: list<int>, meta: dict<string, int>}
290 fn mutate(input: MutateInput) {
291 let items = input.items
292 const alias = items
293 items.push(4)
294 const last = items.pop()
295 items.insert(0, 0)
296 items.extend([9])
297 items.reverse()
298 let state = {stack: [1, 2], meta: input.meta}
299 const top = state.stack.pop()
300 const removed = state.meta.remove("a")
301 return {
302 items: items,
303 alias: alias,
304 last: last,
305 top: top,
306 removed: removed,
307 state: state,
308 }
309 }
310 "#,
311 entry: "mutate",
312 input_json: r#"{"items":[1,2,3],"meta":{"a":1,"b":2}}"#,
313 expected_json: r#"{"alias":[1,2,3],"items":[9,3,2,1,0],"last":4,"removed":1,"state":{"meta":{"b":2},"stack":[1]},"top":2}"#,
314 },
315 PureCase {
316 id: "list-string-and-record-operations",
317 source: r"
318 type SummaryInput = {
319 left: list<int>,
320 right: list<int>,
321 prefix: string,
322 name: string,
323 needle: int,
324 meta: dict<string, int>,
325 }
326 fn summarize(input: SummaryInput) {
327 return {
328 items: input.left + input.right,
329 title: input.prefix + input.name,
330 found: input.left.contains(input.needle),
331 fields: input.meta.len(),
332 }
333 }
334 ",
335 entry: "summarize",
336 input_json: r#"{"left":[1,2],"right":[3],"prefix":"Harn ","name":"Kernel","needle":2,"meta":{"a":1,"b":2}}"#,
337 expected_json: r#"{"fields":2,"found":true,"items":[1,2,3],"title":"Harn Kernel"}"#,
338 },
339 PureCase {
340 id: "list-ordering-runtime-and-constant-folding",
341 source: r"
342 type ListComparisonInput = {left: list<int>, right: list<int>}
343 fn compare_lists(input: ListComparisonInput) {
344 return {
345 runtime_less: input.left < input.right,
346 runtime_equal: input.left <= input.left,
347 constant_less: [1, 2] < [1, 3],
348 constant_greater: [2] > [1, 9],
349 }
350 }
351 ",
352 entry: "compare_lists",
353 input_json: r#"{"left":[1,2],"right":[1,3]}"#,
354 expected_json: r#"{"constant_greater":true,"constant_less":true,"runtime_equal":true,"runtime_less":true}"#,
355 },
356 PureCase {
357 id: "mixed-type-equality-is-structural-not-ordering",
358 source: r"
359 fn compare(input: {value: string}) {
360 return {
361 string_is_not_nil: input.value != nil,
362 string_is_not_int: input.value != 7,
363 nil_is_nil: nil == nil,
364 numeric_cross_kind: 1 == 1.0,
365 }
366 }
367 ",
368 entry: "compare",
369 input_json: r#"{"value":"ui://portable"}"#,
370 expected_json: r#"{"nil_is_nil":true,"numeric_cross_kind":true,"string_is_not_int":true,"string_is_not_nil":true}"#,
371 },
372 PureCase {
373 id: "structured-throw-catch",
374 source: r#"
375 fn validate(input: {value: int}) {
376 try {
377 if input.value < 0 {
378 throw {code: "negative", value: input.value}
379 }
380 return {ok: true, value: input.value}
381 } catch error {
382 return {ok: false, value: error.value, code: error.code}
383 }
384 }
385 "#,
386 entry: "validate",
387 input_json: r#"{"value":-9}"#,
388 expected_json: r#"{"code":"negative","ok":false,"value":-9}"#,
389 },
390 PureCase {
391 id: "negative-index-and-slice",
392 source: r"
393 type ChoiceInput = {values: list<int>, text: string}
394 fn choose(input: ChoiceInput) {
395 return {
396 last: input.values[-1],
397 middle: input.values[-4:-1],
398 suffix: input.text[-3:],
399 }
400 }
401 ",
402 entry: "choose",
403 input_json: r#"{"values":[1,2,3,4,5],"text":"kernel"}"#,
404 expected_json: r#"{"last":5,"middle":[2,3,4],"suffix":"nel"}"#,
405 },
406 PureCase {
407 id: "module-capture-property-mutation",
408 source: r"
409 let state = {count: 0}
410 fn reduce(input: {count: int}) {
411 state.count = input.count
412 return {count: state.count}
413 }
414 ",
415 entry: "reduce",
416 input_json: r#"{"count":9}"#,
417 expected_json: r#"{"count":9}"#,
418 },
419 PureCase {
420 id: "iteration-and-copy-on-write-mutation",
421 source: r#"
422 type IterationState = {count: int, tags: list<string>}
423 type IterationInput = {state: IterationState, values: list<int>}
424 fn reduce(input: IterationInput) -> IterationState {
425 let state = input.state
426 for value in input.values {
427 state.count = state.count + value
428 }
429 state.tags[0] = "updated"
430 return state
431 }
432 "#,
433 entry: "reduce",
434 input_json: r#"{"state":{"count":1,"tags":["old"]},"values":[2,3,4]}"#,
435 expected_json: r#"{"count":10,"tags":["updated"]}"#,
436 },
437 PureCase {
438 id: "renderer-string-and-option-primitives",
439 source: r#"
440 type RenderInput = {name: string, validation?: dict<string, bool>}
441 fn reduce(input: RenderInput) {
442 const options = {allow_network: false}.merge(input.validation ?? {})
443 const name = trim(input.name)
444 return {
445 encoded: replace(json_stringify(name), "<", "\\u003c"),
446 portable: starts_with(name, "Portable"),
447 options: options,
448 }
449 }
450 "#,
451 entry: "reduce",
452 input_json: r#"{"name":" Portable <Harn> ","validation":{"allow_host_bridge":true}}"#,
453 expected_json: r#"{"encoded":"\"Portable \\u003cHarn>\"","options":{"allow_host_bridge":true,"allow_network":false},"portable":true}"#,
454 },
455 PureCase {
456 id: "artifact-regex-hash-and-secret-safety-primitives",
457 source: r#"
458 fn inspect(input: string) {
459 const captures = regex_captures("(?is)<body\\b([^>]*)>(.*?)</body>", input)
460 return {
461 body: captures[0].groups[1],
462 scripts: regex_match("(?is)<script\\b", input),
463 text: trim(regex_replace("(?is)<[^>]+>", " ", input)),
464 digest: sha256("abc"),
465 clean: len(secret_scan(input)) == 0,
466 }
467 }
468 "#,
469 entry: "inspect",
470 input_json: r#""<body class='app'>Portable <b>Harn</b></body>""#,
471 expected_json: r#"{"body":"Portable <b>Harn</b>","clean":true,"digest":"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad","scripts":null,"text":"Portable Harn"}"#,
472 },
473 PureCase {
474 id: "target-independent-path-joining",
475 source: r#"
476 fn paths(input: {root: string}) {
477 return {
478 host: path_join(input.root, ".harn", "state.json"),
479 reset: path_join("ignored", "/absolute", "file"),
480 }
481 }
482 "#,
483 entry: "paths",
484 input_json: r#"{"root":"C:\\workspace"}"#,
485 expected_json: r#"{"host":"C:/workspace/.harn/state.json","reset":"/absolute/file"}"#,
486 },
487 PureCase {
488 id: "compiled-secret-pattern-catalog",
489 source: r#"
490 fn inspect(input: string) {
491 const catalog = secret_pattern_catalog()
492 let high_count = 0
493 let contextual = false
494 let detector = ""
495 let pattern = ""
496 for row in catalog {
497 if row.precision == "high" { high_count = high_count + 1 }
498 if row.precision == "heuristic" { contextual = true }
499 if row.detector == "aws-access-key-id" {
500 detector = row.detector
501 pattern = row.regex
502 }
503 }
504 if pattern == "" { throw "missing AWS detector" }
505 return {
506 measured: high_count >= 10,
507 contextual: contextual,
508 detector: detector,
509 accepted: regex_match(pattern, "AKIA" + input) != nil,
510 rejected: regex_match(pattern, "ordinary text") == nil,
511 }
512 }
513 "#,
514 entry: "inspect",
515 input_json: r#""ABCDEFGHIJKLMNOP""#,
516 expected_json: r#"{"accepted":true,"contextual":true,"detector":"aws-access-key-id","measured":true,"rejected":true}"#,
517 },
518 PureCase {
519 id: "result-enum-match-and-propagation",
520 source: r#"
521 fn divide(value: int, divisor: int) -> Result<int, string> {
522 if divisor == 0 { return Result.Err("division by zero") }
523 return Result.Ok(value / divisor)
524 }
525 fn halve(value: int, divisor: int) -> Result<int, string> {
526 const divided: int = divide(value, divisor)?
527 return Result.Ok(divided / 2)
528 }
529 fn reduce(input: int) {
530 const result = halve(12, input)
531 match result {
532 Result.Ok(value) -> { return [result.variant, result.fields, value] }
533 Result.Err(message) -> { return [result.variant, result.fields, message] }
534 }
535 }
536 "#,
537 entry: "reduce",
538 input_json: "3",
539 expected_json: r#"["Ok",[2],2]"#,
540 },
541 PureCase {
542 id: "float-param-accepts-int",
545 source: r"
546 fn takes_float(x: float) -> float { return x + 0.0 }
547 fn reduce(input: int) -> float { return takes_float(input) }
548 ",
549 entry: "reduce",
550 input_json: "3",
551 expected_json: "3.0",
552 },
553 PureCase {
554 id: "cleanup-runs-once-for-callee-errors",
558 source: r#"
559 fn fail(message: string) -> int { throw message }
560 fn relay(message: string) -> int { return fail(message) + 1 }
561 fn reduce(input: list<string>) -> {finally_runs: int, defer_runs: int, caught: list<string>} {
562 let finally_runs = 0
563 let defer_runs = 0
564 let caught = []
565 for label in input {
566 try {
567 defer { defer_runs = defer_runs + 1 }
568 try {
569 if label == "direct" {
570 fail(label)
571 } else {
572 relay(label)
573 }
574 } finally {
575 finally_runs = finally_runs + 1
576 }
577 } catch (error) {
578 caught = caught + [error]
579 }
580 }
581 return {finally_runs: finally_runs, defer_runs: defer_runs, caught: caught}
582 }
583 "#,
584 entry: "reduce",
585 input_json: r#"["direct","relayed"]"#,
586 expected_json: r#"{"finally_runs":2,"defer_runs":2,"caught":["direct","relayed"]}"#,
587 },
588 PureCase {
589 id: "try-operator-runs-pending-finally",
593 source: r#"
594 fn check(value: int) -> Result<int, string> {
595 if value < 0 { return Result.Err("negative") }
596 return Result.Ok(value)
597 }
598 fn guarded(value: int) -> Result<int, string> {
599 try {
600 const checked = check(value)?
601 return Result.Ok(checked + 1)
602 } finally {
603 if value < 0 { return Result.Err("cleanup") }
604 }
605 return Result.Err("fell through")
606 }
607 fn reduce(input: list<int>) -> list<list<any>> {
608 let rendered = []
609 for value in input {
610 const result = guarded(value)
611 rendered = rendered + [[result.variant, result.fields]]
612 }
613 return rendered
614 }
615 "#,
616 entry: "reduce",
617 input_json: "[-1,4]",
618 expected_json: r#"[["Err",["cleanup"]],["Ok",[5]]]"#,
619 },
620];
621
622pub struct RuntimeFailureCase {
628 pub id: &'static str,
629 pub source: &'static str,
630 pub entry: &'static str,
631 pub input_json: &'static str,
632 pub expected_code: &'static str,
633}
634
635pub const RUNTIME_FAILURE_CASES: &[RuntimeFailureCase] = &[
636 RuntimeFailureCase {
637 id: "catch-cannot-bypass-a-value-limit",
638 source: r#"fn repeat(input: string) { try { return input * 1048577 } catch (_) { return "caught" } }"#,
639 entry: "repeat",
640 input_json: r#""x""#,
641 expected_code: "value_byte_limit",
642 },
643 RuntimeFailureCase {
644 id: "string-repetition-rejects-output-over-byte-limit",
645 source: r"fn repeat(input: {text: string, count: int}) { return input.text * input.count }",
646 entry: "repeat",
647 input_json: r#"{"text":"ab","count":524289}"#,
648 expected_code: "value_byte_limit",
649 },
650 RuntimeFailureCase {
651 id: "string-repetition-rejects-overflowing-output-length",
652 source: r"fn repeat(input: {text: string, count: int}) { return input.count * input.text }",
653 entry: "repeat",
654 input_json: r#"{"text":"abc","count":9223372036854775807}"#,
655 expected_code: "value_byte_limit",
656 },
657 RuntimeFailureCase {
658 id: "integer-modulo-by-integer-zero",
659 source: r"fn modulo(input: {left: int, right: int}) { return input.left % input.right }",
660 entry: "modulo",
661 input_json: r#"{"left":7,"right":0}"#,
662 expected_code: "division_by_zero",
663 },
664 RuntimeFailureCase {
665 id: "float-modulo-by-integer-zero",
666 source: r"fn modulo(input: {left: float, right: int}) { return input.left % input.right }",
667 entry: "modulo",
668 input_json: r#"{"left":7.5,"right":0}"#,
669 expected_code: "division_by_zero",
670 },
671 RuntimeFailureCase {
672 id: "integer-modulo-by-float-zero",
673 source: r"fn modulo(input: {left: int, right: float}) { return input.left % input.right }",
674 entry: "modulo",
675 input_json: r#"{"left":7,"right":0.0}"#,
676 expected_code: "division_by_zero",
677 },
678 RuntimeFailureCase {
679 id: "float-modulo-by-negative-float-zero",
680 source: r"fn modulo(input: {left: float, right: float}) { return input.left % input.right }",
681 entry: "modulo",
682 input_json: r#"{"left":7.5,"right":-0.0}"#,
683 expected_code: "division_by_zero",
684 },
685 RuntimeFailureCase {
686 id: "declared-error-survives-transparent-rethrows",
687 source: r"
688 fn fail(input: int) -> any throws int { throw input }
689 fn reduce(input: int) -> any throws int {
690 try { return retry 2 { try* fail(input) } }
691 finally { try { throw 8 } catch (_ignored) {} }
692 }
693 ",
694 entry: "reduce",
695 input_json: "7",
696 expected_code: "harn_declared_throw",
697 },
698 RuntimeFailureCase {
699 id: "legacy-error-stays-runtime-under-declared-wrapper",
700 source: r"
701 fn fail(input: int) -> any { throw input }
702 fn reduce(input: int) -> any throws int {
703 try { return retry 2 { try* fail(input) } }
704 finally { try { throw 8 } catch (_ignored) {} }
705 }
706 ",
707 entry: "reduce",
708 input_json: "7",
709 expected_code: "harn_throw",
710 },
711 RuntimeFailureCase {
712 id: "pick-rejects-non-record-source",
713 source: r#"fn project(input: any) { return pick(input, ["name"]) }"#,
714 entry: "project",
715 input_json: "42",
716 expected_code: "builtin_type",
717 },
718 RuntimeFailureCase {
719 id: "pick-rejects-non-string-key",
720 source: r"fn project(input: {name: string, keys: list<any>}) { return pick(input, input.keys) }",
721 entry: "project",
722 input_json: r#"{"name":"Ada","keys":[1]}"#,
723 expected_code: "builtin_type",
724 },
725 RuntimeFailureCase {
726 id: "pick-rejects-wrong-arity",
727 source: r"fn project(args: list<any>) { return pick(...args) }",
728 entry: "project",
729 input_json: r#"[{"name":"Ada"}]"#,
730 expected_code: "builtin_type",
731 },
732 RuntimeFailureCase {
733 id: "int-param-rejects-float",
734 source: r"
735 fn takes_int(n: int) -> int { return n }
736 // Deliberate unchecked host boundary: the failure belongs to the
737 // portable runtime parameter contract.
738 fn reduce(input: any) { return takes_int(input) }
739 ",
740 entry: "reduce",
741 input_json: "2.5",
742 expected_code: "argument_type",
743 },
744];
745
746pub struct InvalidCase {
747 pub id: &'static str,
748 pub source: &'static str,
749 pub entry: &'static str,
750 pub expected_code: &'static str,
751}
752
753pub const INVALID_CASES: &[InvalidCase] = &[
754 InvalidCase {
755 id: "pick-unknown-field",
756 source: r#"fn project(input: {name: string}) { return pick(input, ["nmae"]) }"#,
757 entry: "project",
758 expected_code: "compile_frontend",
759 },
760 InvalidCase {
761 id: "pick-dynamic-fields-are-not-required",
762 source: r"fn project(input: {name: string}, keys: list<string>) -> {name: string} { return pick(input, keys) }",
763 entry: "project",
764 expected_code: "compile_frontend",
765 },
766 InvalidCase {
767 id: "frontend-syntax-error",
768 source: "fn reduce( {",
769 entry: "reduce",
770 expected_code: "compile_frontend",
771 },
772 InvalidCase {
773 id: "invalid-mutable-local-program",
774 source: r"
775 fn reduce(input) {
776 var value = input
777 value = value + 1
778 return value
779 }
780 ",
781 entry: "reduce",
782 expected_code: "compile_frontend",
783 },
784];
785
786#[derive(Clone, Copy)]
787pub struct PackageCompilationCase {
788 pub id: &'static str,
789 pub source: &'static str,
790 pub entry: &'static str,
791 pub expected_ok: bool,
792}
793
794impl PackageCompilationCase {
795 pub fn package(&self) -> harn_kernel::PortableSourcePackage {
796 harn_kernel::PortableSourcePackage {
797 root_source: self.source.to_string(),
798 root_imports: Vec::new(),
799 modules: Vec::new(),
800 }
801 }
802
803 pub fn compile(&self) -> Result<harn_kernel::ProgramArtifact, Vec<harn_kernel::Diagnostic>> {
804 let result = harn_kernel::compile_source_package(
805 self.package(),
806 self.entry,
807 harn_kernel::EntryKind::Function,
808 );
809 assert_eq!(
810 result.is_ok(),
811 self.expected_ok,
812 "{} compilation decision drifted",
813 self.id
814 );
815 if let Err(diagnostics) = &result {
816 assert!(
817 !diagnostics.is_empty(),
818 "{} has no failure diagnostic",
819 self.id
820 );
821 }
822 result
823 }
824}
825
826pub fn package_compilation_cases() -> impl Iterator<Item = PackageCompilationCase> {
827 INVALID_CASES
828 .iter()
829 .map(|case| PackageCompilationCase {
830 id: case.id,
831 source: case.source,
832 entry: case.entry,
833 expected_ok: false,
834 })
835 .chain(PACKAGE_COMPILATION_CASES.iter().copied())
836}
837
838pub const PACKAGE_COMPILATION_CASES: &[PackageCompilationCase] = &[
839 PackageCompilationCase {
840 id: "removed-provider-wrapper-alias",
841 source: "fn reduce(input: string) { return with_rate_limit(input) }",
842 entry: "reduce",
843 expected_ok: false,
844 },
845 PackageCompilationCase {
846 id: "retained-provider-call-alias",
847 source: "fn reduce(input: string) { return llm_call(input) }",
848 entry: "reduce",
849 expected_ok: true,
850 },
851 PackageCompilationCase {
852 id: "retained-provider-call-method",
853 source: "fn reduce(harness: Harness, input: string) { return harness.llm.call(input) }",
854 entry: "reduce",
855 expected_ok: true,
856 },
857];