Skip to content

fix: Specify Python str semantics for character classification functions - #177

Open
mwiebe wants to merge 2 commits into
OpenJobDescription:mainlinefrom
mwiebe:feature/string-classification-unicode
Open

mwiebe wants to merge 2 commits into
OpenJobDescription:mainlinefrom
mwiebe:feature/string-classification-unicode

Conversation

@mwiebe

@mwiebe mwiebe commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

fix: Specify Python str semantics for character classification functions

Description of the change. What is being added or fixed?

RFC 0006 and the 2026-02 Expression Language spec describe isdigit,
isalpha, isalnum, isspace, isupper, and islower with one-line
summaries ("True if all characters are digits...") that don't say which
character-class convention applies. Every language draws different lines
through Unicode — Rust's Alphabetic property is a superset of Python's
L* categories, Rust has no notion of Numeric_Type, and so on — so
implementations diverged: openjd-rs used Rust's char predicates, which
made isdigit ASCII-only while isalnum was Unicode-wide, and the two
contradicted each other for characters like '٣' (U+0663 ARABIC-INDIC
DIGIT THREE). Reported as
openjd-rs#309.

This PR:

  • Clarifies in both RFC 0006 and the wiki spec that these six functions have
    exactly the semantics of the Python str methods of the same name over
    the Unicode Character Database, with the specific Unicode properties named
    per function.
  • Documents two subtleties: isalnum is strictly broader than
    isalpha OR isdigit (Numeric_Type=Numeric characters like ½), and the
    cased-character rule for isupper/islower (uncased characters are
    ignored; titlecase characters are cased but neither upper nor lower).
  • Permits Unicode-version differences for newly assigned code points, so
    implementations aren't broken by UCD updates.
  • Adds a conformance test (expr2.2.4--string-classification-unicode) with
    21 assertions on characters where the conventions differ, using \uXXXX
    escapes so the file stays ASCII. The test fails against openjd-rs main
    and passes against the fix in the companion PR.

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

The isdigit/isalpha/isalnum/isspace/isupper/islower descriptions in RFC
0006 and the 2026-02 Expression Language spec did not say which
character-class convention applies, and implementations diverged:
openjd-rs used Rust char predicates, making isdigit ASCII-only while
isalnum was Unicode-wide, so isdigit('\u0663') and isalnum('\u0663')
disagreed (openjd-rs issue #309).

Clarify that these functions have exactly the semantics of the Python
str methods of the same name over the Unicode Character Database,
document the cased-character rule for isupper/islower and the
isalnum-vs-isalpha/isdigit relationship, and add a conformance test
covering Unicode inputs where the conventions differ.

Signed-off-by: Mark <399551+mwiebe@users.noreply.github.com>
@mwiebe
mwiebe requested a review from a team as a code owner August 21, 2026 23:37
Follow-up to the character classification clarification: the title and
capitalize descriptions had the same ambiguity, and implementations
diverged the same way (word boundaries by alphanumeric-ness instead of
cased-ness, uppercase instead of titlecase mappings, no Final_Sigma
context rule).

Clarify both function rows and the section note in RFC 0006 and the
2026-02 Expression Language spec, and extend the Unicode conformance
test with title/capitalize cases covering digit word boundaries,
titlecase digraph mappings, sharp-s expansion, and final sigma.

Signed-off-by: Mark <399551+mwiebe@users.noreply.github.com>
leongdl added a commit to leongdl/openjd-specifications that referenced this pull request Sep 14, 2026
Both fixtures were parked in EXPR/jobs/proposed/ pending spec decisions. Both
decisions have landed, so they move up into jobs/ where the runner discovers
them.

center("hi", 7): section 2.2.4 does not say which side of an odd pad takes the
extra space. OpenJobDescription#177 settles ambiguities of this kind by matching Python, so the
fixture's CPython expectation ("   hi  ", extra space left) is now the rule
rather than a strawman.

isdigit over non-ASCII decimal digits: the classification functions carry
Python str semantics, and openjd-rs#341 implemented that from CPython-generated
tables, so the Unicode expectation now holds.

Fixture changes made while promoting:

- Rewrite both headers: drop the PARKED framing and the 2026-08 divergence
  tables, and state the spec rule, the inputs, and what a wrong implementation
  prints.
- Write the digits as \u0663 and \u0969 escapes so the file stays ASCII. The
  runner reads fixtures with the locale encoding, which mojibakes a UTF-8
  literal on Windows. Same reason OpenJobDescription#177 uses escapes.
- Bracket the isdigit assertions, matching the sibling
  expr2.2.4--string-classification fixture. The runner matches output as a
  substring, so an unbracketed value is a weaker assertion.
- Delete proposed/README-func-lib.md. Nothing is left parked in it: the
  repr_py fixture was promoted on OpenJobDescription#162.

Signed-off-by: David Leong <116610336+leongdl@users.noreply.github.com>
leongdl added a commit to leongdl/openjd-specifications that referenced this pull request Sep 14, 2026
Both fixtures were parked in EXPR/jobs/proposed/ pending spec decisions. Both
decisions have landed, so they move up into jobs/ where the runner discovers
them.

center("hi", 7): section 2.2.4 does not say which side of an odd pad takes the
extra space. OpenJobDescription#177 settles ambiguities of this kind by matching Python, so the
fixture's CPython expectation ("   hi  ", extra space left) is now the rule
rather than a strawman.

isdigit over non-ASCII decimal digits: the classification functions carry
Python str semantics, and openjd-rs#341 implemented that from CPython-generated
tables, so the Unicode expectation now holds.

Fixture changes made while promoting:

- Rewrite both headers: drop the PARKED framing and the 2026-08 divergence
  tables, and state the spec rule, the inputs, and what a wrong implementation
  prints.
- Write the digits as \u0663 and \u0969 escapes so the file stays ASCII. The
  runner reads fixtures with the locale encoding, which mojibakes a UTF-8
  literal on Windows. Same reason OpenJobDescription#177 uses escapes.
- Bracket the isdigit assertions, matching the sibling
  expr2.2.4--string-classification fixture. The runner matches output as a
  substring, so an unbracketed value is a weaker assertion.
- Delete proposed/README-func-lib.md. Nothing is left parked in it: the
  repr_py fixture was promoted on OpenJobDescription#162.

Signed-off-by: David Leong <116610336+leongdl@users.noreply.github.com>
leongdl added a commit that referenced this pull request Sep 15, 2026
* test: Add expected-failure function-library fixtures (parked in proposed/)

Three parked fixtures with dual-implementation observations: center() odd-padding and ASCII-only isdigit() are spec gaps where BOTH implementations agree (no divergence - the spec should document the de facto behaviour), and repr_py raw-newline emission is a bug in BOTH implementations per the explicit spec text. Details in proposed/README.md. Companion to conformance-func-lib-gaps.

Review: quorum-review fixes — corrected the center-odd-padding entry on
the measured facts: CPython's str.center('hi',7) is '   hi  ' (extra
space LEFT, verified by execution), current openjd-rs matches CPython
(re-verified against an upstream/main build this session), and the
Python CLI is now the right-heavy outlier — the fixture's committed
expectation is the de facto answer and the earlier README/header claims
were wrong. isdigit reframed as spec ambiguity with NO divergence between
the openjd implementations (both ASCII-only; the divergence is vs host
CPython). repr_py attribution corrected to BOTH implementations.
Kind-level proposed/ placement with a family-named README
(README-func-lib.md) to avoid add/add conflicts with co-located
expected-failures PRs.

