DEV Community

Tomasz Wegrzanowski
Tomasz Wegrzanowski

Posted on

100 Languages Speedrun: Episode 70: Wren

Wren is an small language meant for embedding that aims to displace Lua before JavaScript does it.

It's so committed to the embedding use case, that it doesn't come with any way to run it in the default package (brew install wren) - that one is just an embeddable library. To actually run it you need to brew install wren-cli, or write an app embedding wren. I think that might be going a bit too far. As a consequence of this philosophy, it has extremely minimal standard library, making it not really suitable for standalone use.

Its syntax feels about halfway between Ruby and JavaScript, which is totally reasonable.

It doesn't seem to be doing anything particularly innovative, but its main competitors are Lua and non-browser JavaScript, so it could win by just having fewer issues.

Hello, World! with Wren CLI

We can use "Wren CLI" to print Hello, World:

#!/usr/bin/env wren_cli

System.print("Hello, World!")
Enter fullscreen mode Exit fullscreen mode
$ ./hello.wren
Hello, World!
Enter fullscreen mode Exit fullscreen mode

It also provides a REPL, with ASCII art wren:

$ wren_cli
\\/"-
 \_/   wren v0.4.0
> 2+2
4
> "Hello, World!"
Hello, World!
>
Enter fullscreen mode Exit fullscreen mode

Embedded Wren

Wren's main focus is being embedded, and it has C API for embedding Wren in your C application. As it's all C API, you could also create a bridge between Wren and pretty much any other language. It's a real thing, I even used one between Ruby and Lua once for a build script for some Factorio mods.

Anyway, I won't be doing any embedding in this episode, and I'll treat Wren CLI as a representative environment where Wren code might live.

Unicode

Wren REPL will outright refuse to accept any non-ASCII characters, like if you try to write some accented characters in a string, this happens:

$ wren_cli
\\/"-
 \_/   wren v0.4.0
> "Unhandled key-code [dec]: 196
> "
Enter fullscreen mode Exit fullscreen mode

I usually test how languages handle Unicode conversion to upper or lower case, but Wren doesn't have any such functions.

Wren makes some really unusual choices with how it handles UTF-8 and bytes:

#!/usr/bin/env wren_cli

var name = "Alice"
System.print("Hello, %(name)")

System.print("Lengths are codePoints by default:")
System.print("Żółw".count)
System.print("🍰".count)

var s = "Żółw"
System.print("Bytes vs codePoints counts:")
System.print("%(s) - %(s.bytes.count) bytes")
System.print("%(s) - %(s.codePoints.count) code points")

for (i in 0..6) {
  System.print("s[%(i)] = %(s[i])")
  System.print("s[%(i)] byte = %(s.bytes[i])")
  System.print("s[%(i)] code point = %(s.codePoints[i])")
}
Enter fullscreen mode Exit fullscreen mode
$ ./unicode.wren
Hello, Alice
Lengths are codePoints by default:
4
1
Bytes vs codePoints counts:
Żółw - 7 bytes
Żółw - 4 code points
s[0] = Ż
s[0] byte = 197
s[0] code point = 379
s[1] = �
s[1] byte = 187
s[1] code point = -1
s[2] = ó
s[2] byte = 195
s[2] code point = 243
s[3] = �
s[3] byte = 179
s[3] code point = -1
s[4] = ł
s[4] byte = 197
s[4] code point = 322
s[5] = �
s[5] byte = 130
s[5] code point = -1
s[6] = w
s[6] byte = 119
s[6] code point = 119
Enter fullscreen mode Exit fullscreen mode

Step by step:

  • variables are declared with var
  • string interpolation uses %() - I keep complaining about how every language now has string interpolation, but every single one decided to use different syntax for it, and well, Wren picked yet another one.
  • Wren has JavaScript-style loops with Ruby-style ranges, so we can do for (i in 0..6) and it will iterate from 0 to 6
  • string.count returns correct UTF-8 character count
  • you can access bytes for a string with string.bytes - or code points with string.codePoints

And the wild part is how indexing works:

  • even though s.count returns number of characters, s[i] uses byte indexes
  • s.bytes acts like a normal array of bytes, [197, 187, 195, 179, 197, 130, 119], s[2] meant byte at position 2 (counting naturally from 0)
  • s.codePoints returns array of codePoints, but it has a bunch of -1s inserted to align it with byte position, so s[2] means "codepoint at byte position 2", not "codepoint at codepoint position 2".
  • s.codePoints is [379, -1, 243, -1, 322, -1, 199]
  • s[i] returns a character (not byte), at byte position i - or garbage if that is not a valid start of a character

