A fictional language inspired by TypeScript, Rust, FrTime, CoffeeScript, Svelte and Dart, that compiles to JavaScript, WebAssembly and Rust and that I will totally make once I have the time.
Like Rust, Gunpowder uses runtime constants by default, instead of variables. You can declare them using the let
keyword.
let a = 4;
a = 5; // err
Shadowing is fully supported.
let a = 4;
let a = a + 1;
a // 5
Types can be declared using a colon.
let a: i32 = 2;
let b: boolean = true;
The reason for prefering shadowed runtime constants over variables is that they let you incrementally refine your values while still being 100% strictly typed.
let a = "-42";
let a = i32.parse(a);
let a: u32 = a.abs();
Standard C-like if-else
.
let a = 1;
if a > 0 {
alert("Huh");
} else if a < 0 {
alert("Heh");
} else {
alert("Ahaa");
}
The ternary operator is represented by the if
, else
and then
keywords.
let a = 1;
let b = if a > 0 then 2 else 3;
b; // 2
The logical operators are not
, and
, or
and xor
.
if not foo and bar or qux { }
There is an unless
keyword equivalent to if not (...)
.
unless a > b { a = -a; }
Primitives include numbers and booleans (taken straight out of JavaScript), and strings. String literals are double-quoted, support escaping and interpolation using ${foo}
.
let a = 2;
let b = 1_000;
let c = true;
let d = "world";
let e = "Hello, ${d}";
Lists and dictionaries have a similar syntax with square brackets. Dictionaries use the thin arrow ->
to separate keys and values.
let list = [1, 2, 3];
let dict = [foo -> 1, bar -> 2];
list[0] // 1
dict["foo"] // 1
They can be combined into a list-dict, a collection with both numbered and named keys.
let listdict = [1, 2, 3, foo -> 1, bar -> 2];
listdict[0] // 1
listdict["foo"] // 1
Lists also support spread syntax ...foo
and collection if. In these contexts, unless
is usasble, as well as if
.
let a = [7, 8, 9];
let b = [
1, 2, 3,
"a" if 1 > 0,
"b" if 1 < 0,
6,
...a,
10,
...a if 1 < 0
];
a // [1, 2, 3, "a", 6, 7, 8, 9, 10]
The same applies to dictionaries.
let a = [foo -> 1, bar -> 2];
let b = [
qux -> 3 if 1 > 0,
xyzzy -> 42 if 1 < 0,
...a
];
b // [qux -> 3, foo -> 1, bar -> 2]
Collections are immutable by default. To make a mutable collection literal, use the mut
keyword.
let a = mut [1, 2, 3];
a.pop(); // 3
let b = mut [foo -> 1, bar -> 2];
b.delete("foo"); // true
Literals have a special type (IntegerLiteral
, DecimalLiteral
, CollectionLiteral
, ...) but at compile time they can be implicitly cast to the corresponding runtime times. For example the literal 1
can be assigned to a 32bit integer, a double as well as an arbitrary precision complex number. The same applies to collections, which might end up as arrays, vectors, matrices, hash maps etc. all depending on the context.
let a: i32 = 1;
let b: f64 = 1;
let c: BigComplex = 1;
let foo: i32[2] = [1, 2];
let bar: Set<f64> = [1, 2];
let qux: HashMap<i32, f64> = [1->2, 3->4];
There is also the trilean
type for three-valued logic. It accepts the values true
, false
and unknown
, hence boolean
is a proper subtype of trilean
. The logical operators not
, and
, or
and xor
can also be applied to trileans.
not unknown == unknown
unknown or true == true
unknown or false == unknown
unknown or unknown == unknown
unknown and true == unknown
unknown and false == false
unknown and unknown == unknown
unknown xor true == unknown
unknown xor false == unknown
unknown xor unknown == unknown
If you really need a variable – and if you'll write idiomatic Gunpowder, you'll only need it rarely – you can use the var
keyword.
let cond = Random.boolean();
var message = "Hello";
if cond {
message = "${message} and welcome";
}
let message = "${message}!";
// message is no longer a variable
On the other hand, if you want a true compile-time constant, use the const
keyword.
const THREE = 3;
const TWO = 2;
const FIVE = THREE + TWO;
let one = 1;
const FOUR = THREE + one; // error! result depends on a runtime value
Gunpowder supports basic number types (like signed & unsigned integers and floats of various lengths), as well as arbitrary-precision numbers, complex numbers, matrices etc. The usual arithmetic operators (+
, -
, *
, /
) are available with one caveat: the division of two integers always produces a fraction.
let x: i32 = 1;
let y: i32 = 2;
let z: i32 = x / y; // error!
let z = x / y; // success, but z has type Fraction
To perform Euclidean division (division rounded towards zero) use a back slash.
let x: i32 = 3;
let z = x \ 2; // 1 of type i32
Modulo operation uses the keyword mod
and will return the non-negative remainder after division, ie. the Euclidean modulo.
let x = 1 mod 3; // 1
let x = 2 mod 3; // 2
let x = 3 mod 3; // 0
let x = -1 mod 3; // 2
It is always true that x mod y == x mod -y
and x/y == x\y + (x mod y)/y
. The usual truncated modulo (%
operator in Rust and JavaScript) is available using the tmod
math function.
TODO: Will exponentiation be **
or ^
?
Generally, arithmetic operations return a new freshly-constructed value – even if it's a large matrix. If you instead want to make an in-place modification of a mutable value, use arithmetic assignment operators (+=
, -=
, *=
, /=
). These operators cannot guarantee that the value won't be copied, but they'll try.
let M: mut Matrix<f64, 2, 2> = [[1,2],[3,4]];
let N: Matrix<f64, 2, 2> = [[2,0],[0,2]];
M *= N; // in-place multiplies M by N
Pointwise operations on matrices use dot-prefixed operators: .+
, .-
, .*
, ./
, .+=
, .-=
, .*=
, ./=
.
let M: mut Matrix<f32, 2, 2> = [[1,2],[3,4]];
M .*= 2;
M .- 1 // [[1,3],[5,7]]
Essentially taken straight out of Rust: while
, loop
, for
. Similar to unless
, the until
loop means exactly the same as while not (...)
.
You can use continue
and break
to continue with the next cycle or stop the loop, respectively. In order to continue or break a nested loop, you can use named blocks.
var result: i32;
for row in rows #outer {
for col in row {
if someCondition(col) {
result = col;
break #outer;
}
}
}
You can use the thin arrow ->
to return values from blocks of code.
let eleven = for i in range(1, 100) { if (i > 10) -> i };
To return from a nested block, use named blocks again – this time the syntax is #label -> value
.
let result =
for row in rows #outer {
for col in row {
if (someCondition(col)) #outer -> col;
}
}
Syntax similar to TypeScript, with a few important differences. Instead of function
, the keyword is fn
. First parameters are positional, separated by commas. Then, you can optionally write a semicolon and a list of named parameters. Optional parameters with a default value are marked by an equal sign followed by the default value. Optional parameters without a default value are marked with a question mark right after the identifier.
fn foo (
a: string, b: i32 = 2, c?: i32;
bar?: string, qux?: boolean
) {
console.log(a, b, c, bar, qux);
}
foo("a") // "a", 2, none, none, none
foo("b", 1, 3) // "b", 1, 3, none, none
foo("c", qux -> true) // "c", 2, none, none, true
foo("d", 4, 5, bar -> "e", qux -> false) // "d", 4, 5, "e", false
Both named and positional parameters accept rest parameters, denoted using three periods and willing to accept zero or more parameters in their place. They work the same way as args
and kwargs
in Python.
fn foo (a: i32, ...b: i32[]; c?: f32, ...d: f32[]) { console.log(a, b, c, d); }
foo(); // err
foo(1); // 1, [], none, []
foo(1, 2, c->3); // 1, [2], 3, []
foo(1, foo->2, bar->3) // 1, [], none, [foo->2, bar->3]
The return type of functions defaults to void
, but can be specified using a colon again. To return a value from a function you can use the return
keyword.
fn foo (a: f32): f32 {
return a * 2;
}
All parameter values are (deeply) readonly by default. To pass a mutable parameter, use the mut
keyword at both function declaration and function call.
let a = [1, 2, 3];
let b = mut [1, 2, 3];
fn foo (c: i32[]) {
c.pop(); // err
}
fn bar (c: mut i32[]) {
c.pop(); // ok
}
bar(b); // err
bar(mut b); // ok
bar(mut a); // err
Same as in Rust, you can't pass a mutable reference unless nobody else is using the variable.
let a = mut [1, 2, 3];
fn first (a: mut i32[]): i32 { a.shift() }
fn add (x: i32, y: i32) { x + y }
let b = add(a[0], first(mut a)); // err
let f = first(mut a);
let b = add(a[0], f); // ok
TODO: Think about functions optionally weakening this requirement with a readonly
or ro
keyword – that would indicate that the function doesn't mutate the input but also doesn't expect it to be immutable.
TODO: Think about how functional this is with garbage-collected data.
Some functions might want to take ownership of a variable, so that they can transform it into something else without needing to copy it. (In Rust, this concept is called move.) This can make your code more efficient, but it means you won't be able to use the original variable after that. This transaction is denoted using the take
and give
keywords.
fn matrixToSortedArray(take m: mut Matrix<f32>): f32[][] {
let arr = m.data();
for row of arr {
row.sort();
}
return arr;
}
let m: mut Matrix<f32> = [[2,1],[4,3]];
let a = matrixToSortedArray(give m);
a // [[1,2],[3,4]]
m // err
If you want to use a function that takes a parameter, but don't want to lose it yourself, you can explicitly create a copy of it using the copy
keyword.
let m: mut Matrix<f32> = [[2,1],[4,3]];
let a = matrixToSortedArray(copy m);
a // [[1,2],[3,4]]
m // Matrix[[2,1],[4,3]]
TODO.
Lambda functions use the fat arrow =>
.
let f = x: number => x;
let g = (a: number, b: number) => a + b;
let k = a => b => a;
TODO: It would be cool to have first-class pure functions. These could even be compared by value, meaning that x => x
would be equal any other x => x
. On the other hand we also need to represent Rust's closures somehow. Can fn
be used for both functions and closures?
TODO: Functions that can be run both during runtime as well as during compile-time.
Inspired by FrTime's cells, Svelte's stores and React's hooks, Gunpowder includes reactive structures called “cells”. They are essentially variables which notify you any time their value changes. There are also derived cells, defined by an expression that includes one or more other cells – their values are recomputed any time the value of any of their dependencies changes.
cell name = "Radek";
cell surname = "Zich";
cell fullName => "${name} ${surname}"; // Radek Zich
surname = "Bek";
fullName; // Radek Bek
The value of a derived cell cannot be set by hand.
fullName = "Jan Novák"; // error
You can also use a block in the definition of a derived block.
cell name = "Radek";
cell surname = "Zich";
cell fullNameConfused => {
let "${nameFirstLetter: char}${nameRest: string}" = name;
let "${surnameFirstLetter: char}${surnameRest: string}" = surname;
let nameConfused = "${surnameFirstLetter}${nameRest}";
let surnameConfused = "${nameFirstLetter}${surnameRest}";
-> "${nameConfused} ${surnameConfused}";
}; // Zadek Rich
surname = "Bek";
fullNameConfused; // Badek Rek
In order to trigger procedural code when a cell updates, use the effect
statement. To access the previous value of a cell, you can use the previous
keyword. In order to run your code just before the value changes, use the cleanup
keyword inside an effect
block.
cell name = "Radek";
effect (name) {
if previous fullName {
print("Wow, ${previous fullName} is now called {fullName}!");
}
print("Nice to meet you, ${fullName}.");
cleanup {
print("Goodbye, ${fullName}.");
}
}
// Nice to meet you, Radek.
name = "Alex";
// Goodbye, Radek.
// Wow, Radek is now called Alex!
// Nice to meet you, Alex.
TODO: Two-way data binding with cell foo =< expression
?
TODO: await next
from DreamBerd?
TODO. But probably something along the way of Python and Rust, including Rust's valued enums.
TODO: Types as compile-time constants and functions.
TODO. A sound type system, a mix of nominal and structural. Nominal types include structs (classes but without extensibility/inheritance) and traits (like interfaces or abstract classes in C#), while structural typing is done using interfaces (they describe the shape of an object and don't have to be implemented explicitly). Struct-like enums à la Rust are supported. The type system also checks for lifetimes of variables and enforces ownership, however it can be silenced using 'gc
and 'static
lifetimes.
Numeric types support ranged subtypes that throw an overflow error when assigned a number out of the range. The indices of fixed-size arrays are ranged by default. Numbers can be set to wrap or clamp the overflow, alternatively the overflow can be catched by a try-catch
.
The default string type is String
(an alias for String8
) which represents a UTF-8 encoded text of arbitrary length. It can be implicitly and losslessly converted to String16
(a UTF-16 encoded string) or String32
(a UTF-32 encoded string) and back. ASCII-encoded text with 8bit units can be assigned to the StringAscii
type, which is a strict subtype of String8
. Strings have a str.length
property which returns the number of code units and a str.count()
method which returns the number of code points.
let utf8 = "hey 👋";
utf8.length; // 8
utf8.count(); // 5
let utf16: String16 = utf8;
utf16.length; // 6
utf16.count(); // 5
let utf32: String32 = utf16;
utf32.length; // 5
utf32.count(); // 5
Since Unicode does not define “characters”, only code units and code points, there are three corresponding types: CodeUnit8
, CodeUnit16
, CodeUnit32
. And because UTF-32 code units are equivalent to code points, there's also the type CodePoint
which is defined as an alias for CodeUnit32
. Strings have an index accessor str[i]
which returns the i
-th code unit and a str.codePoints()
method which returns an iterable of CodePoint
values. Types CodeUnit8
, CodeUnit16
and CodeUnit32
can be implicitly and losslessly converted to and from u8
, u16
and u32
respectively.
let utf8 = "hey 👋";
utf8[1]; // "e"
utf8[4]; // "ð"
utf8.characters(); // "h", "e", "y", " ", "👋"
let utf32: String32 = utf8;
utf32[4]; // "👋"
let n: u32 = utf32[4]; // 128075
TODO. The lazy
keyword assures that an expression won't be run if the compiler recognizes that its value won't be used. See Thunk.
fn doFoo(callback: (x, y) => void) {
let x = cheapFunction();
let y = lazy expensiveFunction(x);
callback(x, y);
}
doFoo( x => print(x) ); // expensiveFunction does not run
doFoo( (_, y) => print(y) ); // expensiveFunction runs
Gunpowder supports code blocks with different languages. The code blocks are denoted either by a single backtick `
, or a triple backtick ```
, prefixed with a language tag. This way you can have HTML snippets or SQL queries directly in your code with full Language Server support.
let el = html```
<div>
<h1>Title</h1>
<p>Paragraph text...</p>
</div>
```;
render(el);
Unless overriden by the included language, escaped interpolation is supported with the ${...}
syntax.
let orange = Color.RGB(255, 255, 0);
let style = css`font-size: 1.5rem; color: ${orange}; font-weight: bold`;
let el = html```
<button style=${style}>Click Me!</button>
```;
TODO: Outdated. Update to use a backtick-delimited syntax instead.
Regular expressions are delimited by |
and have a rather unusual syntax. Whitespace is ignored inside regular expressions, therefore one can write spaces, tabs or even newlines without affecting the expression. Questionmark ?
, asterisk *
and plus +
all have their usual meanings: zero-or-one repetition, zero-or-more repetitions, one-or-more repetitions. All strings to be matched verbatim have to be enclosed in single- or double-quotes. Subsequence can be written with a comma ,
.
let expr = | 'a'+ , 'b' |; // matches ab, aab, aaab, ...
Unicode character categories are available using their name, starting with a capital.
let expr = | Letter, Letter.Lowercase+ |; // matches any letter followed by one or more lowercase letters
let expr = | Punctuation.Open, Number.DecimalDigit*, Punctuation.Close |
// matches any opening bracked, followed by zero or more decimal digits, followed by any closing bracket
Groups are denoted by the group
keyword and they can be either anonymous or named. To match a named group, simply write its name.
let expr = |
group currencySymbol { Symbol.Currency },
Number+,
currencySymbol
|;
// matches any currency symbol, followed by one or more numbers, followed by the *same* currency symbol
Start and end of the string can be matched using Start
and End
. Similarly, start and end of a line can be detected using Line.Start
and Line.End
. A specific number of repeats can be detected using the repeat
keyword.
let expr = |
Start,
repeat 3 {
'a'
},
repeat 1..5 {
'b'
},
End
|;
// matches "aaab", "aaabb", "aaabbb", "aaabbbb"
The traditional regex wildcards .
, \d
, \w
, \s
are represented by AnyChar
, Digit
, WordChar
, Space
respectively. Negation is done using the not
keyword. Non-capturing groups are denoted by parentheses. Logical or is done by the or
keyword.
let expr = | Digit, not WordChar |; // matches a digit followed by anything but a word char
let expr = | 'a', not ( 'b', 'c' ) |; // matches 'a' that is not followed by 'bc'
let expr = | 'a' or 'b' |; // matches either 'a' or 'b'
Character classes, traditionally written using square brackets [
, ]
, are written using the any
keyword.
let expr = |
any { 'a', 'b', 'c' },
any { '0'-'9' },
any { 'a'-'z', 'A'-'Z' }
|;
// matches "a3x" and "c0Z" but not "aaa" or "A3x"
Regular expressions support interpolation similar to those used in strings. This way, a string or a different regular expression can be inserted.
let identifier = | any{'a'-'z'}, any{'a'-z', '0'-'9'}* |;
let numberLiteral = | Digit+, ( '.', Digit+ )? |;
let assignment = | ${identifier}, '=', ${identifier} or ${numberLiteral} |;
- Make a proof-of-concept lexer, reader & parser
- Make a proof-of-concept transpiler to JavaScript
- Investigate type system
- Investigate ownership & GC
- Make a proof-of-concept type checker
- Make a proof-of-concept language server
- Investigate traits, structs, classes and interfaces
- Investigate enums and pattern matching
- Investigate error handling (JavaScript vs Rust approach, or a hybrid?)
- Investigate operator overloading
- Make a proof-of-concept transpiler to WebAssembly
- Make a proof-of-concept transpiler to Rust
- Make a formal grammar for the language
- Investigate cooperation with JS ecosystem (NPM, Vite, Deno)
- Investigate cooperation with Cargo
- Rewrite parser & language server in Gunpowder
unless
→ifnot
?Literals → "comptime int" – mají nějaký typ, který je compile-time castovatelný na jiné typy"modifikovaný seznam" –#[ ... ]
→ mutabilita?dělení – floats vs fractions – jak rozlišit? nebo defaultně fractions?celočíselný dělení → když matematicky, tak matematicky! tj. euklidovskyasdf("4", foo -> "42") – šipečka místo dvojtečky!!!!labeled breaks s return hodnotou → okoukat ze Zigu, nahradit tím Rustovej non-semicolon v blokutry
acatch
statements a vykřičník ve function signatures v Ziguin
operator pro kontrolování existence prvku v množině