Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions .github/workflows/lexer.yml
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion R-in-the-Mac-Terminal.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
4 changes: 2 additions & 2 deletions R-on-Linux.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ Then try:

```r
answer ← 8 ÷ 2
answer = 4
answer 4
```

The final line should print:
Expand All @@ -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.

Expand Down
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
### fn λ → ÷ =
### fn λ → ÷

→ and ← can now be used for assignment, not just `<-` and `->`.

Expand All @@ -13,7 +13,7 @@ Existing R spelling remains available: `<-`, `<<-`, `->`, `->>`, `==`, and `func

```r
answer ← 8 ÷ 2
answer = 4
answer 4
```

The last line returns
Expand All @@ -22,7 +22,7 @@ The last line returns
[1] TRUE
```

because = now means equal.
because ≟ tests equality.



Expand All @@ -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 =.

Expand All @@ -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))
Expand Down
4 changes: 2 additions & 2 deletions RStudio-on-Mac.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions RStudio-on-Windows.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
23 changes: 23 additions & 0 deletions code/src/main/gram.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 '*':
Expand Down
23 changes: 23 additions & 0 deletions code/src/main/gram.y
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 '*':
Expand Down
75 changes: 75 additions & 0 deletions code/tests/ir-unicode-lexer.R
Original file line number Diff line number Diff line change
@@ -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)
)
}
13 changes: 13 additions & 0 deletions code/tests/reg-encodings.R
Original file line number Diff line number Diff line change
Expand Up @@ -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")
)
Expand All @@ -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)),
Expand All @@ -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)
Expand Down
Loading