Skip to content

Instantly share code, notes, and snippets.

@SanderMertens
Last active January 13, 2022 03:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save SanderMertens/39b5fe8492afa1f7e9544dcc4ef536c3 to your computer and use it in GitHub Desktop.
Save SanderMertens/39b5fe8492afa1f7e9544dcc4ef536c3 to your computer and use it in GitHub Desktop.

()

An incomplete overview of a language built from entities, values, argument lists and immutable key/value pairs.

Overview

Dynamic typing:

v = {x = 10, y = 20}
v.y = "foo"

v.z = 30 // add member z
v.-{x} // remove member x

Structural dynamic typing:

// @x is an invariant member. Invariants cannot be removed
a = {@x = 10, y = 20}
b = {@x = 10, @y = 20}
c = {@x = 10, @y = 30}

a == b // false, invariants must match
b == c // true

Nominal dynamic typing:

// A value prefixed with an identifier sets the identifier as type base
a = foo{@x = 10, @y = 20}
b = foo{@x = 10, @y = 30}
c = bar{@x = 10, @y = 30}

a == b // true
b == c // false, base must match

Structural static typing:

// An argument list specifies types for members
(int a, int b) v = {x = 10, y = "hello"} // error, "hello" is not an int

(int x) a = {x = 10, y = 20}.           // a = {@x = 10, y = 20}
(int x, int y) b = {x = 10, y = 20}     // b = {@x = 10, @y = 20}
(int x, int y) c = {x = 10, y = 20}     // c = {@x = 10, @y = 20}
(float x, float y) d = {x = 10, y = 20} // d = {@x = 10.0, @y = 20.0}

a == b // false, invariants do not match
b == c // true
c == d // false, types do not match

Nominal static typing:

// An argument list prefixed with an identifier sets the identifier as type base
fn rad(float v)
fn deg(float v)

a = rad(1) 
// type = rad(float v)
// value = {@value = 1}

b = deg(1) 
// type = deg(float v)
// b = {@value = 1}

a == b // false, base is different

Functions:

fn add(int a, int b) {
  return a + b
}

add(10, 20)
add(b = 20, a = 10)

Partially called functions:

fn point(int color, int x, int y)

red_point = point(red)

v = red_point(1, 2)
// type =  point(int color, int x, int y)
// value = {color = red, x = 1, y = 2}

Subtyping:

// < > is an immutable key-value list
fn draw(point<shape = circle, color> p) {
  print(p)
}

p = point<shape = circle, color = red, size = 10>

// defined keys must match 
// undefined keys are deduced
// keys not mentioned in the lvalue are ignored
draw(p)
// called draw(point<shape = circle, color = red>)

The basic building blocks

Value

A value is one of the following:

  • a scalar
  • a string
  • < ... other ueful types ... >
  • an initializer list

Each value has a type, which is represented by an entity. Values are implicitly assigned builtin types, which is based on their kind.

Values must always appear in expressions and never by themselves. An expression that just consists out of a single value is invalid.

Storage

A storage has both a type and can store a value. The value of a storage must be an instance of the type of the storage.

Initializer list

An initializer list is a curly-brace enclosed list of zero or more values separated by comma's. Values in an initializer list may be accessed with the [] operator. Initializer lists enable composite values and, when used standalone, structural typing. Initializer lists are mutable values. Each initializer list has a "type" property, which is represented by an entity.

{}
{10}
{10, 20}[1] == 20

Values in an initializer list may be annotated with names. Name annotated values may be accessed with the . operator.

{x = 10, 20}
{10, y = 20}
{x = 10, y = 20}.x == 10

An initializer list may only contain names, in which case the value is left undefined. Undefined values are allowed, but accessing an undefined value is invalid.

{x} == {x = undefined}
{x, y}.x == error, value undefined

Initializer lists may be assigned to each other, which produces the union of the lists. The following rules are followed:

  • Named values in the right list will be assigned to matching names in the left list.
  • If a value in the right list has no name, it will be matched based on index.
  • If the right list contains a name not found in the left list, it will be added.
  • If for a given expression these rules are ambiguous, the assignment is invalid
{x, y} = {10, 20}         == {x = 10, y = 20}
{x, y} = {y = 20, x = 10} == {x = 10, y = 20}
{x, y} = {z = 30}         == {x, y, z = 30}
{x = 1, y = 2} = {x = 10} == {x = 10, y = 2}
{x, y} = {10, x = 20}        // invalid, cannot determine actual value for x

Initializer lists may be nested

{start = {x = 10, y = 20}, stop = {x = 30, y = 40}}

Initializer lists may be derived from each other. If the right-hand list contains values, they are ignored

