Skip to content

Instantly share code, notes, and snippets.

@m93a
Last active February 10, 2023 12:50
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save m93a/86313d72d8baf63801cb54053dbbfc5d to your computer and use it in GitHub Desktop.
Save m93a/86313d72d8baf63801cb54053dbbfc5d to your computer and use it in GitHub Desktop.

Gunpowder

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.

Runtime constants

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();

Conditions

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; }

Literals

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

Variables and compile-time constants

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

Arithmetics

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]]

Loops

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;
    }
  }

Functions

Parameters

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]

Returning

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;
}

Mutable params

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.

Transfering ownership

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]]

Generics & Lifetimes

TODO.

Lambdas

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?

Constant functions

TODO: Functions that can be run both during runtime as well as during compile-time.

Cells and reactivity

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?

Pattern-matching

TODO. But probably something along the way of Python and Rust, including Rust's valued enums.

Types

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.

Chars & strings

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

Lazy evaluation

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

Foreign language inclusion

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>
​```;

Regular expressions

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} |;

Roadmap

  • 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
@m93a
Copy link
Author

m93a commented Dec 7, 2022

  • unlessifnot ?
  • 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. euklidovsky
  • asdf("4", foo -> "42") – šipečka místo dvojtečky!!!!
  • labeled breaks s return hodnotou → okoukat ze Zigu, nahradit tím Rustovej non-semicolon v bloku
  • take × take mut – dvě různé věci, první může prostě vzít hodnotu a někam si ji uložit, druhá ji upravuje
  • dokumentace ke kódům – kouknout na texinfo, nápad: k souboru .gp ještě soubor .doc s dokumentací, dostupný přes tooltips
  • try a catch statements a vykřičník ve function signatures v Zigu
  • ranges
  • in operator pro kontrolování existence prvku v množině
  • TODO: naučit se Haskel, naučit se používat monády, je to sranda, viz: odkaz
  • TODO: naučit se Zig, ale for real!

@m93a
Copy link
Author

m93a commented Jan 19, 2023

  • type system as comptime functions
  • reactive features
  • var instead of let mut, leave mut in type only
    • const for compile-time constants, let for runtime constants, var for runtime variables
  • generators
  • foreign code inclusion
  • investigate Effekt for exceptions and generators

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment