Skip to content

Instantly share code, notes, and snippets.

@copygirl
Last active May 6, 2020 20:26
Show Gist options
  • Save copygirl/a705ffb868f8dc6935b0749bd3995e7b to your computer and use it in GitHub Desktop.
Save copygirl/a705ffb868f8dc6935b0749bd3995e7b to your computer and use it in GitHub Desktop.

Overview

Statements

Every statement in this language has an associated type and value. In fact, statements that are not supposed to express any value, instead are of type None, its value usually expressed as [].

Here are some example statements and the type they'd be inferred as:

1         // int
"a"       // string
("b"; 5)  // int
[]        // None
[] => 0   // Function
[1, 2, 3] // Construct (concrete type depends on usage)
["a", 10] // Construct (again, depends on usage)
int       // Type (compile time type)
foo : int // Declaration (compile time type)

End of Statements

When the parser encounters a newline character, a statement is complete, unless there's a yet un-closed block or construct. In global scope or a block, multiple statements can be separated and therefore placed on the same line using semicolons (;) between then.

1; 2
// is equivalent to
1
2

It is not allowed to place a semicolon at the end of a line, unless it is to suppress the inferred type of a block statement and instead infer None.

(1; 2;)
// is equivalent to
(
  1
  2
  []
)

Similarly, while not statements, to specify multiple elements in a construct on a single line, the comma (,) can be used.

[1, 2]
// is equivalent to
[
  1
  2
]

Keep these rules in mind when writing multi-line statements:

1 + // ERROR: Operator `+` expected value, found end of line.
  2
// The following parses as two separate statements.
// One is just `a` on its own, then `+b` using the unary `+` operator.
1
  + 2
// The correct way to split this into multiple lines:
1 + (
  2
)

Blocks

So called () "blocks" take the role of overriding precidence in mathematical operations (as you're used to), but also to create function bodies, creating scopes and otherwise grouping statements (usually, {} is used for this).

bar = (20; 30)
// is equivalent to
(
  20
  bar = 30
)

Constructs

With [] "constructs", multiple values can be grouped together. But such an object won't have any concrete type until context is available. Therefore, it can represent arrays, lists, maps, tuples, structs, anonymous constructs, functions parameters/arguments, ...

In a case where not enough context is available, anonymous constructs might be created:

numbers := [3, 7, 11, 42]                 // Array-like
mixed   := [10, "ten", 0.1, "tenth"]      // Tuple-like
named   := [a = 10.0, b = "foo"]          // Struct-like
lookup  := ["ten" = 10, "twenty" = 20]    // Map-like
["one", second = 2, 3.0, forth = "2 2 +"] // Argument-like
  • Array-, tuple- and struct-like constructs provide field access (see Fields).
  • Argument-like constructs are compile-time-only and can match function parameters.
  • Array- and map-like constructs will automatically provide array and map indexers (see also: Callable).
numbers[2]    //> 11
lookup["ten"] //> 10

mixed[2]   // ERROR: No callable with signature matching 'int -> ?'.
named["a"] // ERROR: No callable with signature matching 'string -> ?'.
named[a]   // ERROR: Using undefined symbol 'a'.

Constructs are also used in certain places where types are expected. For example, to define structs or function parameters.

Fields

Fields in constructs can be accessed using dots (.), the field access operator. Fields must be accessed using symbols or indices known at compile time.

numbers.2 //> 11
mixed.3   //> "tenth"
named.a   //> 10.0

numbers.4  // ERROR: Invalid field access (unknown index '3').
named.what // ERROR: Invalid field access (unknown symbol 'what').
lookup.ten // ERROR: Invalid field access (unknown symbol 'ten').

Struct

Vector3  :: struct [float, float, float]
BlockPos :: struct [int, int, int]

SubjectData :: struct [
  name   : string
  gender : Gender
  level  : uint
]

pos := BlockPos [10, 64, 600]

subject := SubjectData [
  name   = "copygirl"
  gender = Gender.FEMALE
  level  = 8999
]

Callable

Callables are essentially functions that have exactly one input value, and one output value. Either one of these may also be None, or a construct combining multiple values. The call syntax works by simply separating two symbols or values using spaces, blocks or constructs.

// Recommended call syntax:
Log ["DEBUG", "What is this?"]

foo bar quux blub
(foo bar) quux blub
// are equivalent to
((foo(bar))(quux))(blub)

foo (bar quux) blub
// is equivalent to
(foo(bar(quux)))(blub)

Test (20; 30)
// is equivalent to
(
  20
  Test 30
)

Indexing into arrays, lists, maps and other data structures is done by making the object itself callable.

// Recommended indexer syntax:
numbers[0]

// is equivalent to
numbers(0)
numbers 0

Functions

Incr : int -> int     // Function definition.
Incr = val => val + 1 // Function implementation.

// Or, more complete, combining them both:
Decr := [val : int] => int (
  val - 1
)

Let's take a look at that last one in more detail:

  • Decr is the symbol the function will be bound to.
  • := defines (:) and assigns (=) the right-side value to this symbol.
  • [val : int] represents the function inputs. The construct ([]) is not required in the case of just one "parameter", but highly recommended.
  • => creates a function implementation, whereas -> defines a function type.
  • int could be seen as the "return type", and it's recommended to use it as such, but it's not required as everything after the arrow is already the function's body.
  • (val - 1) could be seen as the "function body". The value returned by this block will be what's returned by this function.

Types

You've already seen plenty of types. Let's expand on them!

int           // Primitive type
int -> int    // Function type
[int, string] // Tuple-like type

First of all, you should know about ::, the alias operator. It can be used to create create aliases of types so they may be used interchangeably.

int  :: Int32
uint :: UInt32
Int2IntFunc :: int -> int

When combined with the struct keyword, a new destinct type can be created, which will not be compatible with the "aliased" type.

Vector3  :: struct [float, float, float]
NotAnInt :: struct int

Additionally, maybe this syntax could be used to create namespaces. But I'm personally not sure about this one quite yet.

Game :: [
  Logging :: [
    Severity :: enum [TRACE, DEBUG, INFO, WARN, CRITICAL]
    Log := [severity : Severity, message : string] -> None ( ... )
  ]
]

Game.Logging.Log [Game.Logging.Severity.INFO, "I am here!"]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment