Summary
Add automatic type coercion to where clause comparisons so that keywords (atoms) and strings are compared by value, enabling LLM-generated code like (where :department = :engineering) to match string data values like "engineering".
Context
Architecture reference: PTC-Lisp Specification - Section 7.1 where
Dependencies: None
Related issues: None
Current State
Verified: The comparison helpers in lib/ptc_runner/lisp/eval.ex (lines 545-570) use strict equality without type coercion:
defp safe_eq(a, b), do: a == b # No type coercion
When filtering data with string values using keyword literals:
(where :department = :engineering) ; Returns [] when data has "engineering"
(where :department = "engineering") ; Works correctly
The field accessor already supports flexible key access via flex_get_in in lib/ptc_runner/lisp/runtime.ex, but value comparison does not.
This causes LLM-generated programs to fail silently when they use keywords instead of strings for comparison values - the filter returns empty results instead of matching data.
Acceptance Criteria
Implementation Hints
Files to modify:
lib/ptc_runner/lisp/eval.ex - Add type coercion clauses to safe_eq, safe_in, safe_includes
test/ptc_runner/lisp/flex_access_test.exs - Add tests for keyword/string value coercion
Patterns to follow:
- The existing
flex_get pattern in lib/ptc_runner/lisp/runtime.ex (lines 19-39) shows how to handle atom/string conversion
Implementation approach:
# In safe_eq - add before catch-all clause:
defp safe_eq(a, b) when is_atom(a) and not is_boolean(a) and is_binary(b), do: to_string(a) == b
defp safe_eq(a, b) when is_binary(a) and is_atom(b) and not is_boolean(b), do: a == to_string(b)
defp safe_eq(a, b), do: a == b
# In safe_in - normalize values before membership check:
defp safe_in(value, coll) when is_list(coll) do
normalized = normalize_for_comparison(value)
Enum.any?(coll, fn item -> normalize_for_comparison(item) == normalized end)
end
defp normalize_for_comparison(v) when is_atom(v) and not is_boolean(v), do: to_string(v)
defp normalize_for_comparison(v), do: v
# In safe_includes - for list membership, use the same normalize_for_comparison approach
defp safe_includes(coll, value) when is_list(coll) do
normalized = normalize_for_comparison(value)
Enum.any?(coll, fn item -> normalize_for_comparison(item) == normalized end)
end
Design decision - Ordering operators:
Ordering operators (>, <, >=, <=) should NOT coerce atom/string types. Comparing :active > "active" would be semantically meaningless. These should return false (matching nil behavior) rather than attempting string comparison. No changes needed to safe_cmp.
Edge cases to consider:
nil should NOT be coerced (already handled by existing nil guards)
- Boolean atoms (
true, false) should NOT be coerced to strings - use is_boolean/1 guard
- Numbers should NOT be affected by this change
- Empty atom
:"" coerces to empty string "" (correct behavior)
Test Plan
Unit tests:
safe_eq(:keyword, "string") returns true
safe_eq("string", :keyword) returns true
safe_eq(:keyword, :keyword) returns true (exact match still works)
safe_eq(true, "true") returns false (booleans not coerced)
safe_eq(false, "false") returns false (booleans not coerced)
safe_in(:active, ["active", "pending"]) returns true
safe_in("active", [:active, :pending]) returns true
safe_includes([:active, :pending], "active") returns true
E2E test:
test "where clause coerces keywords to strings" do
program = ~S|(->> ctx/items (filter (where :status = :active)))|
context = %{"items" => [
%{"status" => "active", "name" => "A"},
%{"status" => "inactive", "name" => "B"}
]}
assert {:ok, [%{"status" => "active", "name" => "A"}], _, _} =
PtcRunner.Lisp.run(program, context: context)
end
test "in operator coerces keywords to strings" do
program = ~S|(->> ctx/items (filter (where :status in [:active :pending])))|
context = %{"items" => [
%{"status" => "active"},
%{"status" => "pending"},
%{"status" => "closed"}
]}
assert {:ok, [%{"status" => "active"}, %{"status" => "pending"}], _, _} =
PtcRunner.Lisp.run(program, context: context)
end
Out of Scope
- Changing how field keys are accessed (already handled by
flex_get)
- Coercing numbers to strings or vice versa
- Coercing booleans to strings
- Adding warning/logging when coercion occurs
- Coercion for ordering operators (
>, <, >=, <=)
Documentation Updates
docs/ptc-lisp-specification.md - Update Section 7.1 to mention that comparison values support keyword/string coercion (similar to the existing "Flexible Key Access" section)
Summary
Add automatic type coercion to
whereclause comparisons so that keywords (atoms) and strings are compared by value, enabling LLM-generated code like(where :department = :engineering)to match string data values like"engineering".Context
Architecture reference: PTC-Lisp Specification - Section 7.1
whereDependencies: None
Related issues: None
Current State
Verified: The comparison helpers in
lib/ptc_runner/lisp/eval.ex(lines 545-570) use strict equality without type coercion:When filtering data with string values using keyword literals:
The field accessor already supports flexible key access via
flex_get_ininlib/ptc_runner/lisp/runtime.ex, but value comparison does not.This causes LLM-generated programs to fail silently when they use keywords instead of strings for comparison values - the filter returns empty results instead of matching data.
Acceptance Criteria
(where :field = :keyword)matches string values (e.g.,:engineeringmatches"engineering")(where :field = "string")matches keyword values (e.g.,"engineering"matches:engineering)inoperator handles mixed keyword/string membership (e.g.,(where :status in [:active :pending])matches"active")includesoperator handles keyword/string comparison for list membership>,<,>=,<=) do NOT coerce types - comparing atom to string returnsfalse(consistent with nil handling)true,false) are NOT coerced to strings"true","false"Implementation Hints
Files to modify:
lib/ptc_runner/lisp/eval.ex- Add type coercion clauses tosafe_eq,safe_in,safe_includestest/ptc_runner/lisp/flex_access_test.exs- Add tests for keyword/string value coercionPatterns to follow:
flex_getpattern inlib/ptc_runner/lisp/runtime.ex(lines 19-39) shows how to handle atom/string conversionImplementation approach:
Design decision - Ordering operators:
Ordering operators (
>,<,>=,<=) should NOT coerce atom/string types. Comparing:active>"active"would be semantically meaningless. These should returnfalse(matching nil behavior) rather than attempting string comparison. No changes needed tosafe_cmp.Edge cases to consider:
nilshould NOT be coerced (already handled by existing nil guards)true,false) should NOT be coerced to strings - useis_boolean/1guard:""coerces to empty string""(correct behavior)Test Plan
Unit tests:
safe_eq(:keyword, "string")returnstruesafe_eq("string", :keyword)returnstruesafe_eq(:keyword, :keyword)returnstrue(exact match still works)safe_eq(true, "true")returnsfalse(booleans not coerced)safe_eq(false, "false")returnsfalse(booleans not coerced)safe_in(:active, ["active", "pending"])returnstruesafe_in("active", [:active, :pending])returnstruesafe_includes([:active, :pending], "active")returnstrueE2E test:
Out of Scope
flex_get)>,<,>=,<=)Documentation Updates
docs/ptc-lisp-specification.md- Update Section 7.1 to mention that comparison values support keyword/string coercion (similar to the existing "Flexible Key Access" section)