Rye 0.2 is out. We jumped from Rye 0.1.* versions because this Rye brings a major change in evaluation rules and terminology.

Major footgun

The changes remove a major “footgun” behavior related to evaluation order especially in regards to operators. It introduces a new word type dot-word, and somewhat changes (existing) op-word behavior.

Most of existing code will keep on working without changes, because we generally didn’t use the pattern that is the footgun. But it was always there, locked and loaded.

The footgun: op-words were right associative, applied right-to-left, which was particularly counterintuitive with math operators.

; Rye <= v0.1.*

12 - 6 - 4                       ; returned 10
11 + 2 > 12                      ; error: _+ doesn't accept boolean
; Rye >= v0.2.*

12 - 6 - 4                       ; returns 2
11 + 2 > 12                      ; returns true

Rye comes from the Rebol family, where consistent left-to-right operator evaluation is the norm - not C-like operator precedence rules.

Started with a patch

To fight the occasional clumsy code, I was working on a new mth dialect that uses the shunting yard algorithm to provide correct mathematical operator priority with fast evaluation and minimal “ink” and could be used inline.

if mth { 3 + 3 * 2 = 9 } { print "Yo" }     ; prints Yo

It got implemented, and I was writing a blog-post about how cool it is, trying to make a case that there is no alternative.

But in the middle of it I started questioning my arguments. I am providing a small patch, but the default will always be there, waiting for new or just a little careless users of Rye to trip on it.

I re-evaluated the basics, and what I found was more than a fix, I now think this might be the missing link that completes the circle of Rye’s evaluation mechanisms.

(mth dialect still has its uses, so it is available in Rye)

New: dot-words

Words we called op-words before (.word dot in the front) - minus the operators +, *, … are now called a dot-words. It takes first argument from the left (like op-word before), but is now left associative, evaluates left-to-right.

99 .inc                                ; returns 100
"hello" .upper                         ; returns "HELLO"
"hello world" .replace "world" "mars"  ; returns "hello mars"
2 .inc .string .concat " bears"        ; returns "3 bears"
3 .+ 4 .* 5                            ; returns 35
; prefix operators with dots and turn them into dot-words

Mental model: Dot words pin first (tight) to their left operands, creating sort of atoms (smallest expression clumps).

Changed: Op-words

Op-words are now operators +, *, ->, ++, … and words with angle brackets <concat>. And also evaluate left-to-right.

12 - 6 - 4                             ; returns 2
11 + 2 > 12                            ; returns true
3 * 4 - 5 <string> <concat> " ravens" 
; returns "7 ravens"

Mental model: op-words are more like bridges between values or said atoms. Same behaviour, different priority.

If you are new to Rye - and who isn’t :) The op-word-iness of a function is NOT bound to a word or a function. Every function (and in Rye every active element is a function print if fn …) becomes an op-word if you type it in op-word format: <print>. Operators are just op-words by default.

New priority

  • Dot-words (.word) - pin to the left value and form an atom, left-to-right
  • Op-words (+, -, <word>) - bridge two values or atoms, or connect to another bridge, left-to-right
  • Words (word) - front, takes arguments from the right, wraps pins and bridges
  • Pipe-words (|word) - wall - wait for left to fully evaluate, take the result

First dot-words, then op-words:

10 .dec                          ; returns 9
10 .inc .math/is-prime           ; returns true
12 - 6 - 4                       ; returns 2
12 + 3 = 15                      ; returns true
10 .inc - 10 .dec .dec           ; returns 3
"AB" .lower <concat> "bc" .upper ; returns abBC
4 .+ 2 * 2 .+ 3                  ; returns 30

Then words:

print 12 - 6 - 4                 ; prints 2
if 12 + 3 = 15 { print "Yo" }    ; prints Yo
372 .string <concat> upper "xp"  ; returns 372XP

Pipe-words remain a hard wall:

inc 10 * 10                      ; returns 101
inc 10 |* 10                     ; returns 110
print 12 - 6 |- 4                ; prints 6, returns 2
11 .print + 22 <print> + 33 |print
; Prints:
; 11
; 33
; 66

There’s a deeper post coming in future - probably named “The Geometry of a Language” - which will go deeper into the reasoning behind these new evaluation mechanisms, how it compares to other languages, and why this model could make code more visually exact, local and flexible.

Simpler Type Constructors

Value constructors were always just the name of the type in Rye. context, dict, list, failure, vector. But there were also to-<type>, like to-integer, to-string words that convert to that type. But the difference between constructing a type and converting to a type is not that clear. Constructors also converted to a type from their inputs. We are joining this and removing all “to-” to make this more consistent and less noisy, hopefully.

Old New
to-integer integer
to-decimal decimal
to-string string
to-uri uri
to-file file
to-char char
to-block block
to-word word
integer "42"        ; 42
string 123          ; "123"
file "data.txt"     ; %data.txt

Context Constructor Accepts Dict

Because to-context was merged with context function, it now takes both a block (evaluated) and a dict (converted directly):

; from block - expressions are evaluated
c: context { name: "Alice" age: 30 adult: age >= 18 }
c/adult  ; true

; from dict - values used directly
data: dict { "name" "Bob" "age" 25 }
c: context data
c/name   ; "Bob"

File-URI Method Naming

File-URI methods dropped the redundant File- prefix:

Old New
File-ext? Ext?
Filename? Name?
%path/to/document.pdf .Ext?   ; ".pdf"
%path/to/document.pdf .Name?  ; "document.pdf"
%path/to/document.pdf .Stem?  ; "document"
%path/to/document.pdf .Dir?   ; "path/to"

Flexible Failure Constructor

failure now accepts its arguments in any order - type, status code, and message are identified by their type, not position:

failure { 'validation 400 "Invalid input" }
failure { 400 'validation "Invalid input" }
fail { "Invalid input" 'validation 400 }

; optional parts can be omitted
failure { 'not-found "Resource missing" }
fail { 500 "Server error" }

Affects all failure construction:

1 / 0 |check { "calculation error" 501 }
; Error(501): calculation error 
;   Error: Can't divide by Zero. In builtin  _/ 

age: 20

age >= 21 |ensure { 403 "Must be 21 or above" }
; Error(403): Must be 21 or above

Migration Notes

If you’re upgrading a Rye script from 0.1.x:

  • Replace to-* constructors: to-string -> string, to-integer -> integer, etc.
  • Math with op-words now evaluates strictly left-to-right. If you had parentheses workarounds for basic arithmetic, you can probably simplify them.
  • File-URI method names: File-ext? -> Ext?, Filename? -> Name?.
  • Dotword chaining works naturally now - "hello" .upper .trim .split " " evaluates left-to-right as expected.

As always, feedback is welcome on GitHub or try it out at ryelang.org. For posts directly from the grinder, visit r/ryelang.