From 85bc33cb5bf074172dce6ae42229b857edd38d6d Mon Sep 17 00:00:00 2001 From: Sean Tang <171081544+seant-aws@users.noreply.github.com> Date: Wed, 12 Aug 2026 21:47:30 +0000 Subject: [PATCH] fix(expr): use NULLTYPE for empty flatten result instead of INT MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit flatten_fn defaulted to ExprType::INT when the flattened result was empty. This caused downstream functions without a list[int] overload (e.g. repr_sh) to reject the result, even though the expression is valid — an empty comprehension is a normal case. The correct default is ExprType::NULLTYPE, which is the bottom type for empty lists. All repr_* functions already declare a (list[nulltype]) overload, and overload resolution matches it exactly. Signed-off-by: Sean Tang <171081544+seant-aws@users.noreply.github.com> --- crates/openjd-expr/src/functions/list.rs | 5 ++- .../tests/integration/test_lists.rs | 38 +++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/crates/openjd-expr/src/functions/list.rs b/crates/openjd-expr/src/functions/list.rs index 69c8ddde..cde79f85 100644 --- a/crates/openjd-expr/src/functions/list.rs +++ b/crates/openjd-expr/src/functions/list.rs @@ -67,7 +67,10 @@ pub fn flatten_fn(ctx: Ctx, a: &[ExprValue]) -> R { } } let et = if result.is_empty() { - ExprType::INT + // An empty flatten result has no elements to infer a type from. + // Use NULLTYPE (the bottom type for lists) so the result is accepted by + // any function that declares a list[nulltype] overload (e.g. repr_sh). + ExprType::NULLTYPE } else { result[0].expr_type() }; diff --git a/crates/openjd-expr/tests/integration/test_lists.rs b/crates/openjd-expr/tests/integration/test_lists.rs index 60afd0ab..9e766b3a 100644 --- a/crates/openjd-expr/tests/integration/test_lists.rs +++ b/crates/openjd-expr/tests/integration/test_lists.rs @@ -1714,3 +1714,41 @@ fn empty_listcomp_type_is_nulltype() { "list[nulltype]" ); } + +#[test] +fn flatten_empty_comprehension_has_nulltype() { + // Regression: flatten over an empty comprehension result must yield list[nulltype], + // not list[int], so that functions like repr_sh (which has a list[nulltype] overload) + // accept the result. + let mut st = SymbolTable::new(); + st.set( + "Env", + ExprValue::make_list(vec![], ExprType::STRING).unwrap(), + ) + .unwrap(); + let parsed = + openjd_expr::ParsedExpression::new("repr_sh(flatten([[\"-e\", e] for e in Env]))").unwrap(); + let result = parsed.evaluate(&st).unwrap(); + // repr_sh of an empty list produces an empty string + assert_eq!(result.to_display_string(), ""); +} + +#[test] +fn flatten_nonempty_comprehension_has_string_type() { + // Non-empty case: flatten over a string comprehension must still yield list[string]. + let mut st = SymbolTable::new(); + st.set( + "Env", + ExprValue::make_list(vec![ExprValue::String("A=1".to_string())], ExprType::STRING).unwrap(), + ) + .unwrap(); + let parsed = + openjd_expr::ParsedExpression::new("repr_sh(flatten([[\"-e\", e] for e in Env]))").unwrap(); + let result = parsed.evaluate(&st).unwrap(); + // repr_sh on a list of strings should produce shell-escaped space-separated values + assert!( + result.to_display_string().contains("-e"), + "expected repr_sh output to contain -e, got: {}", + result.to_display_string() + ); +}