Skip to content

Instantly share code, notes, and snippets.

@nothke
Last active October 29, 2023 16:33
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 nothke/a727670a5e043e509f707a1de7fe453f to your computer and use it in GitHub Desktop.
Save nothke/a727670a5e043e509f707a1de7fe453f to your computer and use it in GitHub Desktop.

Hello reader. This post is just for throwing ideas in the air, there is no implementation, it's all just in my head, and I don't know if this would even work in practice. It's incomplete as well.

INSERTNAMELANG is yet another embeddable scripting language! It's imperative, optionally-typed with write first optimize later philosophy.

It borrows elements from all languages so I'm not claiming any inovations.

I'll try to use as few words as possible in explaining. Here we go:

Variables are mutable

var a = 1
a = 2

Constants are written without "var". They are immutable.

a = 4
a = 2 // Error!

variable without specified type is an "any" type, meaning it could be assigned to anything

var a			// any is undefined
a = 4			// any now has number value
a = "whatever"	// any now has string value
a = []			// any now has table value
a = (){}		// any now has function value

A type can be specified. The only types are any, bool 0, number #, range .., string "", table [] and function () (they can also be optional or contain errors). This is how you specify them:

a: 0 = true
b: # = 1
c: "" = "string"
d: .. = 3..5
d: [] = ["string", 1]
e: () = (a) { return a }

Opinion: Using #, "" for type signature instead of "number", "Number", "num" or similar is first, shorter to write, secondly it's self-explanatory, thirdly it's case insensitive.

Constants cannot be an any. They are always a specific type, so specifying type is not necessary

a = 5 					// always a number
f = (a) { return a } 	// always a function

You cannot assign to wrong types

var a: # = 4	// this is a number only
a = 3.14
a = "oops"	// error!

You can cast any type to string with $

str1 = $5
str2 = $[ "string", 42 ]
print(str1, str2)	// 5, [ "string", 42 ]

You can convert string to number with #

var a: # = #"42"
var b: # = #"oops"	// Error!

Default values if not initialized explicitely

var a: 0		// false
var b: #		// 0
var c: ""  		// ""
var d: []		// table of anys
var e: ()		// (){} - a noop

All types are copied on assignment

table = [1, 2, 3]
var table2 = table 	// copies
table2.0 = 10
print($table.0)		// it's still 1

But, you can get a reference to types if you wish to "point" to them

table = [1, 2, 3]
var table2 = &table	// takes reference to table
table2.a = 10
print($table.0)		// it's now 10

Function parameters are always const

func = (a: #) {
    a = 3		// Error!
}

So, if you wish to change parameters, pass them as a reference

func = (a: &#) {
    a = 3
}

var x: # = 3
func(&x)

If a function return type is not defined and it has at least one expressionless return, it returns an any, otherwise it's a void.

But, you can specify return type after ()

increment1 = (a) { return a + 1 } // accepts any, returns any
increment2 = (a: #) # { return a + 1 } // accepts number, returns number

There are no function or operator overloads

max (a: #, b: #) # => if a < b { b } else { a }
max (a: #, b: #, c: #) # { ... } // Comptime error!

There are no optional or variable number of arguments. If you want varargs, pass it as a table.

max (nums: []# ) # {
    var highest @# = nums.0

    for n in nums {
        if n > highest {
            highest = n
        }
    }

    return highest
}

You can write single line return expressions with =>

add = (a, b) => a + b

// ^ is equivalent to:

add = (a, b) { 
    return a + b 
}

Tables are a generic container, they can be used as arrays, dictionaries, or classes/structs:

(?? actually thinking of introducing structs after all (see bottom of page)!)

// as array:
arr = [ 1, "oijg", [ 2, 3 ] ]

// as dictionary
dict = [ Ivan = 1, Maria = 2, Boris = 3 ]
print($dict.Ivan) 	// 1

// as struct:
Vector = [
    x: # = 0,
    y: # = 0
]

var v3 = Vector		// you simply copy the table to "instatiate the struct"

Any-tables can contain pairs of any values. But, if a type is specified, they can only contain one type

var table: []# = [1, 2, 3]
table += 4
table += "string" 				// Error!

Pair types can be specified like this

var table: []("", #)			// Table that maps strings to numbers
table."string" = 1
table.3 = 2						// Error, only string can be used as a key

If only a single value is assigned to a table member, then the keys are automatically assigned to 0, 1, 2..

table = [0, 1, 2]
print($table.0) 	// 0

Keys can be both name or "name" and they are different keys. name can only be defined at compile time (?? not if structs are introduced)

A table that contains functions can reference own members with .:

person = [
    name: "" = "Ivan"
    surname: "" = "Notaroš"

    fullName = () {
        print(.name + " " + .surname)
    }
]

person.fullName() // "Ivan Notaroš"

Length of table can be queried with # (clash with numberifier?)

var table = [1, 2, 3]
print(#table) // 3

Append single items to tables with +=

var a = [1, 2]
table += 3
print(a)			// [1, 2, 3]

Appending tables to tables can be done with ++ operator

a = [1, 2, 3]
b = [4, 5]
c = a ++ b
print(c) 			// [1, 2, 3, 4 ,5]
d = a + b
print(d) 			// [1, 2, 3, [4, 5]]

Tables and strings can be repeated with ** operator

a = "abc"
b = a ** 3
print(b)			// "abcabcabc"

If you wish to init the whole list to zeroes, you can do so like this

a = [0] ** 6
print(a)		// [0, 0, 0, 0, 0, 0]

Tables can have fixed capacity. They still act as array, but will not resize (or move in memory).

table: [2]#		// Empty table of 2 numbers
table += 1
table += 2
table += 3		// Error! Reached max capacity

Tables have a few builtin functions

var table = [1, 2, 3]
a = table.@pop()
b = table.@popFront()
c = table.1 // retuns a value from a key, errors if doesn't exist
table.@insert(1)
table.@clear()
table.@remove(24) // removes element, errors if doesn't exist
table.@at(2) // gets value by index
table.@sub(0..3) // returns new table 0..3
table.@slice(0..3) // returns a reference to table from 0..3 (??)
table.@exists("smth") // returns true if exists

(?) Hashtables are special form of tables turning them into unordered maps. Whatever you use as a key must implement a hash function.

var hashtable: [#] = { "soijd" = 43, "haha" = 42 }

Know the difference:

var y = []			// any type that is table of anys
var x: []			// table of anys
var a: []#			// table of numbers
var s: [5]#			// table of up to 5 numbers
var d: [#5]#		// hashtable of up to 5 numbers
var b: [#][]#		// hashtable of tables of numbers
var c: ?[#][]?#		// optional hashtable of tables of optional numbers

Ranges:

var range: .. = 0..3 // range 0, 1, 2
range = 0..&3 // range 0, 1, 2, 3
range = 0.. // a range from 0 to infinity
range = 10..0 // a descending range from 10 to 0
range.3 // indexing a range, it's 7 in this case
for i in range { ... } // can be used in for loops

Errors can be handled with catch

num: # = #"string" catch 0  	// Numberification errors but is handled
print($num)						// 0

You can return error from a function with "Message"!

divide = (x, y) {
    if y == 0 {
        "Division by 0"!
    }

    return x / y
}

a = divide(6, 3) // no error, a is anytype with value 2
a2: # = #divide(6, 3)// no error, a2 is number 2
b = divide(2, 0) // Error: Division by 0
c = divide(2, 0) catch 0 // catches error and doesn't error

Optionals with ? (inspired by zig)

var num: ?# = 4		// a maybe number
num = null			// can be null

You can cast optionals to their non-optional peers and vice versa. But casting null to non-optional will error.

num: ?# = null		// a maybe number
var num2: # = num			// Error! Number is null
var num2: # = num catch 0	// Error is handled

Optionals are initialized to null by default. If constant is an optional it must have a specific type.

Ifs can only have boolean conditions

i = 3
if i == 3 {}
if i {} // Error! condition is not a bool

For loops work on strings, tables and ranges only. There are no iterators.

for i in 0..10 {}

table = ["asd", "ege", "aosij"]
for str in table { print(str) }

for char in table.0 { print(char) }

Multi object for loops terminate at whatever iterator finishes first

for c, i in string, 0.. {
    if (c == " ")
        print("space at index " + i)
}

For loops can also be infinite

for i in 0.. {
    if i >= 10 {
        break
    }
}

Same thing, but with a while:

var i = 0
while i < 10 { 
    i += 1 
}

There is no ++ or -- btw

Switch is exhaustive and doesn't fallthrough, but the default case can be handled with _:

switch a {
    1: print("Huh")
    2, 3: print("2 or 3")
    3..5: print("can also be a range")
    "case can be a string too": print("whatever")
    _: "This should never happen!"!
}

Devil's advocate

Here's a couple of problems that I can see already:

Structs

In my original proposal I was trying to avoid having structs and only have tables, but there's a couple of problems..

Lets consider a table that exists like a class:

MyClass = [
    x = 1,
    y = 2,

    init = (x, y) => [ x = x, y = y ],
    dot = (vec) => .x * vec.x + .y * vec.y,
]

Since no struct type exists, but only tables, "methods" are just function pointers, they are copied for each instance:

var instance = MyClass // functions get copied
instance.x = 1
instance.y = 2

var instance = MyClass.init(1, 2) // this just returns table, with no methods

// this one also won't have a .dot()
var instance = [ x = 1, y = 2 ]
instance.dot(instance) // Doesn't exist

A constructor in our MyClass would need to be like:

init = (x, y){
     this = MyClass 
     this.x = x
     this.y = y
     return this
}

..a little verbose :(

Maybe ctor should start with a token?

MyClass = [
    x = 1,
    y = 2,

    // syntax sugar for previous, retains all methods:
    @ init = (x, y) => [ x = x, y = y ],

    dot = (vec) => .x * vec.x + .y * vec.y,
]

Maybe const tables should be read like "structs"

Or maybe structs are needed after all! :o

If they would exist, then the best would be to init them with

MyStruct = {
    var x = 1 // var is required to make it mutable
    var y = 2

    pi = 3.14 // you can still have constants, these are "static"

    // now constant functions are "methods"
    dot = (vec @MyStruct) @# => .x * vec.x + .y * vec.y

    // while variable functions are function pointers
    var funPtr = (){}

    // You can also make constructors like this
    init = (x, y) @MyStruct => MyStruct{ x = x, y = y }

    zero = MyStruct{0, 0}
    one = MyStruct{1, 1}
}

Instantiating a struct

vec = MyStruct{} // instantiate with all defaults
vec.x = 1
vec.y = 2
vec.z = 3 // Error! .z not a member of struct MyStruct
vec.pi = 1 // Error! .pi is immutable
vec.funPtr = () { print("hijacked!") }
vec.dot = () { print("oops") } // Error! .dot is immutable

dot = vec.dot(vec)

var vec = MyStruct{ x = 0, y = 1 } // an any that is a MyStruct
var vec @MyStruct = MyStruct{ x = 0, y = 1 }

There is no "struct" superset type (like a: {}), since variables need to be instances of specific structs. Although of course any can be any struct.

var a: {} = MyStruct{} // Impossible, there's no "struct type"
var b = MyStruct{} // Yes, it's an any whose value type is MyStruct
b = Vector{} // Sure, it can be anything else

Possible syntax sugar, infer struct type:

var vec @MyStruct = { x = 1, y = 2 }

Possible syntax sugar, list initialization (wouldn't work if there are constants in the way):

var vec @MyStruct = { 1, 2 }
var vec @MyStruct = { 1, 2, 3 } // Error! Attempting to set constant 'pi'

You can use structs as namespaces

Color = {
    var r = 0
    var g = 0
    var b = 0
}

// A collection of colors, in a separate "namespace"
Colors = {
    white = Color{ r = 1, g = 1, b = 1 }
    red = Color{ r = 1, g = 0, b = 0 }
}

whiteColor = Colors.white

Struct nesting

Company = {
    Person = {
        name @""
        surname @""

        $ = () => name + " " + surname
    }

    people = []@Person
}

company = Company{}

company.people += Person{ 
    name = "Ivan",
    surname = "Notaros" 
}

company.people += Person{
    name = "Max",
    surname = "Payne"
}

for person in company.people {
    print(person)
}

Then tables would not be able to have stringless values like a = 1, instead they would need to be "a" = 1, that solves that.

a = [ "x" = 0 ]
b = [ x = 0 ] // Error!

Optimization possibilities

The language can have a "write first, optimize later" philosophy:

For example:

var table = [234, 345, 3]

This is just a table of any type values not strictly of numbers, if we ever wish, we could add a string later to it:

table.add("something")

And it works! However, it's likely we actually wanted to do only numbers, we could go back and specify the type:

var table: []# = [123, 54, 3]

This provides several benefits: we make the type known so we don't accidentally add a string for example, and it optimizes the memory because the numbers are now stored as values without adding separate heap allocations for each number.

(??) However, we might actually want to optimize further, if we know that the numbers are smaller than 255, they can be represented as u8:

var table: []#u8 = [123, 54, 3]

Now we have reduced the memory footprint from 8 bytes for each number to just 1 byte.

However, if we know that the number of elements is not going to be larger than for example 4, we can specify that as well:

var table: [4]#u8 = [123, 54, 3]

This now becomes a fully stack-allocated array, of 4 bytes. So we went to a heap allocated table of any types where each element was a new heap allocation, to just 4 bytes. This is "optimize later".

Would this even work? Since everything is both a reference and value type?? Unsure.

importing other files

Not sure. Could be done with import keyword. Every file can be a struct (just like in zig)

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