What
The IR constant folder evaluates equality on Char literals but leaves ordering comparisons (<, <=, >, >=) on Char literals alone, even when both operands are ASCII. Extend it to evaluate ordering on two ASCII Char literals at compile time.
Observed
In test/ps/output/Golden.CharLiterals.Test/golden.lua, show ('\t' < '\n') compiles to a runtime branch on a constant condition:
local v = "Data.Ordering∷Ordering.LT" == (function()
if "\t" < "\n" then
return "Data.Ordering∷Ordering.LT"
else
return "Data.Ordering∷Ordering.GT"
end
end)()
if v then return "true" elseif false == v then return "false" else return error("No patterns matched") end
"\t" < "\n" compares two compile-time constants (bytes 9 and 10), so the whole block could fold to "true". The sibling show ('\n' == '\n') one line above already folds to "true", which is what makes the unfolded ordering stand out.
Why it is unfolded today (deliberate)
numericCompare, inside foldPrimBinOp in lib/Language/PureScript/Backend/IR/Optimizer.hs, folds PrimLt/PrimLe/PrimGt/PrimGe only for (LiteralInt, LiteralInt) and (LiteralFloat, LiteralFloat). Note [Folding primops follows Lua 5.1] records the reason: an IR Char/String literal carries a semantic Unicode value, but Lua orders strings by bytes, so folding with the host's Ord could disagree with the runtime on non-ASCII operands. Equality still folds because byte-equality and codepoint-equality coincide; only ordering is held back.
The refinement
For two Char literals whose codepoints are both below U+0080, the single-byte Lua representation orders identically to the codepoint, so host-side folding matches the runtime exactly. Gate the new fold on the ASCII range.
Non-ASCII chars and String literals stay out of scope. Their byte order can diverge from codepoint order, and strings add length and multi-byte questions on top. They can be a later follow-up if they ever earn it.
Proposed implementation
Add one arm to numericCompare:
(LiteralChar _ x, LiteralChar _ y)
| isAscii x, isAscii y → Just (compare x y) -- isAscii from Data.Char
The existing downstream folds then cascade: if True then LT else GT becomes LT, "…LT" == "…LT" becomes True, if True then "true" becomes "true", collapsing the CharLiterals example to log("true")().
Test (ASCII-correctness is the point)
Work test-first. The test has to prove two things, not one:
- An ASCII pair folds:
'\t' < '\n' reduces to a boolean literal.
- A non-ASCII pair is left untouched: comparing two chars with codepoints at or above U+0080 does not fold. This is the soundness guard, and it is the reason to write a dedicated test rather than lean on the golden alone.
A focused Optimizer unit test covers both cases directly. The Golden.CharLiterals.Test golden also moves, since its final log collapses to log("true")().
Payoff
Small in practice. Two literal chars compared at compile time is rare in real code, so this is a completeness fix rather than a performance one. It surfaced while reviewing #180 (PR #219), where the dictionary-method inlining folded everything around this comparison but not the comparison itself.
What
The IR constant folder evaluates equality on
Charliterals but leaves ordering comparisons (<,<=,>,>=) onCharliterals alone, even when both operands are ASCII. Extend it to evaluate ordering on two ASCIICharliterals at compile time.Observed
In
test/ps/output/Golden.CharLiterals.Test/golden.lua,show ('\t' < '\n')compiles to a runtime branch on a constant condition:"\t" < "\n"compares two compile-time constants (bytes 9 and 10), so the whole block could fold to"true". The siblingshow ('\n' == '\n')one line above already folds to"true", which is what makes the unfolded ordering stand out.Why it is unfolded today (deliberate)
numericCompare, insidefoldPrimBinOpinlib/Language/PureScript/Backend/IR/Optimizer.hs, foldsPrimLt/PrimLe/PrimGt/PrimGeonly for(LiteralInt, LiteralInt)and(LiteralFloat, LiteralFloat). Note [Folding primops follows Lua 5.1] records the reason: an IRChar/Stringliteral carries a semantic Unicode value, but Lua orders strings by bytes, so folding with the host'sOrdcould disagree with the runtime on non-ASCII operands. Equality still folds because byte-equality and codepoint-equality coincide; only ordering is held back.The refinement
For two
Charliterals whose codepoints are both below U+0080, the single-byte Lua representation orders identically to the codepoint, so host-side folding matches the runtime exactly. Gate the new fold on the ASCII range.Non-ASCII chars and
Stringliterals stay out of scope. Their byte order can diverge from codepoint order, and strings add length and multi-byte questions on top. They can be a later follow-up if they ever earn it.Proposed implementation
Add one arm to
numericCompare:The existing downstream folds then cascade:
if True then LT else GTbecomesLT,"…LT" == "…LT"becomesTrue,if True then "true"becomes"true", collapsing the CharLiterals example tolog("true")().Test (ASCII-correctness is the point)
Work test-first. The test has to prove two things, not one:
'\t' < '\n'reduces to a boolean literal.A focused
Optimizerunit test covers both cases directly. TheGolden.CharLiterals.Testgolden also moves, since its finallogcollapses tolog("true")().Payoff
Small in practice. Two literal chars compared at compile time is rare in real code, so this is a completeness fix rather than a performance one. It surfaced while reviewing #180 (PR #219), where the dictionary-method inlining folded everything around this comparison but not the comparison itself.