{x = 10, y = 20, z = 30}.{x, y} == {x = 10, y = 20}
{x = 10, y = 20, z = 30}.{x, y = 30} == {x = 10, y = 20}
{x = 10, y = 20, z = 30}.[2, 3] == {y = 20, z = 30}

Values may be removed from an initializer list by name or index

{x = 10, y = 20, z = 30}.-{x, y} == {z = 30}
{x = 10, y = 20, z = 30}.-[1, 2] == {z = 30}

Values can be marked as non-removable. The set of non-removable values ('invariants') are stored in the initializer type.

{@x = 10, y = 20}.-{x} // illegal, x cannot be removed

Initializer lists that have non-matching invariants cannot be assigned to each other

{@x = 10, @y = 20} = {@x = 10, y = 20} // invalid, invariants do not match

Initializer lists can be matched with each other. Two lists are considered an exact match if both lists have the same set of keys with the same values.

{x = 10, y = 20} == {x = 10, y = 20} == true
{x = 10} == {x = 15} == false
{x = 10, y = 20} == {x = 10, y = 20, z = 30} == false

Initializer lists can be left-matched with each other. Two lists are considered left-matching if all overlapping members from the left list match with the values from the right list. The rules to determine overlapping values is the same as when assigning lists. In order to match, types from the right list must be assignable to the left list.

{x = 10} <= {x = 10, y = 20} == true
{x = 10, y = 20} <= {x = 10} == false
{x = 10} <= {x = 15} == false
{} <= {x = 10} == true

The reverse applies to right-matching lists:

{x = 10} => {x = 10, y = 20} == false
{x = 10, y = 20} => {x = 10} == true
{x = 10} => {x = 15} == false
{x = 10} => {} == true

A key in the left list with an undefined value will match with any value in the right list when left-matching:

{x, y} <= {x = 10, y = 20} == true

The reverse applies to right matching:

{x = 10, y = 20} => {x, y} == true

The <=> operator returns the equivalent of (l <= r || l => r):

{x = 10} <=> {x = 10, y = 20} == true
{x = 10, y = 20} <=> {x = 10} == true
{x = 10} <=> {x = 15} == false
{x = 10, y} <=> {x, y = 20} == true

Initializer lists with mismatching invariants never match:

{@x = 10, y = 20} == {x = 10, @y = 20} == false
{@x = 10, y = 20} <=> {x = 10, @y = 20} == false

Entity

An entity is a storage that is uniquely identified by an identifier (name) in the global scope. An entity is created upon first occurrence of the identifier.

delorean

The value property of an entity can be set with an assignment:

delorean = {speed = 88, gigawatts = 1.21}

The same operators that apply to an entity's value can be applied to the entity:

delorean.speed ++;
delorean[1] *= 2;
v = 10
v ++;

When the entity is used without operators, it is interpreted as an entity handle. To refer to the entity value instead, the * operator can be used:

v = 10
v       // entity handle
*v      // value 10

The entity type is derived from the assignment and is immutable. An entity may also be explicitly declared with a type:

time_machine delorean

Typed values

A typed value is a value that has an explicitly assigned type. Typed values allow for nominal typing. A typed value is created by prefixing a value with an entity identifier. The value must be enclosed in curly braces, unless it is an initializer list (which is already enclosed in curly braces).

gravity{9.81}
gigawatts{1.21}
position{x = 10, y = 20}
line{start = position{x = 1, y = 2}, stop = position{x = 3, y = 4}}

Two values of a different explicitly assigned type are not assignable

r = radial{1.0}
r = degrees{180} // illegal, type is not assignable

Immutable key-value lists

Immutable lists are like initializer lists, except that both the set of keys and their values may not be modified. The following examples are immutable equivalents of initializer lists:

<10, 20>
<x = 10, y = 20>
<x, y>

Assignments to immutable lists are invalid:

<x, y> = <x = 10, x = 20>

New immutable lists can be derived from existing immutable lists:

<x = 10, y = 20><z = 30> == <x = 10, y = 20, z = 30>

When the left list contains an undefined value, it can be populated by overlapping values from the right list:

<x, y = 20><x = 10> == <x = 10, y = 20>

When extending lists, they may not have conflicting keys:

<x = 10, y = 20><x = 15, z = 30> // invalid, x does not match

Immutable lists can be matched according to the same rules as initializer lists:

<x = 10, y> <=> <x, y = 20> // true

To obtain a value from an immutable list, the :: operator is used:

<x = 10, y = 20>::x // 10
<10, 20>::[1]       // 20

Storages with immutable key-value list

Immutable lists can be used together with storages to derive new storages. The identifier of an storage with immutable list is called the "base". Entities with immutable lists can be used to implement subtyping. The following example creates 3 entities:

