diff --git a/.github/workflows/lexer.yml b/.github/workflows/lexer.yml new file mode 100644 index 00000000000..3e646dd3d48 --- /dev/null +++ b/.github/workflows/lexer.yml @@ -0,0 +1,48 @@ +name: Unicode lexer + +on: + push: + branches: + - equality-glyph-aliases + pull_request: + paths: + - code/src/main/gram.c + - code/src/main/gram.y + - code/src/include/Rinlinedfuns.h + - code/tests/reg-encodings.R + - code/tests/ir-unicode-lexer.R + - .github/workflows/lexer.yml + +jobs: + lexer: + runs-on: ubuntu-24.04 + timeout-minutes: 30 + env: + LC_ALL: C.UTF-8 + IR_BUILD_LOCALE: C.UTF-8 + steps: + - uses: actions/checkout@v4 + + - name: Install build prerequisites + run: | + sudo apt-get update + sudo apt-get install -y \ + build-essential gfortran perl pkg-config texinfo \ + libreadline-dev libpcre2-dev libcurl4-openssl-dev \ + libbz2-dev liblzma-dev zlib1g-dev libdeflate-dev + + - name: Configure + run: | + mkdir build + cd build + sh ../code/configure \ + --with-x=no \ + --without-tcltk \ + --without-recommended-packages \ + --disable-java + + - name: Build + run: make -C build -j2 + + - name: Test Unicode lexer priority + run: build/bin/R --vanilla < code/tests/ir-unicode-lexer.R diff --git a/R-in-the-Mac-Terminal.md b/R-in-the-Mac-Terminal.md index dd400f4db3f..ae249c3a198 100644 --- a/R-in-the-Mac-Terminal.md +++ b/R-in-the-Mac-Terminal.md @@ -20,7 +20,7 @@ At the R prompt, the quick check is: ```r answer ← 8 ÷ 2 -answer = 4 +answer ≟ 4 ``` The last line should print `TRUE`. diff --git a/R-on-Linux.md b/R-on-Linux.md index f2003e3aefd..cc82fd65e51 100644 --- a/R-on-Linux.md +++ b/R-on-Linux.md @@ -55,7 +55,7 @@ Then try: ```r answer ← 8 ÷ 2 -answer = 4 +answer ≟ 4 ``` The final line should print: @@ -76,7 +76,7 @@ RSTUDIO_WHICH_R="$HOME/.local/r-with-symbols/bin/R" \ rstudio ``` -In RStudio's Console, run `R.home()` and confirm that it begins with your `.local/r-with-symbols` folder. Posit documents `RSTUDIO_WHICH_R` as its Linux version-selection override: [Changing R versions for RStudio Desktop](https://support.posit.co/hc/en-us/articles/200486138-Changing-R-versions-for-the-RStudio-Desktop-IDE). +In RStudio's Console, run `R.home()` and confirm that it begins with your `.local/r-with-symbols` folder. Posit documents `RSTUDIO_WHICH_R` as its Linux version-selection override: [Changing R versions for RStudio Desktop](https://support.posit.co/hc/en-us/articles/200486138-Changing-R-versions-for-RStudio-Desktop-IDE). RStudio was not installed in the build environment used for this project, so the RStudio handoff follows Posit's documented mechanism but has not yet been exercised here. The R build itself and its regression tests were exercised. diff --git a/README.md b/README.md index a47f532b49e..77c6c4cdccb 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -### fn λ → ÷ = ← +### fn λ → ÷ ≟ ← → and ← can now be used for assignment, not just `<-` and `->`. @@ -13,7 +13,7 @@ Existing R spelling remains available: `<-`, `<<-`, `->`, `->>`, `==`, and `func ```r answer ← 8 ÷ 2 -answer = 4 +answer ≟ 4 ``` The last line returns @@ -22,7 +22,7 @@ The last line returns [1] TRUE ``` -because = now means equal. +because ≟ tests equality. @@ -34,13 +34,13 @@ because = now means equal. | `x ↞ 3` | assign in an enclosing frame | | `3 → x` | assign | | `3 ↠ x` | assign in an enclosing frame | -| `left = right` | test equality | +| `left ≟ right` | test equality | | `fn(x) expression`, `λ(x) expression`, or `ƒ(x) expression` | construct a function | | `left ÷ right` | divide | - +Prefer `≟` for equality. `?=`, `=?`, `?=?`, `¿=?`, `=`, and `==` are aliases for the same test. Parameters are still supplied with =. @@ -53,7 +53,7 @@ clean.mean ← λ(x) mean(x, na.rm = TRUE) *Actually this is worse than I thought it would be, I thought there would be no downsides....* -That compatibility is contextual. Inside another call, wrap an equality comparison in parentheses so it cannot be read as an argument name: +Only the `=` equality alias is contextual. Inside another call, wrap an equality comparison written with `=` in parentheses so it cannot be read as an argument name: ```r stopifnot((answer = 4)) diff --git a/RStudio-on-Mac.md b/RStudio-on-Mac.md index a7ad9a7bd55..a71833622e1 100644 --- a/RStudio-on-Mac.md +++ b/RStudio-on-Mac.md @@ -22,10 +22,10 @@ After RStudio opens, verify the session in its Console: ```r R.home() one ← 1 -one = 1 +one ≟ 1 ``` -The final expression should print `TRUE`. Posit documents the environment-variable selection method here: [Changing R versions for RStudio Desktop](https://support.posit.co/hc/en-us/articles/200486138-Changing-R-versions-for-the-RStudio-Desktop-IDE). +The final expression should print `TRUE`. Posit documents the environment-variable selection method here: [Changing R versions for RStudio Desktop](https://support.posit.co/hc/en-us/articles/200486138-Changing-R-versions-for-RStudio-Desktop-IDE). ## Switching back diff --git a/RStudio-on-Windows.md b/RStudio-on-Windows.md index 9ebe863c960..35b1da823dc 100644 --- a/RStudio-on-Windows.md +++ b/RStudio-on-Windows.md @@ -19,14 +19,14 @@ RStudio supports side-by-side R installations on Windows. After installing the t 3. Next to **R version**, select **Change**. 4. Choose the separately installed build described by this page, then restart RStudio. -You can also hold **Ctrl** while RStudio starts to open its R-version chooser. These are Posit's documented Windows controls: [Changing R versions for RStudio Desktop](https://support.posit.co/hc/en-us/articles/200486138-Changing-R-versions-for-the-RStudio-Desktop-IDE). +You can also hold **Ctrl** while RStudio starts to open its R-version chooser. These are Posit's documented Windows controls: [Changing R versions for RStudio Desktop](https://support.posit.co/hc/en-us/articles/200486138-Changing-R-versions-for-RStudio-Desktop-IDE). In the Console, check the result: ```r R.version.string one ← 1 -one = 1 +one ≟ 1 ``` The final expression should print `TRUE`. diff --git a/code/src/main/gram.c b/code/src/main/gram.c index 1625e8fc677..d15fd020d38 100644 --- a/code/src/main/gram.c +++ b/code/src/main/gram.c @@ -5954,6 +5954,18 @@ static int token(void) yylval = install_and_save_mbcs("<<-", c, clen); return RIGHT_ASSIGN; } + if (wc == 0x225f) { /* ≟ is == */ + yylval = install_and_save_mbcs("==", c, clen); + return EQ; + } + if (wc == 0x00bf) { /* ¿=? is == */ + yylval = install_and_save_mbcs("==", c, clen); + if (nextchar('=') && nextchar('?')) { + strcat(yytext, "=?"); + return EQ; + } + return ERROR; + } if (isUnicodeOperatorCodepoint((unsigned int) wc)) { SEXP op = install_and_save_mbcs(NULL, c, clen); if (token_can_end_expression(LastToken)) { @@ -6020,6 +6032,10 @@ static int token(void) yylval = install_and_save("=="); return EQ; } + else if (nextchar('?')) { + yylval = install_and_save2("==", "=?"); + return EQ; + } else if (nextchar('>')) { yylval = install_and_save("=>"); HavePipeBind = true; @@ -6087,6 +6103,13 @@ static int token(void) strcpy(yytext, "]"); return c; case '?': + if (nextchar('=')) { + if (nextchar('?')) + yylval = install_and_save2("==", "?=?"); + else + yylval = install_and_save2("==", "?="); + return EQ; + } yylval = install_and_save("?"); return c; case '*': diff --git a/code/src/main/gram.y b/code/src/main/gram.y index b8adfdc5a83..cf39e473eb8 100644 --- a/code/src/main/gram.y +++ b/code/src/main/gram.y @@ -3640,6 +3640,18 @@ static int token(void) yylval = install_and_save_mbcs("<<-", c, clen); return RIGHT_ASSIGN; } + if (wc == 0x225f) { /* ≟ is == */ + yylval = install_and_save_mbcs("==", c, clen); + return EQ; + } + if (wc == 0x00bf) { /* ¿=? is == */ + yylval = install_and_save_mbcs("==", c, clen); + if (nextchar('=') && nextchar('?')) { + strcat(yytext, "=?"); + return EQ; + } + return ERROR; + } if (isUnicodeOperatorCodepoint((unsigned int) wc)) { SEXP op = install_and_save_mbcs(NULL, c, clen); if (token_can_end_expression(LastToken)) { @@ -3706,6 +3718,10 @@ static int token(void) yylval = install_and_save("=="); return EQ; } + else if (nextchar('?')) { + yylval = install_and_save2("==", "=?"); + return EQ; + } else if (nextchar('>')) { yylval = install_and_save("=>"); HavePipeBind = true; @@ -3773,6 +3789,13 @@ static int token(void) strcpy(yytext, "]"); return c; case '?': + if (nextchar('=')) { + if (nextchar('?')) + yylval = install_and_save2("==", "?=?"); + else + yylval = install_and_save2("==", "?="); + return EQ; + } yylval = install_and_save("?"); return c; case '*': diff --git a/code/tests/ir-unicode-lexer.R b/code/tests/ir-unicode-lexer.R new file mode 100644 index 00000000000..6bd81e66faf --- /dev/null +++ b/code/tests/ir-unicode-lexer.R @@ -0,0 +1,75 @@ +## IR Unicode lexer priority and code-point honesty. + +UTF8 <- l10n_info()[["UTF-8"]] + +if (!UTF8) { + message("SKIPPED: IR Unicode lexer tests need a UTF-8 locale") +} else { + parse1 <- function(text) parse(text = text, keep.source = FALSE)[[1L]] + + ## Keep the literal glyph beside every code point. The assertions below + ## make the comments executable documentation rather than trusting sight. + PI <- intToUtf8(0x03c0) # π + LAMBDA <- intToUtf8(0x03bb) # λ + FLORIN <- intToUtf8(0x0192) # ƒ + LEFT <- intToUtf8(0x2190) # ← + SUPER_LEFT <- intToUtf8(0x219e) # ↞ + RIGHT <- intToUtf8(0x2192) # → + SUPER_RIGHT <- intToUtf8(0x21a0) # ↠ + DIVIDE <- intToUtf8(0x00f7) # ÷ + TIMES <- intToUtf8(0x00d7) # × + COMPOSE <- intToUtf8(0x2218) # ∘ + EQUALITY <- intToUtf8(0x225f) # ≟ + INVERTED_Q <- intToUtf8(0x00bf) # ¿ + GENERIC <- intToUtf8(0x2297) # ⊗ + + stopifnot( + identical(PI, "π"), + identical(LAMBDA, "λ"), + identical(FLORIN, "ƒ"), + identical(LEFT, "←"), + identical(SUPER_LEFT, "↞"), + identical(RIGHT, "→"), + identical(SUPER_RIGHT, "↠"), + identical(DIVIDE, "÷"), + identical(TIMES, "×"), + identical(COMPOSE, "∘"), + identical(EQUALITY, "≟"), + identical(INVERTED_Q, "¿"), + identical(GENERIC, "⊗") + ) + + ## U+03C0 is not merely displayed as π here: make π a real function name + ## in parsed source and call it, so the code point, printed glyph and lexer + ## all have to agree. + syntaxEnv <- new.env(parent = baseenv()) + stopifnot(identical(parse1(PI), as.name("π"))) + eval(parse1(paste0(PI, " ", LEFT, " ", LAMBDA, "(x) x + 1")), + syntaxEnv) + stopifnot(identical(eval(parse1(paste0(PI, "(2)")), syntaxEnv), 3)) + + ## Reserved glyphs must be eaten by their explicit syntax before the + ## generic Unicode infix fallback gets a chance to classify them. + stopifnot( + identical(parse1(paste("x", LEFT, "1L")), quote(x <- 1L)), + identical(parse1(paste("x", SUPER_LEFT, "1L")), quote(x <<- 1L)), + identical(parse1(paste("1L", RIGHT, "x")), quote(x <- 1L)), + identical(parse1(paste("1L", SUPER_RIGHT, "x")), quote(x <<- 1L)), + identical(parse1(paste("1", EQUALITY, "1")), quote(1 == 1)), + identical(parse1("1 ?= 1"), quote(1 == 1)), + identical(parse1("1 =? 1"), quote(1 == 1)), + identical(parse1("1 ?=? 1"), quote(1 == 1)), + identical(parse1(paste0("1 ", INVERTED_Q, "=? 1")), quote(1 == 1)) + ) + + ## Unreserved glyphs still use the broad generic infix path. Define one + ## as an ordinary function, then prove the same glyph parses and evaluates + ## infix rather than being rejected merely because it is Unicode. + eval(parse1(paste0(GENERIC, " ", LEFT, " ", LAMBDA, + "(a, b) a + b")), syntaxEnv) + genericCall <- parse1(paste("2", GENERIC, "3")) + stopifnot( + identical(as.character(genericCall[[1L]]), GENERIC), + identical(eval(genericCall, syntaxEnv), 5) + ) +} diff --git a/code/tests/reg-encodings.R b/code/tests/reg-encodings.R index 00ccb7d4b6c..6c17965f5cc 100644 --- a/code/tests/reg-encodings.R +++ b/code/tests/reg-encodings.R @@ -31,6 +31,10 @@ stopifnot( identical(eval(parse1("1 = 2")), FALSE), isTRUE(eval(parse1("1 + 1 = 2"))), isTRUE(eval(parse1("1 == 1"))), + identical(parse1("1 ?= 1"), quote(1 == 1)), + identical(parse1("1 =? 1"), quote(1 == 1)), + identical(parse1("1 ?=? 1"), quote(1 == 1)), + isTRUE(eval(parse1("1 + 1 ?= 2"))), inherits(try(parse(text = "1 = 1 = TRUE"), silent = TRUE), "try-error") ) @@ -55,6 +59,8 @@ if (UTF8) { DIVIDE <- intToUtf8(0x00f7) TIMES <- intToUtf8(0x00d7) COMPOSE <- intToUtf8(0x2218) + EQUALITY <- intToUtf8(0x225f) + INVERTED_Q <- intToUtf8(0x00bf) stopifnot( identical(parse1(paste("x", LEFT, "1L")), quote(x <- 1L)), @@ -65,6 +71,13 @@ if (UTF8) { identical(parse1(paste0(FLORIN, "(x) x")), quote(function(x) x)) ) + stopifnot( + identical(parse1(paste("1", EQUALITY, "1")), quote(1 == 1)), + isTRUE(eval(parse1(paste("1 + 1", EQUALITY, "2")))), + identical(parse1(paste0("1 ", INVERTED_Q, "=? 1")), quote(1 == 1)), + isTRUE(eval(parse1(paste0("1 ", INVERTED_Q, "=? 1")))) + ) + syntaxEnv <- new.env(parent = baseenv()) eval(parse1(paste("ordinary", LEFT, "1L")), syntaxEnv) eval(parse1(paste("2L", RIGHT, "rightward")), syntaxEnv)