Skip to main content

portable_conformance_json/testdata/
portable_conformance.rs

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        // Widening: a whole int satisfies a float parameter. This is the
543        // half of harn#6267 that payload schemas used to get wrong.
544        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        // harn#9189: cleanup runs once for errors raised by a callee, not
555        // only for an inline `throw`. Runtime failures such as division by
556        // zero are terminal in the portable kernel, so only throws appear.
557        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        // harn#9201: `?` returns an `Err` through pending cleanup. The
590        // `finally` overrides the early return, so the result only reads
591        // `cleanup` when that cleanup ran.
592        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
622/// Runtime failures that every portable executor must agree on.
623///
624/// Unlike [`PURE_CASES`], these are expected to fail after a successful
625/// compile. The int←float case is the drift that used to slip past
626/// `browser_worker_matches_native_portable_corpus_exactly` (harn#6267).
627pub 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];