point<color = red>  // creates position, position<color = red>
point<color = blue> // creates position<color = blue>

When used as a type, two types are assignable as long as their base is equal and their immutable lists are left-matchable

point<color = red> p = point<color = red, size = 10>{10, 20}  // Ok
point<color = red> p = point<color = blue, size = 10>{10, 20} // Not ok, color does not match

The base of a type may be undefined and can be derived:

<color> p = point<color = red>{10, 20}

// equivalent to
undefined<color> p = point<color = red>{10, 20}

// equivalent to
point<color = red> p = point<color = red>{10, 20}

To obtain an immutable-list value from a storage, use the :: operator:

point<color = red> p
p::color // red

Derived types

When assigning to a storage with an undefined type, the storage is instantiated from the assigned value:

undefined v = 10
// equivalent to
int v = 10

The rules followed for deriving a type are the same as those for immutable list derivation:

point<color> v = point<color = red>{10, 20}
// equivalent to
point<color = red> p = point<color = red>{10, 20}

Argument list

Argument lists are entities that, when used as a type, can enforce types on member values. Unlike regular entities which are identified by a name, an argument list is identified by a list of comma-separated elements where each element (argument) has a name and a type:

(int x, int y)

Two argument lists with the same types but different argument names do not refer to the same entity:

(int x, int y) == (int x, int y)
(int x, int y) != (int a, int b)

A storage of an argument list type is initialized with an initializer list where each argument is an invariant with an undefined value:

(int x, int y) v 
// v == {@x = undefined, @y = undefined}

Assigning a value to a storage of an argument list type follows the same rules as initializer list assignment, with the additional constraints that the types of the list members must match with their matching arguments:

(int x, int y) v = {10, y = 20} // Ok
(int x, int y) v = {"10", 20} // Not ok

Excess values of any type may be assigned to an argument list, as long as they follow the rules for list assignment:

(int x, int y) = {10, 20, z = 30} // Ok
(int x, int y) = {10, 20, x = 30} // Not ok, ambiguous value for x

Types may be ommitted from an argument list, in which case they will be undefined:

(a, b)
// equivalent to
(undefined a, undefined b)

An argument list with (partially) undefined types follows the rules for type derivation and will instantiate a new argument list upon assignment:

(a, point<color> b) = (10, point<color = red>{10, 20})
// equivalent to
(int a, point<color = red> b) = (10, point<color = red>{10, 20})

When not all types can be derived, the resulting value and type will be partially undefined:

(a, point<color> b) v = (10, point<color>{10, 20})
// equivalent to
(int a, point<color> b) v = (10, point<color>{10, 20})
// note that color is still undefined

An argument list can be combined with an entity to create a named argument list:

position(int x, int y) p = {10, 20}
// type of p = position(int x, int y)
// value of p = {@x = 10, @y = 20}

Functions

An argument list combined with a code block is a function:

(int a, int b){ return a / b }

Named functions are created with the fn keyword and an entity identifier (the function base):

fn div(int a, int b) {
  return a / b
}

Functions can be invoked with a parameter list, which is an initializer list with parenthesis instead of curly braces that invokes the function body:

v = div(10, 2)          // 5
v = div(b = 2, a = 10)  // 5

When a named function is defined without a body, it returns an instance of its named argument list:

fn position(int x, int y)

p = position(10, 20)
// type of p =  position(10, 20)
// value of p = {@x = 10, @y = 20}

Functions have values which are immutable lists converted from the argument list value:

fn div(int a, int b)
// argument list value = {@a, @b}
// function value      = <a, b>

A parameter list specializes the function value, which creates a new function. Function values may be partially defined. A partially defined function is not invoked by a parameter list:

div(int a, int b)  // value = <a, b>
div_a = div(10)    // value = <a = 10, b>
// not invoked

div_a(20)          // value = <a = 10, b = 20>
// invoked

Partially calling a function is the same as deriving the immutable value of a function:

div_a = div(10)           // value = <a = 10>
div_a_b = div_a(20)       // value = <a = 10, b = 20>

// equivalent to
div_a_b = (*div)<10><20>
div_a_b() // Invokes with a = 10, b = 20

Parameters may be assigned when defining a function by adding a parameter list after the argument list:

fn mul_10(int a, int b)(10) { // partially call with a = 10
  return a * b
}

v = mul_10(2)
// v = 20

Iteration

The | operator iterates a list and forwards the elements to a function that accepts a value:

{x = 10, y = 20, 30} | (v){ print(v) }
// Prints:
// 10
// 20
// 30

The function may accept a key and a value. When an iterated element has no key, the key parameter will be undefined:

{x = 10, y = 20, 30} | (k, v){ print(k, v) }
// Prints:
// x, 10
// y, 20
// undefined, 30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment