Riverstone is a planned programming language that looks similar to Lua or Julia but with a safe, functional design based on Gleam and Roc, and a few unique features.
It is still early in the syntax design stage and does not have a compiler.
The first compiler (if it gets made any time soon) will most likely be written in Zig and target Roc, but compilers will hopefully eventually exist to target some of the following too: Gleam or Javascript and Elixir, Python or Xonsh, Kotlin, C# or F#, F*, Go, D and binary. Likely not all programs that compile to one language will compile to all of the others.
It's also possible that the compiler could target Javascript directly, and target Elixir, instead of targetting Gleam.
Riverstone is a superset of two subset languages: roughstone and shinystone.
Roughstone includes shinystone as a proper subset, so you can write shinystone in a .rough file but not roughstone in a .shiny file, because roughstone also includes some things that aren't allowed in shinystone, Gleam or Roc, such as top level execution in the main.rough file.
"Hello, world!"
print! "Hello, world!"
"Hello, world!" |> print!
Hello, World!
Roughstone and shinystone ideally will compile directly to the non-riverstone target language, and roughstone will also compile to shinystone. Shinystone will be designed such that Gleam and Shinystone can each transpile to the other. Ideally shinystone and at least one other target language such as Roc will each transpile to the other.
Defining any global constant, including a function, uses both the "define" keyword and the "::" operator for easier readbility than other language protocols.
define OTTER_STRING :: "otter"
define DEER_STRING :: "deer"
Value names (excluding function names) in shinystone must contain the type, prefixed by an underscore. This is used for explicit type checking, and increases readability when subsequently referenced. After type checking, these type annotations can be optionally stripped on compilation.
Non-function global constant names in shinystone must use all caps.
It is encouraged to do this in roughstone too.
- Declaring a new variable requires the
declarekeyword and an<-assignment. - Shadowing an existing variable requires the
newkeyword and an<-assignment.
declare chu_string <- "Pikachu"
new chu_string <- "Raichu"
declare count_int <- 0
new count_int <- prior count_int + 1 // new count_int == 1
In shinystone, variable names must use lower case.
Using <- reduces ambiguity and reduces operator overloading.
While declare and new handle variables that can be shadowed, Riverstone offers stipulate for defining strictly immutable local temporary constants. These are bound using the <: operator.
Unlike variables declared with <-, a local temporary constant defined with <: cannot be shadowed or redeclared within the same scope.
stipulate Paralysis_Float <: 0.25
In shinystone, local temporary constant names must capitalise the first letter of each word and not every letter.
Other than returns, whitespace is not meaningful, since colons and keywords determine the beginnings and ends of blocks, but the compiler will enforce formatting.
Formatting rules are stricter for shinystone, which uses indentation for delimiting most blocks but empty lines for public and private blocks. In roughstone, these are interchangable.
In shinystone, public: blocks are not indented; instead, they are separated by empty lines, and must include an explicit end public (also separated by an empty line).
Roughstone Examples:
define main() ::
print! "Hello, world!"
end main()
public:
define main() :: Status(Ok, Err)
"Hello, world!" |> print!
end main()
end public
Hello, World!
Shinystone Examples:
public:
define main() :: Status(Ok, Err)
"Hello, world!" |> print!
end main()
end public
public:
define main() :: Status(Ok: string, Err: string)
print! "Hello, world!"
if Status is:
Ok: "Status: Ok"
Err: "Status: Err"
end if
end main()
end public
Hello, World!
Riverstone allows both symmetrical 1-based positional indexing and symmetrical 0-based vector indexing, with a clear syntax that aims to reduce the risk of off-by-one errors.
- 1-based positional indexing:
[1, 2, 3, ... , -3, -2, -1] - 0-based vector indexing:
[s, s+1, s+2, ... , e-2, e-1, e]
The abbreviations s and e are short for "start" and "end" and are equivalent to 1 and length(list_name) under the hood, so effectively one-based indexing is being used under the hood.
declare list_string_var <- ["a", "b", "c", "d"]
// 1-based positional access
print! list_string_var[1] // "a"
print! list_string_var[4] // "d"
print! list_string_var[-1] // "d"
// 0-based vector access
print! list_string_var[s] // "a"
print! list_string_var[s + 1] // "b"
print! list_string_var[s + 2] // "c"
print! list_string_var[e - 1] // "c"
print! list_string_var[e] // "d"
Inclusive slicing uses : and exclusive slicing uses ... Using these for 1-based indexing and 0-based indexing respectively is enforced, which along with the different index syntax will reduce the risk of off-by-one errors.
define abcd_stringlist :: ["a", "b", "c", "d"]
print! abcd_stringlist[1:2] // ["a", "b"]
print! abcd_stringlist[2:4] // ["b", "c", "d"]
print! abcd_stringlist[1:-1] // ["a", "b", "c", "d"]
print! abcd_stringlist[s..s+2] // ["a", "b"]
print! abcd_stringlist[s+1..s+4] // ["b", "c", "d"]
print! abcd_stringlist[s..e+1] // ["a", "b", "c", "d"]
Unlike in Gleam, the are some overloaded operators in riverstone, but it is restricted to three: +, -, and * operators. There operators are used for both integers and floats, but can only be used for this. Mixing int and float types in a single operation remains prohibited.
Additional operator overloading is prohibited.
declare i_int_var <- 1 + 2
declare f_float_var <- 1.0 + 2.0
define execute("echo " <> message_string) ::
print! "Echoing: " <> message_string
end execute()
define execute("warn " <> warning_text_string) ::
print! "!!! WARNING: " <> warning_text_string <> " !!!"
end execute()
Riverstone hides target-specific import statements. The compiler identifies and injects necessary native modules based on usage.
public:
define main() ::
string.reverse("gnirts") |> print!
end main()
end public
All data structures are immutable. When you use new to shadow a variable, you are creating a new binding, not mutating the underlying data.
define original_intlist :: [1, 2, 3]
define extended_intlist :: [0, ..original_intlist]
Riverstone is statically typed and requires homogeneous lists. The type-in-name requirement helps the developer track these types visually.
declare names_stringlist_var <- ["Alice", "Bob"]
// new names_stringlist_var <- [1, ..prior names_stringlist_var] (This will not compile)
The language uses Option(Some, None) and Status(Ok, Err) (Result) types to handle errors and the absence of values. Pattern matching is used to safely extract values.
public:
define divide(numerator_float, denominator_float) :: Status(Ok: int, Err: string)
if denominator_int is:
0: Status(Err: "Division by zero")
_: Status(Ok: numerator_float / denominator_float)
end if
end divide()
define main() :: Status(Ok: int, Err: string)
declare calculation_float_var <- divide(10, 2)
if calculation_float_var is:
Ok: print! "Success!"
Err: print! "Error!"
end if
end main()
end public