Skip to content

Instantly share code, notes, and snippets.

@porky11
Created January 27, 2019 21:17
Show Gist options
  • Save porky11/c3e9c4a9694c005bdb12d3899b2dd6e9 to your computer and use it in GitHub Desktop.
Save porky11/c3e9c4a9694c005bdb12d3899b2dd6e9 to your computer and use it in GitHub Desktop.
Minimalistic C inspired programming language.

Minimalistic C inspired language

General structure

The program is a list of process calls and type definitions.

The language is line based, so every line is one of these.

A process call looks like this:

process a b

A process can take any number of arguments, even zero.

The special form let is used to define types:

let typename existing-type expression1 expression2...

The expressions are the process body.

The new typename can now be called as a process.

When calling a process, the supplied argument has to be the inner type.

Additionally the new type is returned.

Syntax

The only syntax is the line based structure and round brackets.

If a bracket opens in a line without closing, all the following lines until the matching closing bracket will belong to single grouped expressions.

These Expressoins are equivalent:

a (b c) (d e)

a (
    b c
    d e
)

Comments are indicated by a # at the begin of the line. It lasts until the end of the line, but is sensitive to brackets, so any opening bracket has to be closed:

no comment # comment
no comment again # comment again ( still comment
still comment
still comment ) still comment
no comment
# ( comment ) still comment
no comment
# ( comment ) still comment ) error: too many closing brackets

Types

Values

A value is a compile time type. It's stored as a textual representation until it's attached to a primitive type.

Primitive types

When calling a primitive type, it has to be initialized by a value.

A call to a primitive type takes exactly one argument.

All calls to primitive types don't do anything.

There are a bunch of supported primitive types: Unsigned and signed integers of size pow(2, N) where n is in inclusive range (3; 7), called u/i followed by the size, system dependant integer types usize and isize and the floating point types f32 and f64. Besides there is bool.

Structure types

Structure types can be used to create composed types like this:

struct type1 type2

A struct definition can take any number of arguments.

A struct call takes the number of arguments, a struct requires.

If an argument is not supplied it's interpreted as uninitialized.

Calling a struct does not do anything.

Array types

Array types can be used to create types containing a specified count of other types.

They can be written like this:

array count type

An array definition only takes a these both arguments.

A struct call takes count arguments.

Calling an array does not do anything.

User defined types

User defined types are similar to primitive types.

They can be defined using let and define a process, that can be called.

Calling an user defined type takes all arguments, which calling the inner types allows.

Variables

There are no variable names.

Instead, everytime a process is called, a new variable gets created implicitly.

This variable can be used in other calls.

The variable is accessed as a field of some global struct-like type, which cointains fields of all used variables.

The global type is called the.

Accessing it after a process call looks like this:

sum a b
the.sum

If the result is a struct, the field types can be accessed by specifying the type name after another ..

Here an example definition of the sum type and accessing the arguments:

type sum (struct in out) (...)
sum (in (array a b))
print the.sum.out

These variables are only bound at runtime, when it is used.

There is also a similar type, called a, which can create uninitialized variables explicitly like this:

# calculate some in value
in value
a.out
sum the.in the.out
print the.out

When reading from an uninitialized variable, it will be ensured at compile time, that it has been written to at least once.

Theres another process local variable this. It maps to the inner type of the currently defined type. So when defining a function, which sets out to in, it can be used like this:

let set (struct in out) (
    memcpy this.out this.in
)

Control Flow

Conditionals

It's just if and optionally else and even elsif for simplicity. The condition can only be a bool. The blocks don't introduce a new scope. When a variable is only created in one branch and used afterwards it's an error. The else branch is just a new line afterwards.

if cond (sum a b)
else (mul a b)
print the.sum.out #error, sum only defined in one branch. Result ambiguous (if sum was used before) or result may be uninitialized (if result was never used yet.

Loops

Loops are expressed using something similar to goto:

loop (i32 1) (u32 1)
if (< loop.i32 (i32 5)).out (repeat (inc loop.i32) (mul loop.u32 (u32 2)).out)
print loop.u32

It's possible to use multiple loops. The most recent loop can be renamed to different names.

loop (usize 0)
let first loop
inc loop.usize
loop (bool false)
if cond1 (repeat loop some.first)
elsif cond2 (repeat first some.usize)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment