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!"!
}
Here's a couple of problems that I can see already:
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!
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.
Not sure. Could be done with import
keyword. Every file can be a struct (just like in zig)