Julia is the only language I know which does something similar. I understand that this solution offers good performance and simplicity, but it must be a huge pain to actually use.

Lists

#!/usr/bin/env wren_cli

var a = [1, 2, 3, 4, 5]

System.print([1, 2, 3])
System.print(1..3)
System.print(a.map{|i| i*2})
System.print(a.map{|i| i*2}.toList)
System.print(a.where{|i| i%2 == 1}.toList)
System.print(a.reduce{|i,j| i+j})

System.print(a == a)
System.print([] == [])
Enter fullscreen mode Exit fullscreen mode
$ ./lists.wren
[1, 2, 3]
1..3
instance of MapSequence
[2, 4, 6, 8, 10]
[1, 3, 5]
15
true
false
Enter fullscreen mode Exit fullscreen mode
  • lists have standard syntax []
  • they support .map, .reduce, and .where with Ruby-style blocks - languages are slowly converging towards the same names for these, but as you can see from where, we're not quite there yet
  • .map and .where return MapSequence not a list, so once you're done you need extra .toList
  • Wren does not have structural == - it only works on primitive types, on lists it just checks for object identity, two lists with identical contents will not == each other

Maps

#!/usr/bin/env wren_cli

var person = {
  "name": "Alice",
  "surname": "Smith",
  "age": 25,
}

System.print("%(person["name"]) %(person["surname"]) is %(person["age"]) years old")

for (prop in person) {
  System.print("Person's %(prop.key) is %(prop.value)")
}

System.print({} == {})
Enter fullscreen mode Exit fullscreen mode
$ ./maps.wren
Alice Smith is 25 years old
Person's name is Alice
Person's surname is Smith
Person's age is 25
false
Enter fullscreen mode Exit fullscreen mode

Maps behave pretty much as you'd expect. Iteration returns objects with .key and .value, in undetermined order, which is a nice API.

Unfortunately Wren doesn't have structural == for Maps either.

Classes

#!/usr/bin/env wren_cli

class Point {
  x { _x }
  y { _y }
  construct new(x,y) {
    _x = x
    _y = y
  }
  toString { "Point(%(_x), %(_y))" }
  +(other) { Point.new(_x + other.x, _y + other.y) }
  ==(other) { other.type == Point && x == other.x && y == other.y }
}

var a = Point.new(400, 60)
var b = Point.new(20, 9)
var c = a + b
var d = Point.new(420, 69)

System.print(a)
System.print(b)
System.print(c)

System.print(c == null)
System.print(c == d)
System.print(c != d)
Enter fullscreen mode Exit fullscreen mode
$ ./classes.wren
Point(400, 60)
Point(20, 9)
Point(420, 69)
false
true
true
Enter fullscreen mode Exit fullscreen mode

Step by step:

  • like most things, syntax for classes is a mix between JavaScript and Ruby
  • instance variables have _ sigil
  • Wren has getters .x and zero argument methods .clear() as separate things - it recommends to use () form if method has side effects - that looks like a fairly unique design decision
  • you can override toString and various operators like +
  • by default == only compares object identity
  • we can override == - interestingly that doesn't automatically override != so here we have simultaneously c == d (using overriden structural ==) and c != d (using default object identity). Ouch

Should you use Wren?

The language is brutally minimalistic. It also makes a lot of quirky choices, but each one seems to be motivated by seeking implementation simplicity and performance, traits that matter for its embedded use case.

So Wren gets a hard no from me for standalone use, but that's likely something even its creators would agree with.

For embedded use case, it actually looks like a very reasonable choice. I didn't really cover its C API, but it seems small and practical, and it compares quite well to its main competitors Lua and non-browser JavaScript, as well as other alternatives like Tcl, Guile, creating your application-specific language, or embedding a whole general purpose language like Ruby or Python.

I think Wren is a well designed language given tradeoffs it decided to make.

As Wren is still pre-1.0, here's what I'd recommend it to change, without compromising its goals:

  • have some kind of module system for optional code, like a module with common string operations - not every application needs them, but it's better to make them available if you do
  • really reconsider making == work as structural equality (or at least add === or something that does full structural equality)
  • fix Wren CLI to support non-ASCII input
  • maybe link == with !=? That was definitely an unexpected quirk

Code

All code examples for the series will be in this repository.

Code for the Wren episode is available here.

Top comments (0)