Skip to content

Constant-fold ordering comparisons on ASCII Char literals #222

Description

@Unisay

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:

  1. An ASCII pair folds: '\t' < '\n' reduces to a boolean literal.
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    OptimisationA Compiler Optimisationarea: irIR / optimizer / DCE / inliner

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions