lainchan / λ
Language design
vector · 2023-12-26 12:21
I've already tried many times to design my own programming language. It often starts when I identify features I miss in the language(s) I use and ends when I find another language that addresses those issues.
I'm curious about any languages lainons here might be designing. What issues does it address? What are it's particular strengths? Are you stuck trying to incorporate certain features?
Below is me rambling about the type system I'm designing which is breaking my mind. (Note that the language is meant to be used as a sort of "high-level" IR with useful features for other languages.)
---
I believe the biggest issue with language design is creating a good type system. Ideally you want it to be flexible enough to encode your intentions while being constrained enough to allow meaningful error messages, let alone being (practically) solvable.
Two features I'm trying to incorporate in my own language are pattern matching and "anonymous" sum types / tagged unions. The latter I find most interesting because it's very powerful. For example, you can represent boolean values like
True;
False;
Bool = True | False; Where True and False are their own (unit) types, as well as values, and Bool is the union of the True and False types.
With a bit of compiler "magic" you can extend this to integers, e.g.:
Int[L, H] = L | (L+1) | (L+2) | ... | H; It becomes truly magic when you combine this with pattern matching:
add (0, 0) = 0;
add (0, 1) = 1;
add (0, 2) = 2;
...
add (7, 9) = 16;
... You can represent pretty much every operation as a simple lookup. More, you can automatically construct an output type based on the input types. For example, if you multiply an integer by 3, the output type is (0 | 3 | 6 | 9 ...).
However, I found that this is in practice much too flexible and it is impractical to compile such code. Say you have a function that calculates the n-th prime:
nth_prime (Int[0, N]) = ?; How do you determine the return type? If we were to automatically construct the output type we'd end up needing a huge amount of memory to represent the output type, let alone the time needed to actually construct it!
After grappling with it for a fair amount of time I concluded it is best, at least for public interfaces, to let humans specify the output type instead of deriving it automatically.
# compiler magic
Add[L, H] = ...;
Mul[L, H] = ...;
Min[L, H] = ...;
Max[L, H] = ...;
MulMin[LL, LH, RL, RH] = Min[Min[Mul[LL, RL], Mul[LL, RH]], Min[Mul[LH, RL], Mul[LH, RH]]];
MulMax[LL, LH, RL, RH] = Max[Max[Mul[LL, RL], Mul[LL, RH]], Max[Mul[LH, RL], Mul[LH, RH]]];
add[LL, LH, RL, RH] (Int[LL, LH], Int[RL, RH]) = Int[Add[LL, RL], Add[LH, RH]];
mul[LL, LH, RL, RH] (Int[LL, LH], Int[RL, RH]) = Int[MulMin[LL, LH, RL, RH], MulMax[LL, LH, RL, RH]]; To avoid horrible errors like the ones you get with C++ templates it is useful to be able to constrain them.
As example, let's implement pow for integers:
Pow[B, 0] = 1;
Pow[B, E] = Mul[B, Pow[B, Add[E,-1]]];
pow[BL, BH, EL, EH] (Int[BL, BH], Int[EL, EH]) -> Int[Pow[BL, EL], Pow[BH, EH]]; For integers it does not make sense to have a negative exponent, as you cannot represent fractions. We want to enforce that BL <= BH and 0 <= EL <= EH.
BL <= BH and EL <= EH can be enforced by (ab)using pattern matching for type arguments:
Int[0, 0] = 0;
Int[0, 1] = 0 | 1;
...
Then simply don't define nonsensical types such as Int[1,0].
For enforcing 0 <= EL we can further abuse pattern matching to implement conditions:
Le[0, 0] = True;
Le[0, 1] = True;
Le[1, 1] = True;
Le[0, 2] = True;
...
Le[1, 0] = False;
... And then:
Pow[B, 0] = 1;
Pow[B, E] = Pow[B, E, Le[0, E]];
Pow[B, E, True] = Mul[B, Pow[B, Add[E,-1]]]; Not the most elegant but it should work fine.
Picrel is me trying writing an interface for a base64 encoder+decoder module. When coming up with features I always try to first write some code with it to see if there are any deficiencies before potentially wasting time updating the compiler/interpreter.
Haruyoshi · 2023-12-30 06:08
>>38018
I have been thinking about a language with support for higher inductive types. These types have several constructors for the various kinds of maps: binary search, AVL, etc. Along with the typical data constructors, we also define an equivalence type capturing possibly multiple conversions between these representations, where different conversions offer varying dimensions of efficiency (i.e., time vs. memory).
Suppose that the compiler were able to use this notion to transparently rewrite map representations, the same way it can do today with list fusion? Meanwhile, in your code you get to work with the construction that is simplest to reason about (and makes proof work easier, if you are in such an environment). This may sound just like an abstract interfaces with multiple implementations, but it includes the freedom to choose any implementation and have the compiler transparently substitute another as needed, without affecting the meaning of the program.
Yu-ichi · 2023-12-31 09:23
Yes! I've been thinking about the design of a peculiar sort of language, let me explain.
There is a kind of esolang caled wenyan (https://wy-lang.org) which is supposed to be a sort of executable classical chinese. But there are two related problems with it: it's written in javascript, so no, I won't be using it, and more important, it's syntax is really just javascript in disguise, not really resembling actual literary chinese. For example, you have to declare variables using 吾有, which esentially maps to 'my' declarations in javascript. That's just one of the most obvious examples.
My aim is to make something that more closely resembles actual literary chinese, and there are a few design issues to be made. One of them is context. In classical chinese (hereafter CC) almost every sentence is governed by some context which 'fills in the gaps' in it's meaning. For this I take inspiratikon from lisp's notion of an environment, as well as the sort of "implicit" environment provided by Forth's stack model. Perl does this to an extent too, you may say 'print' without any arguments, and perl sort of 'knows' what to print. Likewise in CC, 學而時習之, the "pronoun" 之 is determined by context, in this case standing for '所學者', "that which is learned". The problem is allowing for a sort of multiple context, where different words alude to different implicit parts of speech, such as the subject, object, indirect object, and others. This leads to the question of arity to a function, and the roles that present or absent elements may play in relation to a given word. In theory, each verb would have a fixed arity, usually for up to 3 arguments, these being subject and direct/indirect objects. This imposes a limitation not usually found on function definition in other languages.
Another, more exciting development, is the versatility of a word. Refer to this talk: https://youtu.be/vBhg2p8aAQ0?si=CNLEhqcbXPrv7-W0, esentially, a word may be a verb (function) or a noun (data) depending on it's place in the sentence. In lisp already functions can be passed as data, but I will take a further idea from Category Theory, where a primitive datum is itself regarded as a function returning itself, thin of 1 being a function 1() returning itself.
I feel like I am just scratching the surface, and I haven't even gotten to implementation yet, it's all in the mind, you know.
Naomi Nascimento · 2024-01-02 07:18
>>38034
Numbers-as-functions is an older idea than category theory:
https://en.wikipedia.org/wiki/Church_encoding
array · 2024-01-02 21:58
>>38018
I made a thread for this kind of topic: >>28919
Still, I suppose that thread's basically mine alone by now.
>I'm curious about any languages lainons here might be designing.
I'm interested in designing tools, which lack the weaknesses of languages.
>What issues does it address?
Imagine programming without the possibility of a syntax error, for one.
>What are it's particular strengths?
It's a tool that removes as much drudgery from the task, and rewards learning, as possible. I think so, anyway.
>Are you stuck trying to incorporate certain features?
No, the issue is of a different kind. It's a separate world, and so everything's bespoke.
>I believe the biggest issue with language design is creating a good type system.
In that case, I recommend familiarity with Ada. Here's the conceptual representation for the built-in Boolean type in Ada:
type Boolean is (False, True);
Conceptually, False and True are now functions of no arguments that return a result of type Boolean, or whatever. To know this isn't particularly important in Ada.
The issue with representing integers using any kind of new method like this is the insanity and difficulty of doing it in practice. I believe in Smalltalk there's still special handling of them, because even if they are conceptually objects which respond to messages, it's just not convenient to actually write all that out most of the time.
>You can represent pretty much every operation as a simple lookup.
Yes, I had a similar realization. I'll link directly to my thoughts on this topic, if they would be wanted. Put simply, I believe all or most programs should be represented entirely as data structure operations. Rather than have code to make some conditional decision, what code exists should unconditionally use the result of some data structure access that encodes the variety instead.
In lieu of linking to my work, I'm going to link to something interesting I found recently. I call my thought by the name Tabular Programming, but there's a related and weaker idea called Decision Tables:
https://www.hillelwayne.com/post/decision-tables/
https://www.hillelwayne.com/post/decision-table-patterns/
Perhaps my greatest bit of advice right now is to reconsider semicolons, names, and everything else. Why use Int over Integer?
Crash Override · 2024-01-03 17:02
I think the most important thing to remember when designing your language's syntax is that correct, efficient code should look right, while buggy, wasteful code looks wrong.
It sounds obvious written out, but it's something that's pretty easy to lose sight of, especially when you're designing your syntax ad-hoc along with the rest of your language.
Zero Cool · 2024-01-05 17:44
I've been working on a really basic language for a while. The idea is that it is stack-based and low-level like forth, but with a key difference: instead of passing arguments on the stack, you have to explicitly move them into a new stack which is used to construct the stack of the callee, and then you pull from the callee's stack back onto your stack.
This turns out to make it a lot easier to write basic register-aliasing optimizations for RISC architectures where straightforward stack machines can be pretty inefficient. The result is essentially a high-level assembly language.
The compiler is almost usable. In a few weeks I will probably be able to show off some code and working examples. For now, an example program that I will be using as a test case:
:gcd %2 [ $1 $0 !! !! %2] [] [$0 $1 blt] .if
:gcd_inner $0 $1 $0 rem !! !! %2 #gcd_inner $0 bnz $1 ret
$n places stack position n in the argument list, ! pops the top stack position to the argument list, and %n pushes the top n values from the argument list. !! pops a value from the stack to nothing.
This is a function which takes two inputs. At input, the top of the stack is R_k and the second from top is R_k-1. gcd first swaps them if R_k < R_(k+1).
It calculates R_(k-1) = Q_(k+1)*R_k + R_(k+1). If R_(k+1) = 0 then return Q_(k+1), otherwise jump back to the GCD loop with inputs R_k,R_(k+1).
It should compile to something like this (using assembly pseudocode):
gcd:
blt r0, r1, if_true
if_false:
jmp if_finish
if_true:
mov r2, r1
mov r1, r0
mov r0, r2
if_finish:
gcd_inner:
mov r2, r0
rem r0, r1, r0
mov r1, r2
bnz r0, gcd
mov r0, r1
ret
Note that instructions only modify temporaries, but when % or ret is encountered the temporaries are moved in to stack positions, which are aliased to registers in-order. The intent there is that parameters at the top of the stack are often already in the position they need to be for function calls.
>>38044
Agreed. Especially when there is little to no IR layer (as in my case) it's very important to try coming up with compilation techniques that don't just result in efficient code, but also readable code. The code I've written here could be improved with more liberal use of macros, which would be straightforward to write. For instance a macro for swapping registers or performing operations on stack-order-reversed values would be very useful.
Netrunner · 2024-01-06 00:43
>>38056
Actually there's a bug in my test program, since I forgot arguments are passed in LIFO order rather than FIFO. That's a little lucky because it makes the test program simpler. The corrected program is this:
:gcd %2 [! ! %2] [] [$0 $1 blt] .if
:gcd_inner $0 $1 ! rem !! %2 #gcd_inner $0 bnz $1 ret
Additionally, I forgot about looping macros. A more ergonomic, and conpletely equivalent, way to write this with macros is:
:gcd %2 [ ! ! %2 ] [ ] [ $0 $1 blt ] .if
[ $0 $1 ! rem !! %2 ] [ $0 bnz ] .do-while $1 ret
The "until" and "do-while" macros will generally be preferred because they compile to faster code than an equivalent "while" macro. In fact I might not implement a "while" macro at all.
Already, looking at test programs has informed me about some aspects of language design.
Haruyoshi · 2024-02-08 13:59
Bumping.
I'm in the very early stages of a very low-level language (lower level than C), and I'm considering making all declarations static, with variables basically acting as aliases to memory addresses. There'd be some sort of way assign multiple variables to the same memory, for cases where you need different things at different times, but I'm not exactly sure how I want to go about it yet.
Is this a terrible idea, and if so, why?
Moore · 2024-02-08 14:45
>>38306
ObjectPascal ans Delphi have the '''absolute''' keyword for this.
https://wiki.freepascal.org/Absolute
https://docwiki.embarcadero.com/RADStudio/Alexandria/en/Variables_(Delphi)#Absolute_Addresses
Example of using:
https://onlinegdb.com/TukDm6UWo
program Test;
{$mode delphi}
var
IntegerValue: Integer;// same as int in C with Delphi mode
SingleValue: Single absolute IntegerValue;// same as float in C
begin
IntegerValue := 1000;
Writeln('Integer: ', IntegerValue);
Writeln('Single: ', SingleValue);
end. In this code, IntegerValue and SingleValue refer to the same memory location, if one changes, then the second changes.
Amazing how much '''absolute''' simplifies your code and gets rid of the ugly hacks you see in similar C code, especially now that compilers like clang/llvm are trying to destroy your code.
Backus · 2024-02-09 00:38
>>38306
The stack is used even in assembly, so it'd be odd if you lacked variables located at offsets to the frame-pointer or stack-pointer registers.
Also struct members are just offsets to a variable (static, stack, or pointer).
If you want the language to be more useful it makes sense to include those features, which are all just treating variables as offsets instead of absolute addresses.
Armitage · 2024-02-09 10:11
>>38308
I'm reluctant to add traditional automatic variables because, as far as I know, it mandates "the stack" or something similar, and I want to make as few assumptions about the hardware as reasonably possible. If there's a way to have automatic variables without needing "the stack", but also taking advantage of it if present, I'm all ears.
I'm also considering adding some kind of 'static pointer' type to handle situations where you need something at an offset. They'd exist for the entire lifetime of the program, but what specific chunk of memory they refer to could be changed at runtime, and that would probably help a lot.
For what it's worth, I'm trying to make something that's basically one step above a high-level assembler. I'm perfectly fine with programmers having to roll their own versions of things that most languages provide for them, because the alternative is Assembly, not C.
Apoc · 2024-02-09 13:40
>>38309
Unless you're in some kind of super specialized DSP, you can and should emulate a stack on a register machine very easily.
Register allocation is the most annoying thing about programming in assembly, so you should definitely implement that if your goal is to make a language that's like programming in assembly but better.
Nguoi La · 2024-02-09 18:26
>>38309
The stack is just reading/writing memory relative to a pointer.
Even atari2600 games used a stack and it had only 128bytes of RAM.
If the hardware supports pointers and has 128bytes of RAM a stack is feasible.
Programming for something that does not have even that requires a different approach & programming with the hardware always in mind.
So it doesn't make sense to omit a feature for the sake of program compatibility when the program cannot be compatible anyway.
If you want your language to target these platforms, you could have the compiler error whenever the code uses the stack.
So you have the ability to use the stack only if the hardware supports it.
Bob Arctor · 2024-02-09 19:20
>>38309
A call stack is only strictly necessary if you have unbounded recursion. If you know the entire (bounded) call graph beforehand you can statically allocate space for all variables. A clever compiler can overlap space for functions that will never need to keep other's frame intact.
If you're going the route of "removing" the stack, I'd also make sure you can't take references to "stack" variables to aid optimizations. While at it, I'd make memory look like an external thing the same way a file descriptor is an external thing. Mainly so people will think more carefully about memory access patterns and actual "zero-copy" since caches are an important thing to consider nowadays.
Case · 2024-02-17 08:30
>>38018
There is a reason why this set theoretic interpretation of type systems is not used. It is less powerful and more painful than modern martin-lof based theories.
St. IGNUcius · 2024-02-20 03:10
>trying to design a language before having tried at least CL, Forth, ML, Prolog and C
How cute.
James K · 2024-02-20 11:23
>>38389
>Forth
It seems to me like everything Forth adds on top of RPN is just a waste of time. Moreso than CL, Forth is the language of people who have strong opinions without good reasoning.
>>trying to design a language before having tried at least CL, Forth, ML, Prolog and C
It is often more interesting to see what someone comes up with outside of the influence of existing things. While the benefits of being well-read are multitudes, it tends coerce different people into producing a more homogeneous output.
J.C. Denton · 2024-03-10 14:16
Is if there's a way to decouple how a struct is accessed by a programmer from the way it actually exists in memory? I'm designing a high-ish level language, somewhere between Go and Java, and I don't want the programmer to have to think about the details of memory layout to get good performance.
James K · 2024-03-10 14:51
>>38515
They are called functions. Usually along the lines of "get" and "set". In C#, they have a very convenient abstraction for it called "properties". There may be some better solution for the problem you are facing, but I can't say more without further explanation.
Meddler · 2024-03-10 16:51
>>38018
>curious about any languages lainons here might be designing.
Glorified Macro Assembler, and a simple lang built from its macros.
> What issues does it address?
Cross platform beautiful assembler.
Compilers should STFU and do only Exactly as I say-- I'm way smarter than automated optimizations... E.g., if I multiply by 1 the compiler should do as I say: I may be trying to ballance a code path so it doesn't have a side channel timing attack vuln.
>What are it's particular strengths?
It's way quicker than C, you invoke the constructs like
#loop_unrolled (test exp){repeated code};
So you opt into code transforms.
The higher level lang is a synthesis of forward and reverse polish notation. It can look like RPN or infix or Lisp, but operator precedence is explicit. Without needing so many parenthesis (when airity is known, they can be omitted). Tired of PEMDAS based BS, PEMDAS is arbitrary and we have bitwise, dereference and etc. ops that algebra's PEMDAS doesn't address, so each lang has its owp precedence table (by the time I look it up I could have written the ops in ASM).
Basically ALL languages are crap, so I'm building a safer lang than C or Rust with a simpler context free assembly environment and building a higher level lang on top. There is no return oriented programming or stack corruption vulns because I formalized the stack, and made it different than C's. So I can resize a stack frame dynamically. Don't allocate a stack frame then invoke a fn() in that mem, instead use as much stack as you want, before you call another fn() mark the stack pos/ip in use, make that the new base of stack and jump into the new fn(). This new fn() can not access caller data on stack, and it uses as much stack as it wants. Ret always returns to caller and restores base stack address. You can give the called fn() part of the end of your stack frame, and recv stack results on the stack. There is no caller nor callee cleanup needed. Higher level lang can do this:
int testFn( int x ){
int dynArray[ x ] = {0};
//...
}
C-ish pseudo code for common medium of exchange. That dynArray will be allocated in quick stack memory. Stack is a mark/release allocator, quickest allocator in the world, but we are not using it in our langs because everyone does stack the way C does it, hemming oneself into fixed frame-size and making dynamic stack usage impossible or expensive... I can push/pull on stack without changing address of stack vars. I can build code that runs way faster than comparable algos written in C.
I bootstrap the compiler backwards, start at linker then implement parser, build a lexer/tokenizer last. Wouldn't it be great if Clang had modularized the lexer so you can just replace the parser to build new langs? My macro libs are ordinary programs that speak an IPC protocol for RPC so the complier/assembler can be extended at compile-time.
TL;DR: meh, rethinking everything about CS from the ground up. Profs are teaching old and busted tech.
Lord Nikon · 2024-03-10 17:02
>>38516
I mean making the physical memory layout of structs separate from the way they're called within the language semantics.
In C (the compiled language I know the most about from a low-level technical standpoint), a struct is basically a fixed-length heterogeneous array. If I create a struct containing a float, an int, and a char, the result will be, more or less, a float, an int, and a char sitting next to each-other in memory. If you create an array of these structs, the result will be float int char repeated for each element of the array. At least, that's how I understand it.
In my language, I want it so that that isn't necessarily the case. If the compiler decides that storing it as int int int char char char float float float is more efficient than storing it as float int char float int char float int char, then it should be able to.
James K · 2024-03-10 17:42
>>38519
I think JBlow did something similar to this in JAI called SOA. You don't actually have to do much special to achieve this. The language will naturally have the semantics of getting and setting values, so the abstraction will already exist within code. The approach you take will depend on what characteristics you want the system to have. For instance, you could declare that all structures you wish to optimise hold array fields and an offset into those fields.
struct S {
Int f1;
Float f2;
}
// After mapping
struct S {
Int *f1;
Float *f2;
int index;
}
Then you represent single instances of that type as arrays of length 1. This has the advantage that the new type fits in a single pointer which might be a desirable proptery for boxing. A disadvantage is that memory usage will slightly increase for object references. Additionally, you lose the identity property of array elements (for some array A, A[i] != A[i]).
To address memory usage, you could hold the actual information at the start of the allocation for the fields, then just take a reference to this. It performs better for arrays but worse for individual allocations. If you are willing to lose the boxing property, you could store references to that structure as fat pointers. You could also store all pointers as fat, which would allow for these to be boxed and additionally solve the issue of interior references at the cost of a good amount of memory. Any approach here will require tradeoffs.
I would question more fundamentally if it is even worth it. The amount of code in applications which would significantly benefit from these optimisations is small, and it is likely possible to do them by hand.
Spooky · 2024-03-10 23:59
>>38515
>>38519
Studying the way Ada handles things with representation clauses may be instructive if you're looking for ideas to copy. Really though, what you describe can be done just by being careful with how the language definition specifies requirements for structures and arrays.
https://en.wikibooks.org/wiki/Ada_Programming/Representation_clauses
http://www.ada-auth.org/standards/rm12_w_tc1/html/RM-TOC.html (check chapter 13)
Backus · 2024-04-08 17:17
I'm designing a static OO language with a flat type hierarchy (classes exist but there's no inheritance).
I want to keep the number of core language constructs down to a minimum, so in lieu of enums, I'm going to have classes contain static consts. I also want user-created types to integrate into the baseline language as naturally as possible (and vice-versa), so I'm considering allowing class constants to be accessed directly, without a namespace operator, if they're used in one of said class' methods. For example:
// in somewhat rushed pseudocode
class color{
private static const int red = 0;
private static const int green = 1;
private static const int blue = 2;
private int value;
public void operator=(int x){
self.value = x;
}
public bool operator==(int x){
return x == self.value;
}
}
void main(){
color foo;
foo = red;
if(foo == red){
"foo is red".print();
}
}
I just thought of this a few hours ago, so I want to know if it's a bad idea. Because right now, it seems like a pretty elegant way to make the language more flexible.
J.C. Denton · 2024-04-08 20:38
>>38757
Seems over complicated. Perhaps look at smalltalk and self for good minimal oo system.
Arisu Mizuki · 2024-04-09 03:51
>>38757
Also enums aren't very OO. In a more proper OO system, boolean reflect the church booleans. I.e. true and false are Subclasses of Boolean, and method is done on object dispatch. Thus enums can be transposed into a view of a class where there are closed Subclasses. If you store objects, youre environment could project it to make it look like you are casing on it rather than viewing them as indivual methods if you so desired
Nameless · 2024-04-10 08:42
>>38761
>>38765
I've messed around with Smalltalk (Squeak) a decent amount, and I like what I've seen of it, but from what I can tell, its way of doing things is heavily reliant on dynamic typing and message-passing, which my language doesn't have. If there's a way to bring that sort of thing to a very static language, I'd be very interested.
the Mailman · 2024-04-10 11:17
>>38018
Interesting!
This is a Turing-complete type system, with all that implies.
IfThenElse[False, X, Y] = Y;
IfThenElse[True, X, Y] = X;
# We map from the type system to set theory by treating any set
# S as a function with X in the set if S[X] has type True.
# X contains itself if X[X] is true.
# This could simply be written
# ContainsItself[X] = X[X]
# But this is arguably clearer.
ContainsItself[X] = IfThenElse[X[X], True, False]
# Y contains X iff X does not contain itself
Y[X] = IfThenElse[ContainsItself[X], False, True]
# Does the set of all sets that do not contain themselves, contain itself?
Boom = ContainsItself[Y]
Void · 2024-04-10 16:48
>>38777
Yeah, nothing about smalltalk requires or doesn't require types. You can type it. Strong talk was an attempt at it and the idea of plugable types exist for smalltalk. Basically you can just slap static types on smalltalk easily
Further messages are just objects. Everything is an object it's what you get when you simplify the OO system. Classes are meta objects. The example I gave with booleans can easily be typed and done In Java, if Java wanted to write more OO style code.
hash table · 2024-04-10 16:55
>>38786
Message sends are just a view into a conceptual framework. It's just another way to say method call. The messages one receives are just methods.
There is more i can go on about, but smalltalk is a pretty good example of a very small primitive set scaling into being a full blown programming system. Types are just a static analysis tool that can be added. I'd recommend reading Types and programming languages (TAPL), as it covers type theory well and gets into object calculus later on
Miho Iwakura · 2024-04-10 21:10
>>38786
>>38787
Thanks for the resources. Would that sort of thing would still apply to a language without subclasses?
My knowledge of OO mostly comes from my experience using OO languages over the years, so I'll fully admit that there's gaps in my knowledge when it comes to the deeper conceptual aspects of it.
Gyula Kovacs · 2024-04-11 02:16
>>38791
Well in what sense are they objects and not simply modules? People call erlang OO even though it's just module based and actors accept "messages" (message sends here aren't just another name for methods, though we can conceptualize them as the same).
A good essay that gets at the core of OO is cooks essay
https://www.cs.utexas.edu/~wcook/Drafts/2009/essay.pdf
This namely covers OO vs ADT style abstractions and designs. Maybe you really want an ADT style language?
If you want that Xavier Leroy has a lot of essays pushing modules, but IMO they are just trying to get at the subset of Objects they really want.
I'll be in the irc in #programming in 2 weeks, i can give you more resources and argumentation there. Just ask for the programming languages/systems person they'll point you to me
Gyula Kovacs · 2024-04-11 07:19
>>38792
>Well in what sense are they objects and not simply modules?
They're still instances of classes. 42 is an instance of int . [1, 2, 3, 4] is an instance of array<int, 4> . Everything that can be assigned or passed around works that way.
hash table · 2024-04-11 07:35
>>38793
That's just sugar over the `t` idiom of modules.
In Erlang/Elixir you have `String.t()` you can only have 1 type per module that you make with `typedstruct`, and you make functions on the type itself. Further in languages like OCaml, the `Foo.t` idiom is enforced via module functors, where you basically get composition of objects via the moudle system.
Thus suppose a language `X` exists that works just like `OCaml` or `Elixir` but happens to cut off the `.t` part of the type meaning that we now have `String` or `Integer`. Then what is the difference between your system and this?
Iverson · 2024-04-11 10:53
>>38794
You're probably right. I don't think that I actually want a 'proper' OO language, a-la Ruby, Smalltalk, or even Java, so much as I want something that's kind of shaped like them. I like classes as a way to describe data types, but that's it, really. I'm not all that interested in doing anything particularly OO with my language beyond that.
Honestly, my main takeaway from all this is that I need to study up on the academic, theoretical side of this stuff. As a programmer, I'm largely self-taught, so most of my knowledge of this stuff comes from what I've learned by trial-and-error, writing code. I don't have a particularly strong background in actual math or logic, so a lot of that kind of thing is lost on me.
Switch · 2024-04-11 14:54
>>38796
Go study lisp. I'd recommend common lisp for a programming environment, and translating SICP into it to get a feel of that kind of stuff.
The fundimentals of CS aren't particularly hard, however you should question the core of ideas, and the core of abstraction. Being self taught vs going to university doesn't change much, as universities are poor at teaching computer science in the current day.
Nanashi No Gombe · 2024-04-23 18:59
Why do languages that aim for type-safety like Rust and Haskell also use type inference? I would assume that if types were so important, you'd want to know what everything was in advance.
James K · 2024-04-24 00:20
>>38932
Because type inference ideally only infers things which are known in advance. There is a certain perspective that you should always write out the types of your interface to avoid changing them by accident, but Rust forces you to do that anyway. Hence type inference is just something that makes the body of a function easier to write without affecting code in any other way.
McCarthy · 2024-04-24 12:37
>>38932
>>38933
one point is avoiding writing 10x " function add(x,y) return a+b; " for each data type.
You write it agnostic once and let the compiler infer data type so it can generate a copy for each used data type using the correct + function.
Outside typing less there is also the trap of implicit casting: If you define a function with fixed data type and pass another type it will just convert it to the function's fixed data type. That can be neat used intentional or a unintended mess in combination with class inheritance. Automating types prevent this.
Personally i think you should either carry a data type entirely through the entire chain which makes it easier to read whats going on and enjoy a warning if you cast implicit of just be agnostic entirely which equals ensuring never changing data type. Mixing is when things go wrong.
() · 2024-04-24 12:46
>>38937
>one point is avoiding writing 10x " function add(x,y) return a+b; " for each data type.
You write it agnostic once and let the compiler infer data type so it can generate a copy for each used data type using the correct + function.
But that's generics, not type inference.
James K · 2024-04-24 13:43
>>38937
None of this seems to have anything to do with type inference.
Six · 2024-04-24 15:22
On the topic of type inference, I find it interesting that most languages, even largely nominally typed ones, have a simple form of type inference with literals. The specifics vary depending on the language, but in general, compiler will decide that 123 is an integer, 123.0 is a float, and "123" is a string, without any need for explicit type annotations.
bit · 2024-04-24 17:25
>>38938
>>38939
type interference as i understand just means automatic type detection for an expression which feeds back to applying types to generic functions and automated casting.
>>38940
.0 and "" are explicit type declarations. Automatic would be if you could write 123 and the compiler decides if its int, float or string on context.
Hacker · 2024-05-24 14:34
Bumping. Is there a clean approach to type-inference on records/structs that doesn't muck things up elsewhere?
OCaml comes close to what I want, but it has weird quirks that make things more annoying than I wish they were. For instance, writing
type foo = {x : int; y : int}
type bar = {x : float; y : float} causes the compiler to forget that foo exists and yell at you if you try creating an instance of it.
Is this sort of thing simply a limitation of the way type-inference works, or is there a better way?
James K · 2024-05-24 17:29
>>38941
>generic functions
These are the basis of any non-trivial type inference, but you can have generic functions with no inference as long as you specify the generic type every time.
A simple example would be Lean, where you can take either approach.
-- round brackets for explicit argument
def explicitIdentity (t : Type) (a : t) : t := a
-- curlies for an inferred one
def implicitIdentity {t : Type} (a : t) : t := a
def two : Int := 2
#eval explicitIdentity Int two
-- => 2
#eval implicitIdentity two
-- => 2 Try it out at https://live.lean-lang.org/.
aliased · 2024-05-24 21:10
Continuing from >>39181, SML, the Other ML Family Language (which is ironic, considering it predates OCaml by a good while), in the purest form of structural typing I've ever seen, treats records more-or-less as tuples with named members; singular things that can be stored in data-structures or passed to functions that accept other similarly shaped singular things. This approach eliminates a lot of the weirdness you see in OCaml, but you sacrifice type-inference for functions that take records.
val foo = {bar = 123, baz = "abc"};
fun my_function x = #bar x; is not allowed. You need to write
val foo = {bar = 123, baz = "abc"};
fun my_function (x: {bar: int, baz: string}) = #bar x; (or some abbreviated version thereof) instead.
I wish I could see how Haskell would handle such a thing. It has, to my knowledge, the most advanced type-inference around, but its approach to records is so laughable that it arguably doesn't even have them at all.
HELIOS · 2024-06-02 11:36
>>38018
For functional programming, is there a more cache-friendly alternative to linked lists that still maintains their quick insertion and deletion?
I'm trying to design a very simple functional language, somewhere between a compiled Scheme, a stripped-down Haskell, and a functional C. I only want there to be two core data-structures: records, and some kind of collection, so they both better be fast.
vector · 2024-06-02 12:43
>>39274
A chunked linked list is the simplest option: like a linked list but each node can hold multiple elements.
A B-tree might be better for frequent accesses in the middle of the list but is a lot harder to implement.
Junichi Fujiwara · 2024-06-02 13:14
>>39276
Got any links, specifically about how they compare to linked lists? I already knew about both, but I'm not 100% sure how they differ in practice.
Unnamed · 2024-06-02 14:57
>>39274
Unrolled linked lists are a thing. They still are not suitable for random access, though.
There's also a thing called a skiplist which is like a compromise between a linked list and an array. A skiplist built on top of unrolled linked lists would probably be an ideal drop-in replacement for standard linked lists.
James K · 2024-06-02 22:10
>>39274
Clojure uses B-tree structures with a high branching factor for its vectors. They are immutable but give a good balance between lookup and insertion.
Just another Random Lainon · 2024-06-03 18:18
Should I treating URLs and pointers identically to allow the ideas and patterns around pointers to carry over to distributed systems?
Lukasiewicz · 2024-06-04 08:33
>>39289
>They are immutable
Good. I want immutable data structures.
藤原純一 · 2024-06-05 00:33
>>39291
You should be treating URLs and pointers identically to allow the high-reliability ideas and patterns around URLs to carry over to centralized systems.
Aristóteles Amadópulus · 2024-06-17 11:05
I (>>39274) am trying to come up with simple ways to allow for state while still encouraging an overall functional style.
My first idea, and the one I'm pretty committed to at this point, is to jettison all imperative control-flow structures. No for and while loops, just maps, folds, and recursion.
The second, which I'm not quite as sure about, is to disallow mutations on non-local bindings. For example,
foo = [2, 3, 1];
fun bar(baz){
baz = map(fun(x){x + 1}, baz);
}
fun main(){
bar(foo);
} wouldn't compile, and would instead need to be written as
foo = [2, 3, 1];
fun bar(baz){
map(fun(x){x + 1}, baz);
}
fun main(){
foo = bar(foo);
}
I want to know if there's any prior art for this local-mutations-only approach. The goal is to deal with stateful problems gracefully and simply, without it overtaking the whole language.
string · 2024-06-17 18:03
(>>39349 cont.)
Part of the reason I want local mutations only is that it makes changes in state a lot more explicit. A reader need not ask "did one of these functions fuarrrk with my bindings?", because your answer lies right in the current scope.
Frank Garza · 2024-06-23 01:21
>>39349
Well if you have local mutation... I need only to make a function called for. Basically giving you a value in a lambda to do some work. If the language has macros, then the sugar can effectively resurrect these forms [1][2][3].
For your general idea of local mutation only, there has been some work done on finalizing values after some creation period by cliff click. So if you are going at it from an optimizations POV, then what you care about is scoping and what you can do with functional values.
Syntax wise, it's odd to make = mutate when you want to discourage mutation. Your syntax sort of combines let bindings with mutable setting, which makes it a pain to discern what your code was intended to do.
[1] https://docs.racket-lang.org/guide/for.html
[2] https://novaspec.org/cl/f_loop
[3] https://hexdocs.pm/elixir/comprehensions.html
Note that [3] works on no mutation so it's just sugar over maps and folds effectively.
Terzibashjian · 2024-06-23 15:39
>>39397
>Syntax wise, it's odd to make = mutate when you want to discourage mutation
You're probably right. I'm using C-with-optional-type-annotations as vague baseline, not because it's the best, but because I'm familiar with C. If you have any suggestions for a better declaration syntax, I'd genuinely love to hear them.
Donna Hawthorne · 2024-06-24 23:11
>>39400
Yeah well := is probably better for assignment and = is better for assignment.
this is because it taxes assignment with an extra character, and = looks more like mathematical =, being used for variable binding.
Note this is the opposite of golang, but like most things golang is wrong.
I'd suggest in the future to branch out of C like languages and syntax, there is a lot of power to be had by just getting rid of syntax
Ender · 2024-06-25 08:52
>>39413
>Yeah well := is probably better for assignment and = is better for assignment.
I beg your pardon?
Drano · 2024-07-14 13:06
How far could you get with pure immutability, where all bindings stay the same for the entirety of their (semantic) lifetime and cannot be mutated whatsoever?
Every functional language I've ever used included some kind of built-in mechanism for mutable bindings, but I'm curious if that's actually necessary? It seems to me like it's possible to do everything with recursion and the like. Am I right? And if so, why isn't this the way functional languages actually work?
Wintermute · 2024-07-14 15:36
>>39557
Real objects are mutable, so mutability tends to be necessary to deal with the external world.
One way to get around it is to have some kind of opaque I/O object, and a way to restrict functions so that the semantic lifetime of the inputs must end when the function is called. Then you have some kind of function query_state: state_obj,query -> state_obj,query_result with the state object always being consumed. Some syntactic sugar could make this not much of a pain at all (just rebind to the same name). I believe Haskell does something like this.
Boole · 2024-07-14 16:32
>>39557
>>39558
To add to this: linear or affine types here are useful to avoid accidentally reusing a consumed object, which I assume is what anon means with "a way to restrict functions".
Yuki Nagato · 2024-07-15 12:49
>>39558
>>39561
I'm not super familiar with the way Haskell does I/O and other stateful stuff. I've used it before, but nowhere near enough to claim a proper understanding.
That aside, the goal is to get rid of internal state specifically. Things like I/O are inherently stateful, but it's state coming from the outside in; state that gets worked out within it's own little subprogram that then hands off the results to the main one. It's fine for that sort of thing to be openly stateful, because the state's self-contained.
My main concern is mutable variables, and the removal thereof. Once something is bound to a name, I want it to refer to (semantically) the same thing for the rest of that binding's (semantic) lifetime. If you declare that x = 15, x should continue to refer to 15 until it stops existing or gets shadowed.
Iverson · 2024-08-14 07:55
>>39557
An idea I recently had and I'm working out now is to keep inherently impure things, like I/O, entirely external by sending messages (and a continuation) to the caller, which will eventually end up at the root of the VM.
No functions end up ever directly referencing/"owning" some impure state, so you can e.g. freely make copies of it without worrying about holding invalid state.
The language I'm designing only has atoms ('nil, '0, '1 ...), pairs ([x . y]) which are like LISPs cons, and functions (\x -> x, \'0 -> '1; '1 -> '0 ...) with pattern matching.
It also has a bunch of syntax sugar to keep code shorter and clearer:
- A list is just a chain of pairs: [x,y,z] is the same as [x . [y . [z . 'nil]]]. [] is the same as 'nil.
- You can make intermediate bindings in a scope like
x = 'a
y = [x,'b]
[x,y,'c] which becomes
(\x -> (\y -> [x,y,'c]) [x,'b]) 'a - Functions only take one argument, but you can have "multi-argument" functions, \x y z -> [x,y,z] becomes \x -> \y -> \z -> [x,y,z]
- Integers are lists like 6 = ['int '0 '1 '1], UTF-8 strings are ['utf8 ['int '0 '0 '0 '1 '1 '0 '1] ...] etc
About message passing for impure functions: you just return a value which is then interpreted however by the caller.
For example, to print a line
f = \'ok -> ...
['println,"Hello, world!",f] 'println tells the caller to print the next argument out, "Hello, world" is a (UTF-8? Unicode?) string, f is the function to call after printing.
Since the callee has no direct knowledge of the caller it's free to change it's state at any point. Good for the real world where the unexpected happens.
From what I find it is similar to algebraic effects. Probably where I got the idea from.
You can also implement "modules" by using atoms and a function with many branches:
foo = "foo".
bar = \x -> [x,"bar"].
\'__DOC = "FOO and BAR(x)!"
; 'foo -> foo
; 'bar -> bar
One somewhat awkward part though: I want to avoid cyclic data structures (since it allows proving termination in more cases and avoids the need for cycle detection) + I'm trying to optimize it for interactive editing and testing, which means you can't do this:
x = [y].
y = [x]. or this:
f = \0 -> 1; \x -> int 'mul x (f (int 'sub x 1)).
f 10 And instead need to write this
f = \f -> \0 -> 1; \x -> int 'mul x (f (int 'sub x 1)).
f f 10 (Like an fixed-point combinator)
Mitnick · 2024-08-17 17:44
> For integers it does not make sense to have a negative exponent, as you cannot represent fractions. We want to enforce that BL <= BH and 0 <= EL <= EH.
> BL <= BH and EL <= EH can be enforced by (ab)using pattern matching for type arguments:
I think Adga does this with dependent typing (which Haskell doesn't have), where arguments can change the type of a function. `pow` could be a function that takes in 4 integers, then outputs another function with the inputs and return type of those restrictions.
https://agda.readthedocs.io/en/v2.7.0/getting-started/what-is-agda.html#id1
HELIOS · 2024-08-24 12:26
>>39814 (me)
I've finished a bootstrap interpreter written in C. It uses a tracing semi-space GC since a lot of short-lived objects are created, which is an ideal case for a tracing GC which only looks at live objects.
To reduce memory usage and number of allocations I keep the type tag in the reference. Putting atom IDs in the reference too then allows avoiding an allocation altogether. This makes the implementation trickier but it should be more efficient overall.
The interpreter still uses pairs for lists, although I've redefined the language to have true lists so I don't have to deal with or think about improper lists.
While it works well and seems... fast for what it is, the nature of the language makes even integer operations not only dead slow but use a huge amount of memory.
Integers are represented as a single list of atoms, starting with 'int and followed by some number of '0 and '1.
Each element of the list is a pair, the left part being the value and the right either another pair or NONE.
References are 32 bits each. An atom is stored in a single reference with tag + ID (no heap allocation). Pairs are 32*2 = 64 bits each.
So for example, the 8-bit integer 0b01110111 would be represented as ['int,'1,'1,'1,'0,'1,'1,'1,'0].
9 elements -> 9 pairs * 32 bits = 288 bits, or 36 bytes just to represent an 8 bit integer. Ouch!
My plan is to have a fancier VM which can recognize such patterns and store them in a much more compact data structure, as well as have it recognize certain functions and translate them directly to certain host instructions.
However, doing this dynamically and at runtime will take a lot of effort and time and I clearly need something better in the interim.
I'm still pondering over it, but I believe the best approach is to define a superset of the language, then translate a program from the base language to this superset while looking for certain patterns.
Recognizing integer values should be easy: check if it's a list, starting with 'int and followed by one or more '0 and '1 atoms. A check to see if it's normalized is also necessary, it's probably worth looking again into integer formats that are guaranteed to have a single representation for every integer.
Recognizing functions is harder, but I might be able to "cheat" here: functions are opaque objects, which means I can substitute it with a builtin and there's no way for the program to check what it actually is (not directly at least).
This means that if I can pass my own implementation of functions a la dependency injection I can easily substitute the "default" implementation with whatever is optimal for the platform. Like this:
# examples/factorial.crash
\int -> # dependency injection
(\f -> f f)
(\f p 0 -> p
;f p n -> f f (int 'mul n p) (int 'sub n 1))
1
10
Or maybe, to help avoid errors:
\['int,int] -> ... Doing this is fairly annoying currently, though I need to experiment more with it.
Another option I've explored is wrapping values in functions which, like stated above, allows me to keep the actual value opaque. However, I can't pattern match such values which makes it lose much of it's value for common types like integers.
It might turn out to be a neat way to emulate OOP a la Smalltalk however. An example (which I'm still working on, since I don't find it satisfactory):
__DOC = "Demonstration of (ab)using functions as opaque objects
with an OOP interface.
".
int = {../lib/int.crash}. # directly import an external module
new = (
fix = \f -> f f.
wrap = (\x -> \'_example_hasher -> x).
expose = \OBJ feed_int digest ->
\'feed_int -> feed_int OBJ
;'digest -> digest OBJ.
digest = \OBJ -> OBJ '_example_hasher.
feed_int = \feed_int OBJ x -> (
state = OBJ '_example_hasher.
state = int 'add (int 'mul 33 state) x.
state = int 'and state 4294967295.
expose (wrap state) (fix feed_int) digest
).
expose (wrap 5381) (fix feed_int) digest
).
module =
\'new -> new
;'__DOC -> __DOC.
(
hasher = module 'new.
hasher = hasher 'feed_int 7.
hasher = hasher 'feed_int 42.
hasher = hasher 'feed_int 1337.
hasher 'digest
)
And of course, I could simply make integers a core type of the language. But then what about floats? Or BCD numbers? Or...
For that reason I'd rather stick to only atoms, lists and functions if possible and optimize ad-hoc.
Otto Normalverbraucher · 2024-08-28 03:05
>>39878
While working on the "superset" interpreter, for which I'm going to use a DSL which is translated to LLVM IR, I happened to need some sort of associative map (dict).
In the process of writing it I've refined the OOP approach:
# dict.crash
\[] ->
fix = \f -> f f.
new = \cmp -> (
get = \WRAP L key ->
(fix \FIX ->
(\[[k,v]:l] ->
(\'1 -> v; '0 -> (FIX FIX) l)
(cmp k key)
)) L.
del = \WRAP L key -> WRAP (
(fix \FIX ->
(\[[k,v]:l] ->
(\'1 -> l; '0 -> [[k,v]:(FIX FIX) l])
(cmp k key)
)) L).
add = \WRAP L key value -> WRAP (
(fix \FIX ->
(\[[k,v]:l] ->
(\'1 -> [] 'duplicate_key; '0 -> [[k,v]:(FIX FIX) l])
(cmp k key)
;[] ->
[[key,value]]
)) L).
(fix \FIX L ->
\'get -> get (FIX FIX) L
;'del -> del (FIX FIX) L
;'add -> add (FIX FIX) L) []
).
\'new -> new It's still fairly boilerplate heavy, but it's a consistent pattern so not too hard to apply.
Here's example usage:
# test/dict.crash
dict = {../dict.crash} [].
cmp = \'a 'a -> '1; 'b 'b -> '1; 'c 'c -> '1; _ _ -> '0.
d = dict 'new cmp.
d = d 'add 'a 'x.
d = d 'add 'b 'y.
'x = d 'get 'a.
'y = d 'get 'b.
d = d 'del 'a.
#'x = d 'get 'a. # will raise exception
'y = d 'get 'b.
['dump,d,\_ -> ['exit,'ok]]
Nameless · 2024-09-19 17:51
Is it possible to have a concatenative language (think Forth or Factor) that is also fully statically typed? Off the top of my head I can't think of a single example of this in the wild.
Batou · 2024-09-21 08:27
>>40081
Does StrongForth or StrongForth.f count?
>https://www.stephan-becher.de/strongforth.f/
>"StrongForth.f is an implementation of StrongForth in ANS Forth"
I have never used it, tho!
Case · 2024-11-17 13:01
Some general language design thoughts:
The problem with designing your language to be "low friction" is that the best solution isn't always the most obvious one. If I think back to when I was young, there's a lot of common programming constructs I accept now that felt very odd and unnatural when I was starting out. I thought gotos and labels made a lot more sense than structured control flow, that having lexically scoped variables by default was weird and confusing, and that strong typing was dumb because "the computer should be able to figure out that 1.5 + "1" is 2.5". Now that I'm older, I strongly disagree with my past self on that kind of thing, but it took me a while to get to that point; to learn the correct way of doing things and why it was the correct way of doing things. "Low friction" also often ends up meaning "like what I'm used to", which loops back around to the same issue; the solution you're most comfortable with isn't necessarily the best solution. It took us way too long to drop C's awful declaration syntax from new languages, and it's because everyone was copying C for the sake of "programmer familiarity". Frankly, if a programmer is unable to learn something new, they're probably not worth hiring to begin with.
I think one of the biggest problems with modern software is that we don't have a big, mainstream language for intermediate-level programmers. You used to have Pascal, which existed at about the same level as C but was a lot stricter and featured a lot more safety checks, and various BASIC dialects that were mostly high-level and often interpreted, but also allowed you to drop down to lower levels and manipulate the hardware fairly directly (down to specific memory addresses!) if you wanted to. Both of these fizzled out by the early 2000s, and for the life of me I couldn't tell you why. Nowadays, all of the mainstream languages are either aimed at bad programmers, like Go, or geniuses, like Zig, and I think that's a mistake. Bad programmers don't produce worthwhile code unless they're being babysat by a good programmer, at which point you might as well cut out the middle man and only have the good programmer. And on the other end, truly great programmers, your Carmacks and your Iwatas, are far and few between, so it's generally not worth using tools built with these kinds of guys in mind unless you really need it. But programmers who are simply Good, people who know how to manage their own memory, know how data structures and algorithms work, and are capable of reading low-level code, but wouldn't necessarily think to do something like performing a bit shift instead of a multiplication because it's faster, are fairly common, and produce code that runs more than fast enough for most situations, so it's only logical that we should try and accommodate them first and foremost. Intermediate languages are also good from a pedagogical standpoint. Back in the 80s, a lot of people would start with simple "Hello World"-type programs in BASIC, and then gradually work their way up to POKE-ing memory addresses manually to make the computer to nonsense. You really don't have that kind of pipeline now; either you're completely insulated from the hardware like with Java, or exposed raw no rubber like with C, and it makes it a lot harder for a beginner to organically move on to the harder stuff.
I know this is obvious, but functional programming really does hit a wall as soon as you need to do something involving state, which is why, to my knowledge, every single functional language provides some kind of escape hatch. That said, there definitely is wisdom in avoiding unnecessary state, and especially in keeping state local. I'm of the opinion that 99.9% of the state in your program should either be entirely local, or happen to variables that were explicitly passed into the function mutating them. The vast majority of problems I've had dealing with other people's code came from things changing state miles away from the current function and me needing to track down what happened and where.
C-style braces are probably the best way to denote a block of code, being less verbose than slapping an "end" at the bottom of everything, and less ambiguous than Python-style indentation. But I've always had a softspot for the Ada way of doing things, where you write, for example, "end if" at the end of an if block. It's definitely verbose, even moreso than simply typing "end", but it also makes where you're coming from a lot easier to keep track of in long functions and deeply nested control-flow. Part of my affinity for this kind of thing might nostalgia though, because you don't really see it that much anymore.
Isn't it weird that most languages don't have things like graphics support, GUI toolkits, networking, etc. as part of the standard library? These are very common problems that are all kind of a pain in the ass to solve yourself; seemingly the perfect candidates to have solutions that ship with the language itself. Almost nobody does it though (off the top of my head the only example I can think of is Java), and my theory is that it's because nobody else does, creating a kind of feedback loop.
Portability of language and portability of code are often at odds with eachother. A language that allows implementations to defer to the hardware is going to run very quickly on a lot of platforms, because the code is so close to the assembly. But this also means there's a good chance the same program will only work correctly on a subset of the hardware the language supports, because different hardware works differently. On the other hand, a language that goes out of its way to specify the exact properties of every single type and operation is only going to run well on hardware that implements those types and operations similarly to how the language itself does. But in return, you know for a fact that your program will at the very least run on every platform your language supports.
On a related note, performance is a characteristic of an implementation, not a language itself. C isn't fast, but C is designed in a way that makes it easy to write good compilers for it, being a small language that purposely leaves a lot of things vague so the compiler can do whatever works best for the environment C's being compiled for. I know this sounds pedantic, and it kind of is, but it's an important distinction to make.
James K · 2024-11-17 15:40
>>40445
I think the reason you don't see GUI toolkits in most languages is that it would be far too much effort. Probably moreso than the entire rest of the compiler. It just doesn't really make sense to provide something so complicated as a part of the language when installing libraries is as easy as it is nowadays. Another issue is that things like GTK exist, which can be embedded in most languages. What you can provide as a language designer might end up being worse than just using that as a library, which would put you in an awkward spot.
Cat Mother · 2024-11-17 16:25
>>40446
IMO the most elegant solution is to provide a widget API; something that wraps around various existing widget toolkits, rather than actually doing any of the heavy lifting itself.
xolatile · 2024-11-26 10:20
>>38018
High level assembly language, with custom preprocessor that has Ada-like syntax (or Porth Pascal or Fortran if you'd like), and it's not line-based like ELENA or m4 preprocessors.
Copyright there is a joke...
sizeof · 2024-12-07 16:12
I'm trying to design a language, intended for pedagogical purposes, where the language allows and expects users to define their own language constructs, all the way down to the syntax. The idea is that users start with basically nothing, and learn how things like control flow, memory management, object systems, etc. work by implementing them themselves.
I'm trying to look for prior art for this sort of thing. Racket and Smalltalk are the closest to what I'm looking for, and are what inspired me to do this in the first place, but I'm looking to make something that's statically typed and, by default, is around the same level as C.
Spike Spiegel · 2025-01-03 16:11
I was thinking of making an interpreted language with the main datatype is a SQLite table/record
kinda like lua where the main datatype is a SQL table
My first approach was to build a preprocessor that would """compile""" SQL statements with special macros into a stored procedure to be stored as a BLOB in an in-memory sqlite database
however I realized this was stupid and writing parser in C is pure pain
I found a fork of sqlite that has support for stored procedures however desu i kinda wanted to use plain sqlite desu
found a neat tutorial on building your own interpreter in python, gonna start from there, but currently im preoccupied with other projects
theres some equivalence of SQL tables and records and OOP classes and objects that need to be fleshed out
Case · 2025-01-05 00:17
>>38798
>as universities are poor at teaching computer science in the current day.
As someone who is about to finish their bachelors I would trust a random retard on here more than most of my teachers when it comes to programming. Top uni in my country and half of the people that pass it dont know how to do anything beyond simple school projects that were spelled out to them and even then they are wrong.
Im going on a tangent at this point but we had an assignment regarding clustering and the acceptable runtime of the program was 1 hour, 90% of people had it in range of 15 to 30 minutes. Anyone who was not retarded (maybe 5%) had it in seconds. These people will then go onto work for financial institutions and write java to make all of our lives worse.
Rick Deckard · 2025-01-11 07:30
im thinking about making macros for assembly in an os called "Kolibri" since its a pretty easy asm language to understand. it seemed cool to me that there's a syscall that directly opens a window.
Project 2501 · 2025-01-11 07:52
>>40922
btw does anyone know how to run qemu without it having access to ethernet or internet?
i couldnt find a way.
Batou · 2025-01-11 12:23
>>40923
If you want to disable network access entirely I think
-net none should work
segfault · 2025-01-23 09:02
I'm playing around with making something similar to makefile, but where deps and targets can be xpath selected nodes in XML documents.
Unknown · 2025-03-01 01:45
>>41010
why would you use xml by choice?
Drifter · 2025-03-01 02:49
>>41307
Not him, but what else is there? JSON crap? Weakly specified (no comments, lol) and the ecosystem isn't as stable or ubiquitous as xpath/XSLT.
Kay · 2025-03-05 08:36
>>41308
>Not him, but what else is there? JSON crap? Weakly specified (no comments, lol) and the ecosystem isn't as stable or ubiquitous as xpath/XSLT.
Well if you are making a build system then I think it would be better to have a format that's as human readable as possible. If you take CI/CD stuff as an example they all use yaml. You also don't need an ecosystem around the format if its an input for your tool and you won't really have other software consuming that like in the case of web APIs.
buzz · 2025-04-16 01:30
I have been thinking about the typing rules and integer promotion rules for a C-like language. Here's what I came up with concerning binary arithmetic operations:
- each operand must have the same width and the same sign
- in general there is no integer promotion
However, as a quality-of-life feature, limited and very strict integer promotion rules are applied to integer literals and only to integer literals.
Integer literals are always unsigned unless the number is explicitly negative (-3).
Binary (0b11) and hexadecimal (0xff) literals are always unsigned.
Integer literals have the minimal wdith that can contain them: 0xff is uint8, 0x100 is uin16, -128 is int8, -129 is int16.
In the case of a binary arithmetic operation where none of the operands are integer literals, no integer promotion occurs.
In the case of a binary arithmetic operation where one of the operand is an integer literal:
- if the operands have a different sign, no integer promotion occurs and it is a type check error
- if the integer literal has a smaller width that the other operand, it will be widen (zero extended or sign extended) to match the operand. After the promotion both operands will have the same type and the subexpression type checks.
- if the integer literal has a larger width than the other operand, no promotion occurs and it is a type check error.
The rationale for this last rule is that expressions are never integer promoted in any way to avoid surprises, and the integer literal can't be narrowed because otherwise it would be necessarily truncated since the width of an integer literal is already the narrowest it can be.
Integer conversions and castings are considered expressions, so a casted integer literal won't be subject to integer promotion. If you are already explicitly casting an integer literal, you might as well cast it all the way.
The case where both operands are integer literals is a particular case where one operand is an integer literal.
Nobody special · 2025-04-16 02:47
>>41682
>Integer literals are always unsigned unless the number is explicitly negative (-3).
>if the operands have a different sign, no integer promotion occurs and it is a type check error
I assume you mean if the operand types have a different sign.
Does this mean that if I try add or multiply a positive constant with some signed integer variable x, your language will throw a type error?
Why not let the literal take the signedness of the variable's type?
Yamaoto Toshi · 2025-04-16 05:14
>>41684
>I assume you mean if the operand types have a different sign.
Yes.
>Does this mean that if I try add or multiply a positive constant with some signed integer variable x, your language will throw a type error?
Yes, I hadn't thought about that but now that I do this seems fine. The type checker needs to check that there isn't a sign overflow however and if there is one it's a type error.
For example:
int32 x = 3;
x = x + 0x80000000; # type error
# should be:
x = x + (int32)0x80000000; # ok
>Why not let the literal take the signedness of the variable's type?
Well the main idea is to remove (implicit) integer promotions except for the rare cases where it is safe to do so (which is only when literals are involved imo), because C's type system is way too weak in this specific aspect and this leads to a lot of avoidable bugs and to some vulnerabilities. More specifically the issue is about signedness, it's all about signedness.
You don't get logical errors due to zero-extending an unsigned value or to sign-extending a signed value. But casting a signed value to an unsigned value (no matter the width) or casting an unsigned value to a signed value of the same width is a potential bug or vulnerability waiting to happen.
vector · 2025-04-16 15:18
>>41690
>Yes, I hadn't thought about that but now that I do this seems fine.
I'm thinking more
int32 x = -10;
x = x + 3; This would throw an error, and would be annoying because the solution is to explicitly cast the constant, but shouldn't the compiler be able to do this?
>But casting a signed value to an unsigned value (no matter the width) or casting an unsigned value to a signed value of the same width is a potential bug or vulnerability waiting to happen.
It would make sense to error if the user tries to do
int8 x = 0;
x = x + 145; this is a bad operation, because no meaningful conversion can be found. But if it's less than the signed types largest positive number, shouldn't it be fine?
melz · 2025-04-16 15:35
>>38018
>I'm curious about any languages lainons here might be designing.
i really wish i had the requisite knowledge to design my own language, i don't even truly know any languages let alone how to make one
Lady 3Jane.. · 2025-04-16 15:51
>>41717
you can easily make your own interpreter with minimal programming knowledge.
And then making a real time REPL should keep you busy for awhile.
try crafting interpreters, and then github a reference implementation for whatever language you like.
melz · 2025-04-16 15:59
>>41719
i'm going to be completely honest with you, i'm an absolute beginner who is not fluent in any language and i have no clue how to decipher whatever jibber jabber i just read i am very sorry can you dumb it down for me
Terzibashjian · 2025-04-16 16:38
>>41720
>i'm an absolute beginner
Then maybe go through a book or two first.
Interpreters are like a beginner version of compilers though.
At the highest level, you can just parse strings and turn them into commands you execute within the programming language you're already using. Any game that has console commands, has an interpreter and a mini-programming language (More accurately a Domain Specific Language) the player can use.
https://en.wikipedia.org/wiki/Interpreter_(computing)
https://en.wikipedia.org/wiki/Domain-specific_language
When you feel you're ready to try making one yourself, try: https://craftinginterpreters.com/contents.html
It's a very popular book, so lots of people have posted their versions in all kinds of languages on github, so if you get stuck, you can look what other people do.
melz · 2025-04-16 16:46
>>41723
>Then maybe go through a book or two first.
i know you guys probably hate hearing all of this from beginners but do you have anything that can be viewed as like a pdf or something that my broke ass doesn't have to pay for?
Dr. X · 2025-04-16 16:50
>>41724
Any popular book can usually be found by just googling the title + "pdf", or on archive.org
melz · 2025-04-16 17:00
>>41725
>Any popular book can usually be found by just googling the title + "pdf", or on archive.org
thanks, i'll see what i can find
Cat Mother · 2025-04-16 17:25
>This would throw an error, and would be annoying because the solution is to explicitly cast the constant, but shouldn't the compiler be able to do this?
You're right, but this is already what I understood that you meant from your first reply.
>But if it's less than the signed types largest positive number, shouldn't it be fine?
And yes, I also agree.
The rules are a little bit too strict. The typing rules of literals are fine but the transformation that the compiler should be able to do, still only on constant literals, are:
- sign-extend a signed literal into a signed literal of a wider type
- zero-extend an unsigned literal into an unsigned literal of a wider type
- zero-extend an unsigned literal into an unsigned literal of a wider type and then cast that wider unsigned type to to a signed type of the same with (makes your [int32] + 3 example works fine)
- cast an unsigned literal into a signed literal of the same width but only if the literal is not too large that casting it will result in a negative numeber (prevents my example above with [int32] + 0x80000000 and your example with [int8] + 145 to be promoted, causing a type error)
and that's it.
This should be allowed but this rule is rendundant because the typing rules of literals make it impossible to have a positive literal of a signed type, because fuarrrk signed integers that's why.
- cast a positive signed literal into an unsigned literal of the same width and the zero-extend it to a wider unsigned type if need be
This should notably not work without manual casting. It is still a valid thing to want to do however.
uint32 x = 10;
x = x + -1;
Switch · 2025-04-21 09:50
I'm working on ways to signal failure during program execution.
My end-goal with this language is to port the compiler to a PDP-11 emulator, just to see how much modern ergonomics would've been possible even back then.
Thus, option types via discriminated variants et al are out, as they carry too much runtime information.
My idea is a "task", ie. a function that can fail, and return a failure object with equal(or lesser) size and alignment requirements as the return type, thus getting rid
of the additional errno step ubiquitous with C calls.
Failure would be indicated by some bit in the PSW(processor status word, ie the status register) being set, although I'm unsure whether those were automatically reset on the PDP 11.
Do you lot have some ideas?
Y.T · 2025-04-21 17:20
>>41822
I find your stance very odd, how are discriminated unions out. Wasn't there full smalltalk implementations (pre smalltalk 80 I believe) and proper lisp implementions (i believe Mac lisp was for the pdp10). Which means there was likely a whole condition system implemented on a pdp11 which is much more advanced than any exception system and far more advanced than simple discriminated unions.
Remember lisp with its full interactive powers was implemented in the 50s on tape that could save full images to prevent people from putting the entire programs through save jobs via punch cards so you don't have to do it again
queue · 2025-04-21 17:26
>>41822
>>41829
Upon a simple search I come across this mailing list thread
https://groups.google.com/g/pidp-11/c/VPVy9jljRpE
Incognito · 2025-04-22 10:12
I've been messing around with Clojure, and I'm kind of surprised more functional languages don't have something like its (loop) s. Maybe I haven't used the language enough to understand its drawbacks, but just going off of what I've seen of it, It's a really nice shorthand for creating a recursive loop function, a somewhat common pattern in functional languages when you need to deal with algorithms directly.
drop · 2025-04-23 14:24
>>41829
I never want an int returnt from a function to occupy more than one register, as suggested by the most natural size and alignment.
Splicer · 2025-04-23 15:48
I'm working on an assembler! It makes .o files (x86-64 elf object files). Today I added support for external dependencies. It's so comfy to work on. Like adding an instruction here, addressing mode there. Slowly slowly it's becoming something.
JARGON · 2025-04-24 01:17
>>41849
why would you need (loop) when you couldjust call a function from itself? isn't (loop) needed by clojure only because of the limitations of the java virtual machine ?
Nameless · 2025-04-24 14:07
>>41875
I can't think of any (non contrived) answers off the top of my head, but I've written multiple functions that look roughly like
//pseudocode
foo x =
let loop x y =
| x + y / 2 > x * 10 -> x + y
| else -> loop y x + 1
in loop y x + 1 That I feel like having syntactic sugar for it would be nice.
Zero Cool · 2025-04-24 16:19
>>41875
Yes and no. For clojure it exists as a limitation, however the idea isn't without merit. Nock and hoon has tail calls and lets you make recursion points trivally so you can recurse back to it. This works well with what functions really are in nock, which is much neater than any lambda machine.
Further, languages like scheme have this. With scheme this is called named-let which does what the psuedo code of >>41888 states. This is an easy macro to make in CL but nothing hurts just labels and doing it by hand. In languages like haskell local function declaration is small enough that it doesn't matter, to do it with functions would force you to have an iter HOF that you pass your recursive function to yourself, you'd have to think how to make it fully nameless though as doing it naively wouldn't work (namely you need to pass a lambda to the function that acts as a catamoprhism) as you have to name for the lambda you are currently defining.
car · 2025-06-13 12:40
I've been designing a programming language intended for educational purposes, kind of like a hybrid between C, Basic, and Racket. I've been using C itself as a starting point, and it's interesting to see how much individually small changes can dramatically alter the way the language works as a whole.
As an example, I modified the preprocessor to work recursively; rather than going through the code once and handing it off to the compiler, it instead repeatedly expands #defines until there's none left. This allows for tons of crazy metaprogramming stuff, a lot of which I wasn't even sure would be possible with simple textual substitution.
James K · 2025-06-14 01:27
>>41849
It's inspired by Scheme.
(let loop-name ((x 1))
(if (f x) (g x) (loop-name (+ x 1))) The Scheme version is more general because it bind the loop name to a function and calls that function.
藤原純一 · 2025-06-15 06:57
>>41875
>why would you need (loop) when you couldjust call a function from itself?
Because the humble loop costs a mere CMP and JMP or whatever architecture equivalent, less than dumping a few dozen bytes of processor state in a function call.
Aristóteles Amadópulus · 2025-06-19 08:28
Does anyone have any literature about doing extensible programming in statically typed languages? Specifically, languages that are able to define what would traditionally be built-in features like control-flow, object systems, error handling, generics, etc. through programmer-acessable tools?
Pretty much every extensible language I'm aware of is either dynamically typed like Lisp, and Smalltalk, or "untyped" like Tcl and Forth. The only major exception I can think of is Seed7, which doesn't have much written about it and seems to be limited to introducing custom syntax for standard functions. Beyond that, I've seen people do some cool tricks with Haskell, but those don't really count.
I understand that a statically typed language is inherently going to be more limited when it comes to this kind of thing, because you need to figure out a lot more in a single pass, but I still feel like there ought to be more written on the subject than what I've seen.
Turner · 2025-06-20 05:20
>>38018
it would be nice to have something like
a * 10 = 1; // a = 10
// or more generally:
a * 10 = b * 10 = c * 10 = 1; // a = 1000, b = 100, c = 10
// equivalent to:
c = 1 * 10;
b = c * 10;
a = b * 10; sugar for simple computations where you want to store intermediate values
Motoko Kusanagi · 2025-06-20 05:27
Visual programming languages aren't often talked about (for a good reason!) but I would like to explore the concept a bit more. Most people know about Scratch but there are other that are more serious (tm) like ColorForth (historic) or PWCT (open-source & semi-active) and Prograph (historic).
>https://noelrappin.com/blog/2018/11/prograph/
>https://courses.cs.washington.edu/courses/cse583/00wi/lectures/class9/sld038.htm
I also like how DrRacket IDE automatically draws little arrows that point to variables.
Spike Spiegel · 2025-06-20 06:21
>>42397
Have you seen:
https://gtoolkit.com/
Smalltalk makes it easy to make guis for all data, structures. Gt encourages micro tools and hence it would give you a visual programming on big systems you'd desire
John Titor · 2025-06-21 00:51
>>42397
>for a good reason!
The only reason is that so far they are primarily teaching languages, experiments and DSLs, no one has seriously attempted to design a visual language from the ground up which is meant to build off of the strengths and quirks of visual languages.
PureData could be reworked into a Python level language without much effort; dump the scheduler that is highly tuned for realtime audio for something more suited to general purpose use, replace the pd way of doing interfaces with a set of Tk objects and make some system interface objects (many of which can just be reworked from existing pd externals) and you have a fairly useful general purpose interpreted language. I have a fairly hacked up pd which does much of this and have considered putting in the time to polish it up, but I can't justify the time and really I think it should be rewritten in something like C++ which is more suited to OOP which would make writing externals far less of a headache and the performance hit would not be a huge deal here. If I found the right accomplice I would be all for developing such a language but I don't see that happening, I am probably far too opinionated when it comes to language design considering my programming ability, but it allows me to see the potential in graphical languages instead of how they will never be rust or C or what ever.
Spike Spiegel · 2025-06-22 08:45
>>42397
>>42402
In theory, syntax and semantics are largely independent, so you could, for example, easily make a VP editor for C that freely converts between textual and graphical representation.
We don't see many VPLs outside of DSLs mostly for historical reasons. The languages that provide the basis of modern programming, Fortran, Algol, Lisp, etc. were developed at a time when computers dealt primarily in text and/or punch cards, so the people designing them were naturally going to make textual representation their top priority.
Agent Jones · 2025-06-22 12:07
>>42417
Programming languages are more than just syntax and semantics but text languages are mostly terrible at representing anything other than syntax and semantics and are designed around that limitation with the modern IDE and various tools attempting to make up for those limitations. A graphical language needs to be designed around the information it shows well which are things like data/code flow, program logic, etc or you will end up with either a very limited or a very messy language.
The historical reason you speak of is more that text was portable between systems and all displays could show a reasonable amount of text. It has only been ~30 years since reasonable graphics ability/resolution for rendering such languages could be relied on and ~15 years for cross platform graphics.
Project 2501 · 2025-06-22 16:03
>>42419
NTA, but what exactly is a programming language supposed to be besides a notional semantic and an operational semantic?
Jens Hansen · 2025-06-22 20:49
>>42420
I think they're referring to the connection between identifiers and the actual declarations, and the connections between the declarations.
Macro systems that act upon declarations and the program tree itself is a feature lacked in most languages.
And most compilers lack program tree exploration capabilities, which an external program could interface with to generate diagrams and display search results.
Dujour · 2025-06-26 08:59
Does anyone have any papers on metaprogramming? Specifically for statically typed languages?
I might have already mentioned it ITT, but I'm designing a statically typed extensible language, and I want to go as far as I can with metaprogramming in a statically typed language while still having something reasonably usable.
All of the ideas I've had thus-far have either been too weak to justify the language's existence (Rust-like AST-tree macros), or so powerful that they make it basically impossible to work with any code that was written by anyone who isn't you (treating the program as a string that can be arbitrarily read and written to at compile time). So if anyone knows any resources for metaprogramming techniques, please share them.
JARGON · 2025-06-26 12:44
>>38018
Almost all of the type definitions here can be done with inductive types in Intuitionistic type theory. The functions here probably are probability ill defined in type theory since you have to prove they terminate by only using the inductive function's for inductive types to "pattern match" each type definition.
Chapter 1 of the HoTT book (https://hott.github.io/book/hott-online-15-ge428abf.pdf) pretty much explains how to do everything here. It's about 30 pages.
FILLER · 2025-06-26 16:41
>>42459
There's jai's #run directive.
Splicer · 2025-06-29 15:45
>>42461
I'm actually already aware of Jai and its approach to metaprogramming, but thank you for the example. It's cool, but it doesn't go far enough for my needs.
To elaborate a bit, my goal with all this is to have a language to go along with an online course, inspired partially by handmade hero and partially by SICP, where programmers start with a simple set of low-level components and then learn how higher level features work by implementing them themselves.
I've looked at existing languages, but none of the ones I've seen do everything I want in one place. Forth comes kind of close, but its lack of a proper type system makes it a no-go, and it's a bit too unconventional anyway. What I need is a statically typed language with about as many features as C that is also capable of seamlessly implementing about 70% of C++'s with metaprogramming.
I think the biggest thing tripping me up thus-far is reflection. Having it as a first-class feature would make the language significantly more expressive, but it would also make it significantly more complex, possibly compromising its value as a teaching tool. I want something like it (or that is capable of implementing something like it) without the added complexity that typically comes with it, and I'm not entirely sure such a thing is possible.
Ivan Petrov · 2025-07-01 04:24
>>42469
You can just add types to a language. Factor shows you how to do it syntactically. If you want the static semantics it's not too difficult, you know the words so define your own : and lookup the words and check their type.
In general adding types to an existing language isn't too hard you'd make it gradual but you can enforce it statically in a reflective language if you try
Random · 2025-07-01 05:57
>>42469
The problem is creating C++ features with metaprogramming and actually implementing features for a C++ compiler is basically the same thing.
The big features in C++ are metaprogramming, its just restricted and obscured that that's exactly what it is.
C++ templates are type safe substitution into a parsed program tree.
This is why "Substitution failure is not an error." is a thing, C++ doesn't actually check templates for validity until they have instantiations, because templates are just copy paste substitutions.
Virtual structs can be implemented as autogenerated tagged unions, which is just copy pasting the virtual members into the union & creating a unique enum ID for the tagged union.
Virtual functions can be implemented as copy pasting all the virtual functions into a switch statement on the tagged union enum ID.
Even using (or jai's `use`) is just replacing the accessor identifier with the full identifier path, substitution again.
It's all macro expansion & substitution, but in a more restricted form attempting to make it more legible.
(Unfortunately C++ does not expose its unrestricted form for the cases where it is desired, and unfortunately the syntax & restrictions C++ places also make it all more difficult and limited.)
You could have functions that return a type to implement templates for example.
MyDarray :: (T : #type) -> struct
{
return struct { #code { data : *`T; size : u32; } };
}
a : MyDarray(int);
Frank Garza · 2025-07-01 12:13
>>42506
Would that mean that, in theory, a language that allowed macros to have custom syntax could basically implement all of C++?
Sipho Nkosi · 2025-07-01 23:18
>>42507
At that point it's a language that can write compiler extensions to itself within itself.
Sure you can do that, but at that point why not put the features in the base language?
Putting things in the standard library instead of the core compiler doesn't make things easier to implement.
Though I suppose this is based on my own personal compiler experience and skills, maybe it would be easier for another person to organize things like this.
>>42469
Can't reflection just be implemented as compile time #run code which utilizes built in functions which access, create, modify program tree elements?
And/or extra keywords like sizeof alignof offsetof which extract information about the program tree element?
The issue is to implement #run the typer phase needs to have a "jobs system" which completes tasks that have no remaining dependencies until there are no tasks left or it cannot complete tasks due to circular dependencies.
This requires some organization & effort to achieve.
It would also be useful to have a system to @tag extra data onto declarations, which can be accessed by reflection.
To specify details that effect the result.
Or reflection code can iterate over the whole program tree looking for a particular tag, to perform operations on nodes that request them via the @tag.
For example virtual structs could be specified like this, with reflection code later turning `Base` into a tagged union.
struct Base @virtual_container
{
u32 type; @virtual_type
u32 x, y;
};
struct Crate @virtual(Base)
{
u32 wood_quality;
};
struct Barrel @virtual(Base)
{
u32 metal_quality;
u32 fluid_quality;
};
Dujour · 2025-07-13 14:24
Why hasn't there been more done with zero overhead automatic memory management?
And before people start bugging me about definitions, I mean "zero overhead" in the sense that the only runtime overhead you incur from memory management is that required to allocate and deallocate the memory itself. malloc() and free() are zero overhead but not automatic, garbage collection is automatic but not zero overhead. Got it?
Anyway, the only mainstream language I can think of that has full-on zero overhead automatic memory management as the standard is Rust with its borrow checker, and it has a significant impact on the way programs are structured. Why is that? Is it just a hard problem to solve? Is the technology that allows us to do these things just too new for it to see wide adoption? Because I'd love to see it in higher-level languages. Eliminating the performance penalties of runtime memory management and reporting memory-related errors at compile time would make userland software, the kind of stuff traditionally written in languages like Java, not only much faster but also much safer.
Yet Another Lainon · 2025-07-13 15:19
>>42507
And now I started thinking whenever one could implement C++ in Racket. I wish to cast at you a malediction for that.
Tank · 2025-07-15 07:25
>>42591
It's because it needs to be built into the programming language itself.
It takes so much time and effort to build a decent comprehensive programming language that the effort stops before extra features like this are added in.
And most programming languages are made by groups of people rather than individuals, so its hard to get everyone on board with extra features, and it's difficult to work out the logistics to actually get it implemented.
Memory management is how the program organizes its memory, so I don't know if zero overhead memory organization is feasible.
Even malloc and free aren't really zero overhead, custom memory allocators can have substantial performance gains compared to general purpose memory allocators.
However with proper programming language integration, compile time optimization for tracing garbage collection or reference-counting is possible.
Techniques like that could eliminate the majority of work involved with automatic memory management.
Another reason why all this isn't done is that automatic memory management is usually associated with interpreters and JITs rather than static compilation.
Those programming languages usually tend to avoid anything like static compilation and ahead of time optimization.
Frank Garza · 2025-07-18 11:43
>>42598
I guess the reason I'm confused is that we already have fairly straightforward compile-time automatc memory management with region inference, and unlike the borrow checker, it does not significantly influence how a program is written.
Sure, a region-based approach has its flaws; overly small regions that are allocated and dealloated to often can hurt performance, and, on the other end, you can run into a similar problem as reference counting, where large amounts of dead memory take too long to be released. But I don't see why we aren't putting more effort into mitigating these flaws. I could easily imagine a region-inferred system that is competitive with if not faster than manual memory management.
Jens Hansen · 2025-07-18 12:28
>>42629
Progress is horrendously slow in established programming languages.
For example C++ only sort of got tagged unions in C++17, and C++ is only sort of getting limited modules now.
It doesn't help that time itself is going faster these days.
Ivan Petrov · 2025-07-22 09:36
>>42630
C++ is a particularly strange case because it has a lot of "kinda sorta" features. Unique pointers, for example, are kinda sorta uniqueness types, but not really. And sometimes you'll have the kinda sorta feature and the real feature it's trying to emulate next to eachother at the same time, like namespaces and modules.
Wintermute · 2025-08-02 14:41
I've been experimenting with a GC design that fuarrrks up the program's memory layout as much as possible without destroying your computer. It runs at random intervals, only frees a random subset of dead objects, allocates dummy objects, and shuffles around memory like crazy when compacting, leaving gaps on purpose.
I don't really have a practical use for such a thing, and, at least in my prototype implementations, it impacts performance pretty negatively, but it's been a fun project nonetheless.
Wirth · 2025-09-05 17:17
>>42459
this one?
Yannis Lilis, Anthony Savidis. 2019. A Survey of Metaprogramming Languages.
https://dl.acm.org/doi/abs/10.1145/3354584
https://www.oilshell.org/archive/lilis2019.pdf
Motoko Kusanagi · 2025-09-25 09:18
Is there a way to implement monadic error handling that doesn't eat up too much space? It's something that makes way more sense to me than traditional exceptions, but I don't like the idea of using up a bunch of extra space for conditions that should generally not occur.
Joey · 2025-09-25 12:05
>>43200
The way it's done in Rust (and C++) is super lightweight because Result and Option objects are guaranteed to be unboxed and live on stack instead of heap (unless you don't explicitly wrap things in Box). So, the way to make it lightweight is to make a language where you can ask for structure data to be unboxed and for structure allocation to be always on stack, or a language where such behaviour is by default (like Rust of C++). I mention C++, it has this kinda stuff somewhere in boost, but it sure has less syntactic sugar and combinator functions for this than Rust.
Fred Nurk · 2025-10-08 12:03
Some moons ago I got pointed into the direction of Forth and concatenative languages in general.
I dabbled a lot with combinator calculus first before taking a look at Forth and variants. In an effort to grok it I made two very barebones interpreters based on Forth.
A few things I noticed:
- You can interpret the input (code) stream however you like. It is even more "powerful" than LISP macros in this regard.
- It is trivial to emit machine code instructions directly. This may not necessarily be the most efficient, but I found this is even easier than writing your own bytecode interpreter, as I can use the "interpreter built in the hardware" instead.
During these attempts I focused a lot on keeping the interpreter absolutely minimal. Only much later did I understand that this is misguided: to use Forth-likes effectively you should focus specifically on the problem at hand and write a tailored, sensible solution.
As an experiment I'm writing the interpreter for a CAD program in a made-up Forth dialect starting from just GNU assembler. Some excerpts:
: _op_ri32 ( op | reg imm reg -- ) op3_prep_move32 #swap printsp REG32 print_, num_to_string println ;
: and_ri32 ( reg imm reg -- )
IF #dup2 0 =
THEN "xor" printsp REG32 @dup print_, #drop #drop println
ELSE "and" _op_ri32
END ;
: _sXX_ri32 ( reg imm reg | op -- )
IF #dup2 0 =
THEN @drop #drop2 move32
ELSE _op_ri32
END ;
: sll_ri32 ( reg imm reg -- ) "shl" _sXX_ri32 ;
: srl_ri32 ( reg imm reg -- ) "shr" _sXX_ri32 ; Concatenative languages make it very easy to take out duplicate code and put in a separate definition. I use it here to shere common parts between and_ri32, sll_ri32 and srl_ri32.
Note that I use separate stacks for integers and strings. The main idea here is to separate things that may eventually require a GC. Having a second stack happens to simplify code too, in some instances.
: code_BEGIN ( reg reg -- ) #dup "code.instr.head" loadu64_l >code_reg ;
: code_END ( -- ) <code_reg code_SET ;
: code_ADVANCE ( imm -- ) <code_reg #swap <code_reg add_ri64 ;
: code_PUSH8_i ( imm -- ) <code_reg storeu8_i 1 code_ADVANCE ;
: code_PUSH16_i ( imm -- ) <code_reg storeu16_i 2 code_ADVANCE ;
: code_PUSH24_i ( imm -- ) <code_reg storeu32_i 3 code_ADVANCE ;
: code_PUSH32_i ( imm -- ) <code_reg storeu32_i 4 code_ADVANCE ;
: code_PUSH64_r ( reg -- ) <code_reg storeu64_r 8 code_ADVANCE ;
: code_PUSH64_xmm0 ( -- ) <code_reg "movsd [" print REG64 print "], xmm0" println 8 code_ADVANCE ;
: code_return ( -- ) 0xc3 code_PUSH8_i ;
: code_sub_r15,8 ( -- ) 0x08ef8349 code_PUSH32_i ;
: code_add_r15,8 ( -- ) 0x08c78349 code_PUSH32_i ;
: code_movabs_rax ( reg -- ) 0xb848 code_PUSH16_i code_PUSH64_r ;
: code_movabs_rax<-xmm0 ( -- ) 0xb848 code_PUSH16_i code_PUSH64_xmm0 ;
: code_mov_[r15],rax ( -- ) 0x078949 code_PUSH24_i ;
: code_mov_rax,[rax] ( -- ) 0x008b48 code_PUSH24_i ;
: code_mov_rax,[r15] ( -- ) 0x078b49 code_PUSH24_i ; Lots of small, compact definitions.
: routine_as_nodef ( name -- label )
"routine." @swap strcat
".pushsection .text." print @dup println
@dup putlabel ;
: routine_as ( name -- )
@dup routine_as_nodef
@swap CODE_defconst ;
: routine ( -- label ) readword routine_as ;
: endroutine ( -- ) return ".popsection" println ; Again, many small definitions, but the interesting part here is readword: all it does is take a single word from the input stream and put it in a string. This allows defining routines like so:
routine nop endroutine
routine retnull ( -- a:null ) 0 A set_imm endroutine
routine parse_number
( si:base c:len -- xmm0:number )
"xorpd xmm0, xmm0" println
fp64.1 "movsd xmm2, [rip+" printsp print "]" println
fp64.10 "movsd xmm3, [rip+" printsp print "]" println
SI C BP add_rr64
: f ( ifsep -- )
SI A loadu8_r
A '.' asm_ifeq_ri32
A '0' A sub_ri64
"cvtsi2sd xmm1, eax" println
"mulsd xmm0, xmm3" println
"addsd xmm0, xmm1" println
SI 1 SI add_ri64 ;
: fl SI BP asm_ifne_rr64 ;
asm_gendynlabel
asm_gendynlabel
asm_putdynlabel
@dup2 f fl
@dup2 jump
putlabel
SI 1 SI add_ri64
asm_putdynlabel
"multiple '.' characters in number" panic f
"mulsd xmm2, xmm3" println
fl
"divsd xmm0, xmm2" println
putlabel
endroutine (number rounding isn't done correctly yadayada not a priority right now)
Also, "global" variables are quite useful:
: #drop >__0 ;
: #2drop >__0 >__0 ;
: #dup >__0 <__0 <__0 ;
: #dup2 >__0 >__1 <__1 <__0 <__1 ;
: #2dup >__0 >__1 <__1 <__0 <__1 <__0 ;
: #swap >__0 >__1 <__0 <__1 ;
: #drop2 #swap #drop ;
: @drop >@__0 ;
: @dup >@__0 <@__0 <@__0 ;
: @dup2 >@__0 >@__1 <@__1 <@__0 <@__1 ;
: @dup3 >@__0 >@__1 >@__2 <@__2 <@__1 <@__0 <@__2 ;
: @swap >@__0 >@__1 <@__0 <@__1 ;
I emphasize again that the syntax is entirely made up on the spot. It's part of the experiment.
(The experiment also involves extensive use of message passing. The viewer is written in a small Rust wrapping wgpu, and the CSG operations in C++ using manifold).
Aristóteles Amadópulus · 2025-10-09 18:55
>>42709
I figure the guys working on javascript frameworks might be itnerested.
Drifter · 2025-10-10 00:33
>>43290
A slightly different perspective: What I like about concatenative languages is that they are the natural fit for scripting in environments when you want oneliners. For example, if you want to add actual in-game scripting where you just go type series of oneliners into chat, there's no better choice. All the usual picks like Python/JavaScript/Lua will require special multiline UI for code and it totally breaks the interleaved rapid gaming/scripting loop.
St. IGNUcius · 2025-10-25 13:03
>>43293
I think APL-like languages may also fit quite well here. In general, to have nice one-liners you need a good way to pass parameters implicitly. Something like stack in concatenative languages or trains in APL or, to some extent, $_ in perl.
Mentat · 2025-11-02 14:31
I'm designing a language with arbitrary-width integer types, and I'm trying to figure out how to handle integer literals.
As far as I'm aware, there really isn't much literature on the subject. Existing languages assume that there's some kind of "default" type or types that integers literals can be safely mapped to, something that my language doesn't provide.
Knuth · 2025-11-03 09:56
>>43442
You might want to look how Haskell did it. All integer literals in code are parsed as Integer (arbitrary width integers) and then replaced with "fromInteger value" calls, where fromInteger has type "Num a => Integer -> a". Which means it will work for any types that implement Num typeclass (even used defined types). And by the power of inlining / constant folding, most of those will normally be computed at compile time.
Roy Batty · 2025-11-03 10:50
>>43442
I guess look at how SBCL implements integers (https://www.lispworks.com/documentation/HyperSpec/Body/t_intege.htm) or its [un]signed-byte subtypes (https://www.lispworks.com/documentation/HyperSpec/Body/t_unsgn_.htm).
Liet Kynes · 2025-11-03 11:12
>>43442
idris2 use gmp underhood and what this anon say from haskell >>43442
Otto Normalverbraucher · 2025-11-03 15:27
>>43449
>>43450
>>43451
I should specify that, like Ada, bindings have fixed ranges. Range doesn't need to correspond to any specific width, and actual operations using these bindings can produce numbers larger than they're allowed to store, but assignment specifically is limited.
I could very-well use traditional bignums, but I feel like there's gotta be a way to do this that is not only faster but also lets me do stronger range checks at compile time.
Frank Garza · 2025-11-11 10:51
Does anyone have any introductory texts on type-inference algorithms? There's a lot of literature on the subject, but everything I've been able to find has been very academic and technical.
Void · 2025-11-11 11:44
>>43509
Check out Hindley-Milner Algorithm W, it's very simple unification-based introductory type inference algo, and here's your best simple tutorial on it in existence. You just need some basic familiarity with Haskell and lambda calculus in general:
https://github.com/wh5a/Algorithm-W-Step-By-Step/blob/master/AlgorithmW.pdf (you can click "load more pages" after you scroll to the bottom)
https://en.wikipedia.org/wiki/Hindley%E2%80%93Milner_type_system#Algorithm_W
Boris Grishenko · 2025-11-12 14:54
>>43511
Would you say that this is the simplest general-purpose type-inference algorithm you know, or is there anything simpler? I'm trying to understand the very basics of type-inference from an implementation perspective, so I want to go as simple as I can while still having something usable.
Unknown · 2025-11-21 13:36
I find it odd that indentation-based syntax isn't more popular in procedural languages. In practice, the vast majority of real-world code bases use indentation and line breaks to communicate the structure of code, even if the language requires curly braces and semicolons, making the latter redundant.
My only guess as to why not is that the popular procedural languages that do use it, the likes of Python and CoffeeScript, don't implement the concept particularly well, souring people to the idea. And that stinks, because functional languages like Haskell and F# execute the idea perfectly fine. In fact, in Haskell, it's actually syntactic sugar for a more C-like curly-brace syntax, so you can still use that if you prefer, but the vast majority of Haskell programmers almost never do, because they prefer whitespace.
Nobody important · 2025-11-23 07:10
>>43556
Two words: merge mistakes. The real answer to the whole problem is a Lisp-like syntax where you can easily tune out the parentheses meant for the computer thus read it through indentation.
We'd only need to "fix" LET to follow SETF (i.e. "(LET (a 1 b 2) ...)" instead of "(LET ((a 1) (b 2)) ...)" by disallowing implicit NIL init) to really improve readability; and standardize something like the hundred small macros unifying LET/DESTRUCTURING-BIND to avoid ugly staircases.
Maybe apply the same "parse as plist instead of list of clauses" to some other forms like COND or CASE.
SHODAN · 2025-11-25 08:44
>>43556
People really really suck at writing parsers.
That's basically it.
It's easily possible to have a configurable syntax or different modes for a programming language,
One with whitespace indentation for scopes that ends statements on \n newlines
One with {} for scopes that ends statements on ; semicolons.
That would help greatly with providing a casual version and pro version.
People are also largely unable to conceive of software having beginner/advanced modes with different interfaces.
Instead there's an obnoxious focus on making things easier for beginners and casuals stripping away functionality and usability, or refusing to refine the interface claiming it needs to be jank because its for pros.
Also not having user configurable interfaces in general.
Backus · 2025-11-30 21:44
I became genuinely discouraged with programming after trying my own language. Basically, it was Lisp in a Rust VM. The performance was awful. I thought I could leverage Rust to at least get close to the performance of native Rust. The result is that in the end, I was performing worse than Python in all the benchmarks I tried.
After that, I still tried to do another project in the networking domain, but I became completely discouraged by my own stupidity. I think I've hit a ceiling and I can't go any further. Apparently, I wasn't born to do anything relevant. Maybe my future will be creating endpoints in Spring/Dotnet and some screens with React until I'm replaced by some AI and condemned to have some job I don't like.
Well, at least making my own language gave me a greater understanding of how certain things work (errors, memory management, recursion, etc.). It was good while it lasted, but I think now I no longer have the energy to do any more toy projects.
I read about the Lua specification (register-based instead of stack-based) and got very excited, but when I tried to implement a simple Fibonacci sequence in a VM I created, I completely gave up.
How do you deal with frustration? I can't even use it as motivation anymore, since apparently my IQ doesn't allow me to go any further.
DEBUG-CONTENTS · 2025-12-01 03:37
>>43566
> How do you deal with frustration?
Get a lot of it and desensitize lol. Overall, your post is a rather comic rant. You really shouldn't expect to compete in performance with runtimes that have 100000x more hours of expert work sinked in like .NET, JVM, V8, SBCL, etc. If you still want to get better, at least look at how they do things on actual implementation level, not just spec.
Guthrie · 2025-12-01 16:58
>>43592
My main problem was perhaps overcomplicating things that should be simple. In my mind, if I simply translated certain commands from my language directly to Rust, it could reduce performance issues, but in the end, things aren't as simple as they seem.
I had already implemented constant folding, it worked well, but the problem is that something like that is rarely really enough when you have a slightly more complex codebase.
Another exercise I tried was reading and writing. I ended up being more than 2x slower than Python to write 100MB of text.
But one thing I found very interesting was that I had practically no runtime bloat, which I think was interesting to see. Perhaps the only correct decision I made was implementing reference counting instead of a traditional mark-sweek garbage collector.
In any case, this experience was good for better understanding memory management. Not to mention that it made me appreciate more the work of people on projects like GraalVM, JVM, V8, etc.
Maybe my next step is to try to do something performant, but I would probably have to start from scratch. Perhaps something with LLVM, but I don't know if I have enough IQ to go down that road. Performance and optimization seem like a gigantic rabbit hole to me. Still, thank you for the reality check, I really needed it.
Shogun · 2025-12-01 17:50
>>43603
Doesn't rust have known issues with i/o slowness in its standard library?
Hououin Kyouma · 2025-12-01 17:57
>>43604
Maybe? But the real problem is my architecture. I did the same test in pure Rust, basically "translated" from my language to Rust, and the performance was basically the same as the same program in C (gcc -O3). Because I was using Rc (my first experience with it), I ended up implementing many clones and copies, especially when I needed to deal with strings, something that didn't happen in computing, since I took advantage of constant folding.
Anyway, this served as a learning experience in terms of programming languages. Despite having almost two decades of experience in the programming field, this was the first time I ventured into something more complex. I hope not to make the same mistakes in the future. As >>43592 said, I need to pay more attention to how the Lua team implemented it, not just read the pure specification. This might help me, or at least show me new paths to follow.
Torvalds · 2025-12-01 23:26
>>43605
My understanding is that the lua VM bytecode directly operates on the CPU registers.
Some are unavailable because the VM itself needs those registers to operate, but most of the CPU registers are available.
Python is slow because of what it is, its design not its implementation. (Unless if there's poor memory organization, like bad cache locality with allocations or excessive memory rearrangement - moving memory to different addresses.)
It's a dynamically typed language without a JIT without a register VM.
Java is faster because its statically typed, types are known at compile time and can be treated as discrete byte quantities.
JS is faster because it has a JIT and insane man hours spent on it.
If your benchmark is writing 100MB of text, how is that array allocated and reallocated in your VM?
Memory may not be being reserved like it is in python, it may be doing many reallocations instead of reserving extra space to avoid reallocating, it may also be memcpy'ing the memory around to different addresses as it runs out of space instead of extending the allocation into unmapped adjacent memory.
Dox · 2025-12-02 14:32
>>43609
>My understanding is that the lua VM bytecode directly operates on the CPU registers.
Unless there's a misunderstanding, you are mistaken. PUC Lua using a register instead of stack based VM has nothing to do with CPU registers.
>Python is slow because of what it is, its design not its implementation; it's a dynamically typed language without a JIT without a register VM.
Being a JIT of bytecode compiler + VM is an implementation matter, though. The other real performance problems of Python are that it's extremely dynamic but was never designed to provide enough optimization potential where it matters (e.g. inlining) from the start, unlike ANSI CL, and that it's using refcounting instead of a proper GC.
>Java is faster because its statically typed, types are known at compile time and can be treated as discrete byte quantities.
While that's a major reason, there's still a lot of boxing in Java and the wonderful (now generational) ZGC is a big reason Java flattens Python.
PS: don't forget https://pypy.org !
Dujour · 2025-12-14 17:20
>>38306
>variables basically acting as aliases to memory addresses. There'd be some sort of way assign multiple variables to the same memory
I have thought about it and after few days I realized I probably shouldn't do it.
Having 2 variables that affect the same memory is useful for in-place changes with functions but having adam_cup and alice_cup manipulating the same memory address (it's the same cup but depending on who is using it their maximum may change because Alice pours to the edge and Adam always leaves some) was outclassed imo by retroactive setter and getter:
var cup
#default set is: old = new
#default get is: return self
var adam_cup
set
cup = max(min(new, 200), 50)
get
return cup
var alice_cup
set
cup = max(min(new, 250), 0)
get
return cup VS:
var cup
var adam_cup & cup
set
old = max(min(new, 200), 50)
#I intended to use & for aliases since they are like reference and use 'and' for logic
var alice_cup & cup
set
old = max(min(new, 250), 0)
I also considered aliases for functions (different names and default values, maybe some hardset values, same logic inside) and also for classes (Car and Automobile can be used interchangeably because Automobile is an alias for Car but they can contain different Constructor and Destructor)
Ultimately it's just extra confusion with extra classes (and it's hard to actually find use)
Functions are a bit better but it already could be done with function calling the original.
func spawn_unit(hp=50, atk, type)
#logic
func spawn_general(hp, atk=20)
spawn_unit(hp, atk, "General") VS:
func spawn_unit(hp=50, atk, type)
#logic
func spawn_general(hp, atk=20, "General") & spawn_unit()
I will just stick to the reference to pass it to function. It makes me a bit sad because at first it seemed like a great idea but the more I thought about it the less it made sense to use aliases.
I also considered being able to decide size of a variable:
var int x
set
old = max(min(new, 1), -1) VS:
var int 2b x
# 2b takes 2 bits and is: -1, -0, 0, 1
var int 6B y
var bool 1kiB z
Retroactive setter (and dynamic variable sizing) seem to win once again since it offers more flexibility.
Generally retroactive setter and getter are things that tidy up the language quite a bit:
No need for private/public (can still add private to make temporary state not accessible outside)
No need to do getter and setter ahead of time, if suddenly you need one you can just add it later and it will activate automatically
No need for variable sizes when set is coupled with dynamic sizing
Does reference/aliases
BloodCat · 2025-12-16 22:27
i've been working on designing a compiled language that takes inspiration from B and BASIC.
the hello world would look something like:
@inc "stdio"
defunc main(argc, argv)
puts("hello, world!")
end
list · 2025-12-17 15:14
>>43708
Well, since you posted here, I imagine you're looking for opinions, so I'll give mine:
Honestly, I don't like code without limiting characters. The absence of curly braces, semicolons, etc., gives a feeling of incompleteness.
Anyway, this is something completely relative and there's no real consensus.
Something that is consensus, on the other hand, is features. Are you working on a specific/experimental feature that you'd like to see in other languages, or are you doing it just for fun/curiosity? I also started trying to develop a language recently, but I confess I didn't have many ideas about features, and I ended up abandoning the project temporarily. Other features I wanted to implement (e.g., RC instead of mark-sweep) proved to be much more difficult than I initially imagined.
Neo · 2025-12-17 16:03
>>43709
i was thinking about using a more expanded character set (e.g: lambda, alpha, omega, independent division and multiplication signs).
also, when it comes to features, i'm mostly just working on this for fun. i'm not a full-time software engineer, nor do i plan to become one due to it not being a very stable career field.
Apoc · 2026-05-21 19:21
I made an esolang for the rubiks cube. Just simple functional semantics given to words in the rubiks cube group; that is to say sequences of half move turns that finish in exactly the position they started in. For example, `RUU'R'` is a move sequence with the property that it finishes in whatever state the cube was in when it started, and insofar as sequences of cube moves are members of the rubiks cube group sequences with this property are known as words; if i read wikipedia right anyway. That word `RUU'R'` is the word for division in my language, and `RU UU' UU' U'R'` would be a properly formed expression, dividing `UU'` by itself. `UU'` being the primitive word for 1/12. A cube has six faces and you can turn a face two ways. with twelve basic moves, i figured a base twelve number system was in order. Primitives 0/12 through 11/12 exist. Typical arithmetic operators, along with binding and scoping.
```F B LL' RURUUU'UU'U'R'UFF'U'U'R' B' RU' LL' LL' UR' F'```
is
```
{
bind B := ((1/12) / (1/12)) / (4/12);
B * B
}
```
scopes are sequences of expressions, evaluate to the value of the last expression etc etc. so the above program should evaluate to 9.
FILLER · 2026-05-27 13:49
https://sundee.neocities.org/programminglanguage/
>TFD is my vision for a programming language. It is basically an overhauled version of C, meant to be cleaner and more comfortable to use, mostly by being less pedantic and allowing you to do the same kind of things with less effort, without adding complex features or things that aren't "C-like".