- No place expressions/lvalues. This means the
=operator takes two arguments, of type*TandT, and sets the left side to the right side. There are several other implications of this, but I'm not including them here. - No booleans. The type system is powerful enough to allow easily swapping them out for more comprehensible alternatives. For example, consider
fn evaluate(expr: Expr, deep: bool). When calling, it looks likeevaluate(expr, true), which doesn't really communicate whattruemeans. Instead, withfn evaluate(expr: Expr, depth: .shallow or .deep), the call looks likeevaluate(expr, .deep), which communicates the meaning of it much better: deep evaluation. This doesn't implement any math operators, but if what you need is a 2-value integer, you can just create an an integer type with two elements (syntax undecided as of now) and use normal mathematical operations on them. If you think booleans are really needed, you can simplyconst Bool = .true or .false. - Declarations (
x: int) and definitions (x = 5) are expressions. Since definitions are essentially equal to struct values with one type, they can be passed around as expressions during runtime (declarations can't because they're types). - No operator precedence. Instead of complicated precedence rules for infix operators, the compiler forces you to add parentheses to remove ambiguity. References: Zig, Pony, a blog post.
- Merged namespaces and struct literals. This means that having a namespace of values (in Zig,
struct { const x = 5; const y = 6; }) and struct literals (again, in Zig,.{ .x = 5, .y = 6 }) are the same thing. This makes the language simpler. - Names are references. If I have
int x = 2;, the type of the expressionx(e.g.@TypeOf(x)) is a pointer to an integer - this is because using the name as a reference to a variable ought to actually be a reference, instead of introducing complicated language semantics. BLISS (1970) turns out to have this same feature. - No tuples (structs with unnamed fields). Only structs (named tuples). Tuples honestly aren't that useful, especially when the language has the right features in their place, and they complicate parsing (especially with infix type construction, mentioned later), and only having named product/sum types means you have to give everything a name.
- No hidden copies. Since names are references and pointers are "transparent," copies are never made automatically (e.g.
int x = 5; int y = x;implicitly copies in most languages). If you don't writex.*anywhere, it's never copied, and if you do, it's always copied. The only exception are that pointers can be implicitly copied, but pointers don't have identity and are always extremely cheap (onemov), so you can't have implicit memcpys killing you (e.g. for TigerBeetle). - No address-of (
&) operator. This makes code clearer, since it's often unclear if using the address-of operator is making a short-lived reference to a local variable (&a) or simply finding the reference of an operation applied to another variable (e.g.&a.b). Instead, you'll need to add another declaration to get a reference, making it more obvious what's happening: e.g.int x = 5; *int y = x; - Infix type construction. This means, instead of
struct X { int a; int b; }, you writeconst X = (.a: int and .b: int). This has several benefits, including completely disallowing the creation of single-field unions or structs (since you can't call an infix operator with one argument), which, if you think about it, doesn't make sense because single-field unions and structs don't have any meaningful difference, but have different semantics anyway. - Main file is an expression. Instead of files being some magic construct, or guaranteed to be a container, files are just expressions. If you have a file that contains literally the text
5, you can@importit and it will evaluate to5. - Circular imports are a non-issue. Since files are expressions, the concept of "circular imports" is equivalent to mutually dependent tuples/containers/structs/product types, something like
.main = (.a = 5; .b = other); .other = main.a. This is not an issue because we have lazy type checking for product types due to them being negative types (which behave well with respect to lazy evaluation strategies), which means that dependency loop errors are non-eager and only occur if there is actually a dependency loop. This also means that mutually dependent files that aren't product types are always dependency loops, which is the correct behaviour, and yet doesn't even need a special case in the compiler. - Transparent pointers. It's possible to do most operations through pointers without having to dereference them, allowing behaviour that makes more sense. For example, instead of having an implicit dereference when doing
(&x).y, a similar expressionx->yresults in a pointer toy, instead of also dereferencing. This makes more sense, as it allows the same behaviour, but with fewer operators and less hidden behaviour. This also removeses cases of not knowing when a dereference copies or not, which makes it harder to reason about performance. - Syntax that reflects theory: functions and switch statements are essentially functions over product types (structs) and sum types (unions) respectively, so why not have syntax that reflects this? Loops also have a simpler syntax that reflect what's going on, with
loop if x == 0 then {} else break;representing a for loop, butloop ifisn't a special syntax at all, either. Another small and somewhat unrelated syntax change:ifstatements areif x then y else zinstead ofif (x) y else z. - Arrays of a given length (e.g.
[3]u8) are just syntax sugar for(.0: u8 and .1: u8 and .2: u8), and slices are a language feature instead:Slice(T: type) = (.len = usize and .ptr = [*]T)(unknown length array syntax pending; stolen from Zig right now). - Const pointers by default. Zig had a rare regression in this aspect; this is righted here with
*u8(or[]u8) being const by default, with the user having to specify*var u8/[]var u8to make it mutable. - The language also has most of the great features Zig introduces, such as
comptime,@builtinsyntax, types as values, etc. Also, simplicity/minimalism.
Last active
August 24, 2025 23:52
-
-
Save GoldenStack/09cb66ec29ff80e0aebda528d2cdb2e4 to your computer and use it in GitHub Desktop.
Language explanation and rationale for some key features of my language (https://github.com/GoldenStack/overscore)
Author
Yeah, that's basically right. It's just discount monads. I don't think I'll make stuff too focused on monads, but I guess I'll just see how the design process goes.
I don't see where/how this would be the case, considering the object contains stuff you need to do impure stuff.
It would basically be a vtable with fields that happen to be comptime-known, or it could just be a unit type that just calls static functions. Either way I think it's manageable.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
If my point wasn't clear, what I meant was:
You can only use the functions of a monad (here
IO) when it is your return type, anddonotation is just imperative-ified monad code.In the case of this language, I see it roughly translating to: You can only use the functions of a (insert word here) (here
IO) when it is passed as an object to the function.