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