- Basic types:
Integer numbers. Includes:
Int
- largest native signed integer type available on current architecture- Differently sized signed and unsigned integers
BigNum
- infinite size signed integral number
All integral literals are BigNum
, but are by default inferred to Int
Real numbers. Includes:
Float
- largest native floating point type available on current architecture- Differently sized finite-precision floating point types
Rational
- an infinite precision real number
All floating point literals are Rational
, but by default inferred to Float
Dynamically and statically sized arrays. Includes:
Array<T: Type>
Array<T: Type, N: Int>
A pointer represent a memory address of a piece of typed or untyped data.
String
,WString
,ByteString
- They represent text, in various formats.Map<K: Type, V: Type>
- Represents a key-value mapping. It's actually a trait(roughly like interfaces) with a default implementation(a hash map)Tuple<...>
- Anonymous structures of arbitrary size, fields can be named.- Smart pointers, including unique, reference-counted, and garbage-collected ones.
- Other types included in standard library, including various maps, sets, graphs, etc.
NOTE: Syntactic sugar exists for maps and arrays contained inside tuples.
E.g. t: Tuple<Int, Array<Int>> = (1, 2, 3, 4, 5)
is valid code.
Primarily used for varargs and keyargs in procedure calls
- Procedures:
A procedure is a possibly-anonymous function that maps an input value(tuples for multiple params), into an output value
Procedures have access to the scope where they are defined, allowing nested functions. Trying to return a procedure accessing a local scope is an error, instead to pass closures around, you need to explicitly capture variables from the surrounding scope(s) via compiler directive macros
On its own, a procedure is simply a block of code: {}
, the rest is achieved via macros, for example:
lambda(x: Int) { return x+1}
do { a = getValue(); print(a); }
for(i in range(1, 10)) { print(i); }
- Functions:
A non-anonymous identifier for a group of procedures with the same name and parameters, they represent ad-hoc polymorphism or in other words, overloading. For example:
- The
+
function can have separate procedures for+(Int, Int)
and+(Int, Float)
- A
hash(value)
function might have different implementations for different types ofvalue
Assertions can impose constraints on accepted values, and will produce compile-time errors when possible, and otherwise error out at runtime.
Value-based dispatching is contemplated, e.g. being able to define behavior for not just
different types, but also specific values or ranges of values, such as
hash(s: String) { definedFor(s:isUpper()); return hash(s:lower()) }
.
The behavior would be enabled when using binary modules as well, forcing calls to be dispatched between the local and remote version(s)
- Structures:
A layout of named fields of specific types, essentially translates to a memory layout.
Will presumably be implemented as a macro that works on {}
blocks,
to allow reusing an existing mechanics of scope lookup,
but with restrictions applied and modified semantics.
- Typed unions:
Composition of a type tag and alternatively typed entries. Size is equivalent to the size of the biggest option.
Unions can be switched on, and assigned values of types they account for, and otherwise are treated like other types.
Examples:
- Built-in
Option<T>
can be eitherSome(val: T)
, orNone
value: Option = 23
will inferOption<Int>
, and assignOption<Int>.Some(23)
to it
- Traits:
Similar to interfaces, but can provide implementations of functions, including those it requires. Can include further constraints on types it can apply to, including "inheriting"/requiring other traits
All traits implicitly require destructors, if such exist for a given type.
The implementation is equivalent to:
struct MyTrait {
vtable: &MyTrait_vtable;
object: &Void;
}
Traits can be empty, and can be marked as implicit, e.g. implemented for any type for which required functions are implemented.
- For example, with a hypothetical
ImplicitlyHashable
, requringhash(T): Int
, aprintHash(v: ImplicitlyHashable) { print(hash(v):hexDigest()) }
function will be usable with any typeT
for whichhash(T): Int
exists. - Empty traits can be used as tags
- An implicit empty trait
Any
can be used to pass around values, and dynamically typecheck or cast them. Note that aDynamic
type exists which wraps this into a more convenient package.
- Classes and OOP:
They are not a built-in feature, and instead an OOP model is provided by the standard library and implemented using macros.
Functionality includes:
- Multiple Inheritance, including fields, vtables,
- User-defined vtables, containing lists of functions to be dynamically-dispatched
- Automatic generation of:
- Cast function implementations for casting between classes in hierarchies.
- Functions that dynamically dispatch analogous vtable entries
- Specialization of base pointer types to handle (potentially virtual) destructors
- Classification functions, e.g.
isPOD
,inherits
, etc.
Aim is to be roughly equivalent to C++ classes.
- Generics:
Generics are compile-time procedures that take one or more parameters, and produce types(trait/struct/union, by extension classes), functions, or procedures.
Generic arguments are inferrable, e.g.:
str = "hello world!"
will be inferred toString
ids = ["John": 42]
will be inferred toMap<String, Int>
arr = [1, 2, 3]
will be inferred toArray<Int>
, orArray<Int, 3>
, depending on its usage. For example, ifarr:append(4)
, or other procedure changing its size is called,arr
will be inferred to be a dynamically-sized arrayarr = []
will be inferred toArray<Unknown>
, and it will remain partially-typed until the full type can be inferred, e.g. viaarr:append("hi!")
. Usage of partially-typed generics is limited, as they are not complete types.
Generics can include macros, which will be executed after the types are applied, for example:
- A macro inside a generic could construct
N
fields based on anN: Int
parameter - Or define the type signature of a
printf<Format: String>
procedure - Or do anything else that macros are capable of
Partial and full generic specialization is allowed.
It's possible to disable type inference for a specific generic.
- Macros and related functionality:
Macros are functions executed at compile time, capable of manipulating the parser, compiler, and other concepts that make no sense at runtime.
Data types and names that help leverage the power that macro functions offer exist, and some of them are available at runtime and for non-macro usage as well.
ExprOf<T>
can be used as a parameter type to facilitate lazy evaluation, and will be passed as a procedure returningT
. As it can contain references to the context in which it is defined, limits might be imposed on what can be done with them in non-macro functions. To avoid that, closures must be used.- For example,
log(ExprOf<String>)
andlog("[%s] %i: %s" % (system, code, name))
can be used to only perform the costly string formatting operation if logging is currently enabled.
- For example,
Identifier
can be used to prevent identifier resolution, e.g.:f(id: Identifier) { return id:getName() }
Arguably a feature not very useful outside macros.- Reflection can be used at runtime to facilitate metaprogramming, but when used at compile-time will instead be optimized into fast, static code, as well as be allowed to do things impossible at runtime(e.g. add fields to types)
There are two main things that macros can do that is impossible at runtime, access core macros, and the compile-time context objects used for symbol(and operator/custom literal) resolution
Core macros are macro functions that directly map to the final AST, or otherwise interact with the process of parsing and compilation.
Core macros include:
- Core language constructs such as loop(), branch(), call(), return(), ...
- Parsing-related macros such as
defineInfixOp
,defineOpAssociativity
, and other (for-now) secret sauce of my parser ;)
Context objects are objects that the parser/compiler uses to resolve symbols, operators, custom literals(marked with backticks), etc.
Can be leveraged to allow:
-
Affect symbol resolution, e.g. by providing an alternative symbol lookup function that makes all symbols default to
0
-
Inject new expressions into scope, either at call-point, beginning, end, or arbitrarily chosen point.
-
As scopes are all actually functions, macros let you execute them, e.g
do { print "hello" }
is a macro that executes the anonymous function contained inside{}
-
That involves "joining" scopes, e.g. mapping the inner scope's return function to the surrounding scope's one. Without it,
do { return 4 }
would only return into ether, in other words, the value would be discarded. -
Execute expressions as if written in callee's scope, e.g.:
macro incrementA(call_scope :Scope) { inScope(call_scope) { a += 1 } } a = 3; incrementA(@); print a // prints 4.
The @ symbol specifies the current scope, and is by default passed to all macro functions, it is only marked here for demonstration purposes.
Most of the language is implemented using a combination of core macros and context manipulation, this includes for-in, do and while blocks, if-else, pattern matching, ...
NOTE: Macros can be "plugged" into the generics system as type factories, and be otherwise treated like any other generic, except with disabled inference.