DEV Community

Tomasz Wegrzanowski
Tomasz Wegrzanowski

Posted on

100 Languages Speedrun: Episode 90: YueScript

In the previous episode I reviewed MoonScript, "CoffeeScript for Lua", which unfortunately has not seen any releases since 2015.

What I didn't know about is that its fork YueScript is actively maintained, and with some extra features.

All MoonScript examples from the previous episode also work on YueScript, so I'll try a few new things.

Lua and all Lua-based languages suffer from extremely underpowered standard library. Instead of repeating the same points again, this time I'll try to use third party modules for things which really ought to be in the standard library, like Unicode support, dealing with files and strings, and so on.

With Lua library situation is a bit more complicated, as your end goal is often running your code in some existing program (like a video game), and not all libraries will work there, depending on Lua version they use, and if library has any C code dependencies etc. So "you can just install libraries" really does not excuse Lua's inadequate standard library, even if that would sort of work for other languages.

Hello, World!

You can install YueScript with luarocks install yuescript.

To just run something we need to pass -e to YueScript, as by default it compiles to Lua without executing:

#!/usr/bin/env yue -e

print "Hello, World!"
Enter fullscreen mode Exit fullscreen mode
$ ./hello.yue
Hello, World!
Enter fullscreen mode Exit fullscreen mode

FizzBuzz

We can do it just like in MoonScript, or we can also use |> operator from Elixir to pipeline data. |> is one of the big hits, and it's making its way into more and more languages.

I'm not saying this is a particularly good use case for |>, but here it goes:

#!/usr/bin/env yue -e

for i = 1, 100
  if i % 15 == 0
    "FizzBuzz" |> print
  elseif i % 5 == 0
    "Buzz" |> print
  elseif i % 3 == 0
    "Fizz" |> print
  else
    i |> print
Enter fullscreen mode Exit fullscreen mode

Unicode

Lua treats all strings as collections of bytes, which is ridiculous in this day and age, especially as its main use case is games, which all have to get translated into a lot of languages including usually Chinese.

Lua 5.3 (which is not what every Lua platform supports) added extremely minimal UTF-8 support - literally just "UTF-8 length" and "Nth UTF-8 character", but if we're on Lua 5.3+ we can get a slightly less wrong answer:

#!/usr/bin/env yue -e

"Hello" |> utf8.len |> print
"Żółw" |> utf8.len |> print
"💩" |> utf8.len |> print

"Żółw" |> string.upper |> print
"Żółw" |> string.lower |> print
Enter fullscreen mode Exit fullscreen mode
$ ./unicode.yue
5
4
1
ŻółW
Żółw
Enter fullscreen mode Exit fullscreen mode

Even this little doesn't work on LuaJIT:

$ yue unicode.yue
Built unicode.yue
$ luajit unicode.lua
luajit: unicode.lua:1: attempt to index global 'utf8' (a nil value)
stack traceback:
    unicode.lua:1: in main chunk
    [C]: at 0x01099eb300
Enter fullscreen mode Exit fullscreen mode

Unicode with third party libraries

Let's try to use third party Unicode support with luarocks install luautf8.

#!/usr/bin/env yue -e

utf8 = require "lua-utf8"

"Hello" |> utf8.len |> print
"Żółw" |> utf8.len |> print
"💩" |> utf8.len |> print

"Żółw" |> utf8.upper |> print
"Żółw" |> utf8.lower |> print
Enter fullscreen mode Exit fullscreen mode

It works:

$ ./unicode2.yue
5
4
1
ŻÓŁW
żółw
Enter fullscreen mode Exit fullscreen mode

Unfortunately that's a C library, so it doesn't work with LuaJIT, and it's not something you can use as a modder. So existence of a third party library is not an excuse for not just fixing the standard library.

Wordle

Here's slightly improved version of Wordle from the previous episode. Using split from luarocks install string-split, which does not leave leftover empty string at the end. YueScript also allows method calls with either backslash or slightly more conventional ::.

#!/usr/bin/env yue -e

split = require "string.split"

readlines = (path) ->
  file = io.open(path)
  text = file::read("*a")
  file::close!
  split text, "\n"

random_element = (array) ->
  array[math.random(#array)]

class Wordle
  new: =>
    @words = readlines("wordle-answers-alphabetical.txt")
  report: (word, guess) =>
    for i = 1, 5
      letter = guess::sub(i,i)
      if word::sub(i,i) == letter
        io.write "🟩"
      -- non-regexp string.contains?
      elseif word::find(letter, 1, true)
        io.write "🟨"
      else
        io.write "🟥"
    io.write "\n"
  __call: =>
    word = random_element(@words)
    guess = ""
    while guess != word
      io.write "Guess: "
      guess = io.read!
      if #guess == 5
        @report(word, guess)
      else
        print "Guess must be 5 letters long"

game = Wordle()
game()
Enter fullscreen mode Exit fullscreen mode
$ ./wordle.yue
Guess: crane
🟥🟥🟩🟥🟥
Guess: smash
🟨🟥🟩🟩🟥
Guess: blast
🟥🟩🟩🟩🟥
Guess: flask
🟥🟩🟩🟩🟥
Guess: glass
🟩🟩🟩🟩🟩
Enter fullscreen mode Exit fullscreen mode

Functional Programming

MoonScript and YueScript have list comprehensions. YueScript additionaly has |> operator. Lua lacks proper print but we can install library luarocks install inspect to get something usable. With all that put together, let's try doing some functional programming:

#!/usr/bin/env yue -e

inspect = require "inspect"

nums = [i for i = 1, 10]
evens = [i for i in *nums when i % 2 == 0]

nums |> inspect |> print
evens |> inspect |> print
Enter fullscreen mode Exit fullscreen mode
$ ./functional.yue
{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }
{ 2, 4, 6, 8, 10 }
Enter fullscreen mode Exit fullscreen mode

It's a start, but not exactly impressive. We don't even have map, filter, and reduce, which would be the absolute minimum for functional programming.

More Functional Programming

Fortunately YueScript is flexible enough that we can define our own range, map, filter, reduce and so on.

#!/usr/bin/env yue -e

inspect = require "inspect"

range = (a, b) -> [x for x = a, b]
map = (list, f) -> [f(x) for x in *list]
filter = (list, f) -> [x for x in *list when f(x)]
reduce = (list, initial, f) ->
  value = initial
  for x in *list
    value = f(value, x)
  value

range(1, 10)
  |> filter (x) -> x % 2 == 0
  |> map (x) -> x * 10
  |> inspect
  |> print

range(1,10)
  |> reduce 0, (a, b) -> a + b
  |> print
Enter fullscreen mode Exit fullscreen mode
$ ./functional2.yue
{ 20, 40, 60, 80, 100 }
55
Enter fullscreen mode Exit fullscreen mode

This code might seem trivial, but if you look at generated Lua, you'll see some of the value YueScript provides:

local inspect = require("inspect")
local range
range = function(a, b)
    local _accum_0 = { }
    local _len_0 = 1
    for x = a, b do
        _accum_0[_len_0] = x
        _len_0 = _len_0 + 1
    end
    return _accum_0
end
local map
map = function(list, f)
    local _accum_0 = { }
    local _len_0 = 1
    for _index_0 = 1, #list do
        local x = list[_index_0]
        _accum_0[_len_0] = f(x)
        _len_0 = _len_0 + 1
    end
    return _accum_0
end
local filter
filter = function(list, f)
    local _accum_0 = { }
    local _len_0 = 1
    for _index_0 = 1, #list do
        local x = list[_index_0]
        if f(x) then
            _accum_0[_len_0] = x
            _len_0 = _len_0 + 1
        end
    end
    return _accum_0
end
local reduce
reduce = function(list, initial, f)
    local value = initial
    for _index_0 = 1, #list do
        local x = list[_index_0]
        value = f(value, x)
    end
    return value
end
print(inspect(map(filter(range(1, 10), function(x)
    return x % 2 == 0
end), function(x)
    return x * 10
end)))
return print(reduce(range(1, 10), 0, function(a, b)
    return a + b
end))
Enter fullscreen mode Exit fullscreen mode

One downside of YueScript is that you get really bad error messages if you forget that * in the list comprehensions. And overall quality of error messages is not great.

YAML style objects

YueScript provides nice syntax for YAML-style objects. This might seem trivial, and most programs don't really need that, but in specifically game mods, there's so much data of this kind:

#!/usr/bin/env yue -e

inspect = require "inspect"

person =
  name: "Patricia"
  age: 42
  occupation: "programmer"
  hobbies:
    * "programming"
    * "biking"
    * "swimming"
  friends:
    * "John"
    * "Mary"
    * "Bob"
    * "Sally"

person |> inspect |> print
Enter fullscreen mode Exit fullscreen mode
$ ./yaml.yue
{
  age = 42,
  friends = { "John", "Mary", "Bob", "Sally" },
  hobbies = { "programming", "biking", "swimming" },
  name = "Patricia",
  occupation = "programmer"
}
Enter fullscreen mode Exit fullscreen mode

Here's another one:

#!/usr/bin/env yue -e

inspect = require "inspect"

-- data from Factorio wiki
fireArmor =
  name: "fire-armor"
  icons:
    * icon: "fireArmor.png"
      tint: {r: 1, g: 0, b: 0, a: 0.3}
  resistances:
    * type: "physical",
      decrease: 6,
      percent: 10
    * type: "explosion",
      decrease: 10,
      percent: 30
    * type: "acid",
      decrease: 5,
      percent: 30
    * type: "fire",
      decrease: 0,
      percent: 100

fireArmor |> inspect |> print
Enter fullscreen mode Exit fullscreen mode
$ ./yaml2.yue
{
  icons = { {
      icon = "fireArmor.png",
      tint = {
        a = 0.3,
        b = 0,
        g = 0,
        r = 1
      }
    } },
  name = "fire-armor",
  resistances = { {
      decrease = 6,
      percent = 10,
      type = "physical"
    }, {
      decrease = 10,
      percent = 30,
      type = "explosion"
    }, {
      decrease = 5,
      percent = 30,
      type = "acid"
    }, {
      decrease = 0,
      percent = 100,
      type = "fire"
    } }
}
Enter fullscreen mode Exit fullscreen mode

Macros

YueScript also has simple macro system, but it's all string-based, and YueScript is whitespace-sensitive, so I really can't see how it could work except for the most trivial macros.

#!/usr/bin/env yue -e

macro maybe = (code) -> "#{code} if math.random() < 0.5"

for n = 1,10
  $maybe print n
Enter fullscreen mode Exit fullscreen mode
$ ./macro.yue
1
3
6
8
10
Enter fullscreen mode Exit fullscreen mode

And a few other things

To save you having to check the MoonScript episode, here are some examples that work perfectly well in YueScript as well:

#!/usr/bin/env yue -e

fib = (n) ->
  if n <= 2
    1
  else
    fib(n-1) + fib(n-2)

for n = 1,20
  print "fib(#{n})=#{fib(n)}"
Enter fullscreen mode Exit fullscreen mode
#!/usr/bin/env yue -e

class Person
  new: (@name, @surname, @age) =>
  __tostring: =>
    "#{@name} #{@surname}"

maria = Person("Maria", "Ivanova", 25)
print "#{maria} is #{maria.age} years old"
Enter fullscreen mode Exit fullscreen mode
#!/usr/bin/env yue -e

class Vector
  new: (@x, @y) =>
  __tostring: => "<#{@x},#{@y}>"
  __add: (other) => Vector(@x + other.x, @y + other.y)
  __eq: (other) =>
    @__class == other.__class and @x == other.x and @y == other.y

a = Vector(20, 60)
b = Vector(400, 9)
c = a + b

print a
print b
print c
print c == Vector(420, 69)
Enter fullscreen mode Exit fullscreen mode

Should you use YueScript?

I didn't have terribly high expectations coming in, but this is the best version of Lua I've seen so far. YueScript can't fix fundamental issues with Lua (maybe some Lua 6 will?), but between much nicer syntax, and copious use of third party libraries for stuff Lua should have included, you can have a drastically better experience than coding in plain Lua. Unfortunately many such libraries require compiling C code and won't run in games.

I think as a game modder, you should use YueScript instead of Lua for your mods.

As for embedding in new apps, I don't think Lua VM with any language is a great choice, due to major missing components.

One thing YueScript really should do is get VSCode plugin. There's one for MoonScript (which I used while writing this episode), so it shouldn't be much work to just tweak it a bit.

An even more interesting thing would be some lua2yue reverse compiler. js2coffee was my favorite tool back when I was using CoffeeScript. That however, would be a fairly big project.

Code

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

Code for the YueScript episode is available here.

Latest comments (5)

Collapse
 
romeerez profile image
Roman K • Edited

I've been thinking for a while why this fundamental poo issue of "💩".length == 2 is there for ages in many languages

  • arguably, it's not a big issue, of course sometimes it may be a problem. But for some reason for many years in JS community nobody speaks about this problem, like there is no problem at all, and in my experience I didn't encounter on this as if it was a problem, despite of making various text boxes, sometimes rich text editors.

  • changing how string works will break backward compatibility, and break it really hard, so this cannot be changed in already existing language

I never had any problem before with counting chars. But it really sucks, after thinking. If we take database column with limited size, database can count poo as 4 bytes or as 1 char (I tested length('💩') and char_length('💩')). Then why the heck it's 2 in JS and so many languages including Lua? This can easily cause little unpleasant bugs when saving to db.

Collapse
 
taw profile image
Tomasz Wegrzanowski

None of the issues I mention are individually big and you can absolutely work around them, it's just the big theory behind this series that if a language has a lot of issues with extremely basic stuff, there will be a lot more coming for more complex stuff and you'll spend more time working around issues than coding the actual thing. Incidental complexity will eat you.

Maybe there are languages which break this pattern (a lot of issues early but totally amazing later; or amazing early but huge problems later), but tbh I'm not sure which languages could be described this way. (unless you count installation issues, like a lot of very nice languages are huge pain to install on Windows; so I don't mention that).

Ruby 1➡️2 and Python 2➡️3 took the pains of breaking backwards compatibility to support Unicode properly, and it was definitely worth it.

Collapse
 
romeerez profile image
Roman K • Edited

I'm glad you found and even liked YouScript!

Another thing I love about Lua, but didn't mentioned in first comment under Lua overview, tables can accept anything as a key, as opposed to only strings in JS.

Unfortunately that's a C library, so it doesn't work with LuaJIT, and it's not something you can use as a modder.

LuaJIT has compatible with Lua 5.1 C API, I even wrote bit of C for it back in days. Also LuaJIT has ffi to access C functions from Lua side. So if doesn't work - hmm, strange, should work.

Are you sure modders cannot use C or C++? Popular game engines, like Unity and others, can use C.

Collapse
 
taw profile image
Tomasz Wegrzanowski

I tried to install it with luarocks --lua-version 5.1 install luautf8 but that didn't work with LuaJIT. There could be a way to do this.

Game developers can use C or C++, but most games would not allow modders to distribute compiled binaries with mods on any mod portal for security reasons, and many games are DRM-locked and really hate when people mess with their binaries to make such things work. Lua or such are sandboxed so it's fine. It all varies from game to game, but I'd expect that pure-lua libraries would work but C libraries usually wouldn't.

Collapse
 
taw profile image
Tomasz Wegrzanowski

Also most languages allow arbitrary hash keys. ES6+ JavaScript as well, you just need to use Map not {}.