Signed-off-by: David Leong <116610336+leongdl@users.noreply.github.com>

* test: Drop the repr_py fixture, promoted on #162

openjd-rs#374 landed the repr_py escaping fix, so
expr2.2.6--repr-py-newline-roundtrip passes and no longer belongs in
proposed/. It is promoted to EXPR/jobs/ on #162 alongside its
WRAP_ACTIONS twin, where the two land together and the control run
against a pre-fix binary is recorded.

Removing it here so the two copies do not collide when both merge. Its
README section goes with it.

The other two fixtures on this branch are untouched and still parked:
both are spec decisions, not implementation bugs.

Signed-off-by: David Leong <116610336+leongdl@users.noreply.github.com>

* test: Promote the center and isdigit function-library fixtures

Both fixtures were parked in EXPR/jobs/proposed/ pending spec decisions. Both
decisions have landed, so they move up into jobs/ where the runner discovers
them.

center("hi", 7): section 2.2.4 does not say which side of an odd pad takes the
extra space. #177 settles ambiguities of this kind by matching Python, so the
fixture's CPython expectation ("   hi  ", extra space left) is now the rule
rather than a strawman.

isdigit over non-ASCII decimal digits: the classification functions carry
Python str semantics, and openjd-rs#341 implemented that from CPython-generated
tables, so the Unicode expectation now holds.

Fixture changes made while promoting:

- Rewrite both headers: drop the PARKED framing and the 2026-08 divergence
  tables, and state the spec rule, the inputs, and what a wrong implementation
  prints.
- Write the digits as \u0663 and \u0969 escapes so the file stays ASCII. The
  runner reads fixtures with the locale encoding, which mojibakes a UTF-8
  literal on Windows. Same reason #177 uses escapes.
- Bracket the isdigit assertions, matching the sibling
  expr2.2.4--string-classification fixture. The runner matches output as a
  substring, so an unbracketed value is a weaker assertion.
- Delete proposed/README-func-lib.md. Nothing is left parked in it: the
  repr_py fixture was promoted on #162.

Signed-off-by: David Leong <116610336+leongdl@users.noreply.github.com>

---------

Signed-off-by: David Leong <116610336+leongdl@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants