Skip to content

Instantly share code, notes, and snippets.

@klardotsh
Last active Nov 23, 2021
Embed
What would you like to do?

gluumy: a type-safe, minimal-ish, and hackable language targeting Lua

it's pronounced "gloomy" (or maybe "glue me"), and is spelled in lowercase, always

 _.    _  |         ._ _       ._  o  _  |_ _|_   |_   _   _  o ._   _ 
(_|   (_| | |_| |_| | | | \/   | | | (_| | | |_   |_) (/_ (_| | | | _> 
       _|                 /           _|                   _|          

gluumy is a small, "fast enough for most day to day stuff", quasi-functional (and the rest trait-and-interface-based) language that compiles to Lua, and thus should run mostly anywhere Lua 5.1 (plus some compatibility modules, see below) can. It probably won't win many benchmarks, it may or may not be most appropriate for all or any domains, and it certainly has no basis in academia nor a founder with any background in programming language design. What it lacks in those departments it tries to make up for in ease of learning, understanding, tinkering, and Just Getting Shit Done.

It takes influence from languages like Gleam, Rust, Ruby, and Lua, and aims to create a language that is small, understandable in a day or so, easy to hack on, safe, and expressive. If you're here for the latest and greatest in programming language research or to make use of your degree in theoretical mathematics (or even category theory), this is not the project you're looking for. If you've ever wanted a subset of the type system of Rust with an offshoot of ML-esque syntax and the mental-model simplicity of Lua, you might be in the right place.

Now, to toot gluumy's horn on its awesome traits and features:

  • No exceptions or nil, instead offering Result and Option types, respectively

    • It's worth noting that "no exceptions" doesn't mean foreign code wrapped by gluumy's FFI contraptions can't cause runtime panics. FFI is considered inherently unsafe for a reason - gluumy can't save you from things it can't control!
  • A small-but-useful standard library that, in general, tries to offer as close to one way to solve a problem as possible. Learn a few patterns and you should be good to go for core and std.

  • A trait-based functional-ish paradigm encouraging free functions accepting as broad of interfaces as possible as opposed to narrow member functions.

  • To complement said paradigm, a strong type inference system that often eliminates the need for type annotations entirely (indeed, much of the standard library lacks explicit annotations, and instead happily works on any inputs that fit the inferred expected shape).

  • Complementing almost all of the above, two pipeline operators (|> and |>>) to prepend and append (respectively) the results of one function to the arguments of another (those who have used Gleam, Elixir, or F# shuold feel at home with this).

  • It all becomes Lua in the end, allowing for easy portability, inspectability, and optimization (by way of alternative Lua implementations such as LuaJIT). Forget about cross-compilation woes from many languages and many of the runtime exceptions from many others.

... and as a bonus, the spec, implementation, and standard library are all Copyfree software.

This repository contains various components:

  • src/stage0 contains the bootstrapping compiler in dependency-free Lua 5.1. This is an extremely unsafe, raw translator of gluumy source to Lua source. Its output is unoptimized and only debatably readable. It also assumes all input code is type-safe. Use of stage0 is not supported for any purpose other than compiling src/compiler and any gluumy source files it may reference, notably, lib/core. Do not file bugs against stage0 unless they directly cause broken src/compiler builds. For now, the bootstrapping compiler will be retained such that the only requirement to build the gluumy compiler is a Lua 5.1 build, however there is no promise of how long this will last.

  • src/compiler, lib/compile, lib/tc, lib/lsp, lib/lint, and lib/fmt are the actually-safe and as-production-ready-as-feasible gluumy compiler, type-checking engine, language server, linter, and formatter. They are all implemented in gluumy.

  • lib/core and lib/std define the core (always present and in-scope) and standard (optional, by import) libraries, each also implemented in gluumy.

Please note that gluumy is a personal side project, mostly aimed towards developing things I want to build (which generally means command line and/or networked applications, and glue scripts). The standard library is thus only as complete as is necessary to solve those problems (and, of course, to self-host the toolchain). If you find this language interesting and choose to use it, be prepared to have to fill in holes in the standard library and/or to have to write FFI bindings and typedefs to Lua modules, and most of all, don't expect API stability across versions yet.

Dependencies

  • Any Lua compatible with the intersection of the LuaJIT-defined subset of Lua 5.2 and lua-compat-5.2. In practical terms, on most Unixes this means LuaJIT, Lua 5.2, Lua 5.1 with compat52, or anything else backwards-compatible to those APIs. Clear as mud, thanks Lua fragmentation!

Alternatives

I find gluumy to fill a somewhat unique niche within the Lua ecosystem, but you may wish to compare it against some related art in the community:

  • Teal is by far the closest relative in the ecosystem, written by @hishamhm who also brought us htop, luarocks, and various Lua libraries. Hisham's work is routinely awesome, so give it a look. Teal, just like gluumy, compiles to Lua after its type-checking stage. gluumy deviates from Teal in a few key areas:

    • Clearly, the syntax. Teal retains Lua's overall syntax style, with a few keywords and symbols added as necessary. gluumy opts for a bespoke hybrid of ML-esque, LiveScript-esque, Ruby-esque, Rust-esque, and anything else to suit my personal taste. While the syntax should be understandable to those with backgrounds in Lua plus at least one of those families, it will not feel familiar to those coming from a pure-Lua background.

    • Teal implicitly allows nil for all types, whereas gluumy lacks a nil value entirely, instead requiring the use of option types. Teal's decision was made in the spirit of maximum compatibility with existing Lua code which depends on such looseness. gluumy is not inherently compatible with existing Lua code without at least some degree of bindings and glue, and thus was able to take a stricter stance.

  • Pallene aims to be a "sister language for Lua", offering AOT compilation to dynamically-loadable native modules. It seems to target creating a more-type-safe data layer, called into by existing Lua code

-- gluumy is copyfree software with no warranty, released under the CC0-1.0
-- public-domain-esque dedication found in COPYING in gluumy's source tree, or
-- at https://creativecommons.org/publicdomain/zero/1.0/
. ./result Result
Option { T } ~>
Some => T
None
--- Maps a Boolean to an Option, where `false` becomes None, and `true` becomes
--- `when_true`. If `when_true` should be a dynamic value (i.e. requires a function
--- call), use `from_bool_with` instead.
from_bool { true when_true } -> Option.Some when_true
from_bool { false } -> Option.None
--- Maps a Boolean to an Option, where `false` becomes none, and `true` becomes
--- the return value of `when_true`. If `when_true` need not be dynamic,
--- `from_bool` is more efficient.
from_bool_with { true when_true } -> Option.Some when_true!
from_bool_with { false } -> Option.None
--- Maps an Option to a Boolean, where Some becomes `false` and None becomes
--- `true`.
---
--- For the inverse operation, see `is_none`.
is_none { Option.Some } -> false
is_none { Option.None } -> true
--- Maps an Option to a Boolean, where Some becomes `true` and None becomes
--- `false`.
---
--- For the inverse operation, see `is_none`.
is_some { Option.Some } -> true
is_some { Option.None } -> false
-- Maps an Option to its inner value, if Some, or a fallback value, if None.
-- There is no requirement at this level that the inner value of the Option and
-- the value of `fallback` are of the same type, mostly because the type system
-- currently can't express such a requirement. However, downstream callsites will
-- almost certainly expect congruence, meaning such enforcement usually happens
-- elsewhere in the callgraph anyway.
--
-- If `fallback` should be a dynamic value (i.e. requires a function call), use
-- `or_with` instead.
or { val(Option.Some) } -> val
or { Option.None fallback } -> fallback
-- TODO docs
or_with { val(Option.Some) } -> val
or_with { Option.None fallback } -> fallback!
--- Maps an Option to a Result, wrapping a Some value in Result.Ok when
--- applicable, and otherwise returning a Result.Err containing `when_none`.
must { val(Option.Some) } -> Result.Ok val
must { Option.None when_none } -> Result.Err when_none
--- Maps an Option to a Result, calling `when_some` with the wrapped Some value
--- or `when_none` with no arguments as appropriate. The result of either handler
--- are then wrapped in Result.Ok or Result.Err, respectively (meaning most callers
--- should not return a Result themselves unless nested Results are desired).
---
--- For the inverse operation, see `must_not_with`.
must_with { val(Option.Some) when_some } -> when_some val |> Result.Ok
must_with { Option.None _ when_none } -> when_none! |> Result.Err
--- Maps an Option to a Result, wrapping `when_some` in Result.Err and
--- `when_none` in Result.Ok based on the state of `it`. The inner value of the
--- Some type is ignored; to handle it, see `must` or `must_not_with` instead.
--- Most callers should not pass Results for `when_some` and `when_none` unless
--- nested Results are desired.
must_not { Option.Some when_some } -> Result.Err when_some
must_not { Option.None _ when_none } -> Result.Ok when_none
--- Maps an Option to a Result, calling `when_some` with the wrapped Some value
--- or `when_none` with no arguments as appropriate. The result of either handler
--- are then wrapped in Result.Err or Result.Ok, respectively (meaning most callers
--- should not return a Result themselves unless nested Results are desired).
---
--- For the inverse operation, see `must_with`.
must_not_with { val(Option.Some) when_some } -> when_some val |> Result.Err
must_not_with { Option.None _ when_none } -> when_none! |> Result.Ok
-- gluumy is copyfree software with no warranty, released under the CC0-1.0
-- public-domain-esque dedication found in COPYING in gluumy's source tree, or
-- at https://creativecommons.org/publicdomain/zero/1.0/
Result { T1 T2 } ~>
Ok => T1
Err => T2
-- gluumy is copyfree software with no warranty, released under the CC0-1.0
-- public-domain-esque dedication found in COPYING in gluumy's source tree, or
-- at https://creativecommons.org/publicdomain/zero/1.0/
. std.io println eprintln
. std.random
random_bool = bool
main { .. } -> get_ten_somes! |> output_result
output_result { msg(:ok) } -> println msg
output_result { msg(:err) } -> eprintln msg
maybe_something { .. } -> random_bool! |> :option.from_bool "body once told me"
--- A silly statistics-defying method that tries to build an array of 10 Some("body
--- once told me") values randomly. It will almost certainly return an error, but
--- on the off chance that RNGesus smiles upon you, it may return an ok.
--- Regardless, it makes for a nice silly example of the Result and Option types in
--- one swing.
get_ten_somes ->
:array.new_with_capacity 10
|> :iter.map maybe_something
|> :iter.find :option.is_none
|> :option.must_not
"as expected, at least one None found"
"against all odds, the array is all Somes"
-- gluumy is copyfree software with no warranty, released under the CC0-1.0
-- public-domain-esque dedication found in COPYING in gluumy's source tree, or
-- at https://creativecommons.org/publicdomain/zero/1.0/
. std.io println
--- => denotes a shape, gluumy's catch-all for a bunch of related concepts:
--- structs, interfaces, traits, bounds. They are made up of members, which must
--- have a type (explicit or implicit) and an underlying implementation to be valid
--- at runtime, however those parts may be provided in any order, allowing shapes
--- to fill the role of interfaces despite otherwise operating most like structs.
---
--- In this example, we're defining a partial shape, where a member `repr` is a
--- function accepting an unbound (meaning not used in the function body) argument
--- of the special type Self which will be expected to return the type :String
--- (unlike Ruby or Elixir, colon prefixes are just an alias to the Core library
--- rather than symbols, and desugar to `core.`). Since types are not, themselves,
--- returnable values in gluumy, the type checker knows this is a definition and
--- not an implementation, and we'll have to provide such an implementation
--- elsewhere.
ExampleRepresentable =>
repr { Self } -> :String
ExampleDebuggable =>
-- Simply because it's confusing to allow otherwise, debug-printing an object
-- requires that it be printable in non-debug form, too. This also makes a
-- convenient opportunity to show off shape interdependence. `<=` allows
-- merging in one shape definition per line, with the ultimate semantics
-- bearing much similarity to TypeScript's `&` operator.
<= ExampleRepresentable
-- This default implementation will be derived assuming its shape dependencies
-- are fulfilled, and otherwise, an override must be provided matching whatever
-- signature this returns
--
-- The first argument to any method will be of type Self, which is always
-- type-inferred (no need to annotate `self: Self` here), but in this case,
-- we're further mandating that all properties within the shape of `self`
-- fulfill ExampleDebuggable themselves. If all members of a shape fulfill
-- a shape, so does the shape itself.
--
-- If we really wanted to be explicit about Self here, we could (though the
-- linter would complain) with `self(ExampleDebuggable Self)`
repr_debug { self(ExampleDebuggable) } ->
type_name =
:reflect.type_name self
|> :option.or "Shape"
fields =
-- indentations can continue the preceeding statement, which can
-- greatly help readability (and allow splitting long function
-- calls onto multiple lines). this also reduces the need for
-- parentheses, because each line, unless followed by an indent, is
-- considered a "complete thought" and evaluates (in a one-line
-- version of this example, :shape.fields self would need to be
-- wrapped in parentheses to treat it as a function call returning
-- a single result, rather than as two separate arguments to
-- :iter.map)
:iter.map
:shape.fields self
{ ele } -> ele.repr_debug!
|> :iter.join ", "
:string.format "%s(%s)" type_name fields
Song =>
-- By declaring this, the type checker will ensure we've fulfilled the shape
-- requirements somewhere within this definition, since ExampleRepresentable
-- contains only stubs, not implementations.
<= ExampleRepresentable
artist = :String
album_artist = :Option :String
title :String
track = :Option =>
num = :Natural
total = :Natural
-- We can fulfil traits without claiming to fulfil them with the "is"
-- keyword. We'll get gnarlier error messages if the contracts ever change
-- in the future, though, so generally "is" is encouraged.
repr { (num total) } -> :string.format "Track %d/%d" num total
repr { (title artist album_artist) } ->
:string.format
"%s by %s"
title
:option.or album_artist artist
-- this prints "Wandering Star by Portishead"
main { .. } ->
Song <=>
artist = "Portishead"
title = "Wandering Star"
track =
:Option.Some
=>
num = 5
total = 7
|> println
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment