I DID IT! After a mad, possessed, sleepless 30 hour bender I finally solved the #programming language design problem I've been wrestling with for a decade! How to make a concatenative language both type-safe and infix/postifx (not RPN). I think this is the most elegant language I've ever designed: https://gist.github.com/Kroc/62fd60dda68f667e0e4d94c9e08bf2af #pling #ezOS
@kroc That's an interesting language.
With regards to the data stack, I'm concerned from a debugging perspective. I feel like data could make its way into the data stack in many different ways, leading to many confusing/hard-to-debug results. Having a global data stack instead of just pushing a function's return value onto a variable stack seems like it could get pretty messy. Does each scope have it's own data stack? How would unit testing work?
I like that you can iterate over collections easily using "with".
The "var" keyword seems unnecessary given that you could instead allow "get" arguments to behave as variables. What does "var" do that "get" couldn't just do?
What would a chain of else-if statements look like?
Any thoughts on allowing for specifying if a variable is mutable or immutable?
Can we easily map all of the values in the data stack after iterating over all of them, having them back in the data stack in the same order they were in prior to the mapping?
@livingcoder Lots to unpack so I’ll provide a separate toot for each question;
A chain of if-else statements would essentially be a `switch` block and since a function can programmatically read parameters I’m thinking that a `switch` function would keep reading expression + lambda pairs until one passes. An expression that always passes could be the default switch case on the end, I.e. `true :;`
@livingcoder What’s cool is that `switch` is something that can be implemented using existing functionality, it doesn’t have to be a compiled-in statement. I wanted as few ‘statements’ as possible in the language so as to avoid over-specifying the language!
@kroc The fact that a switch comes free with the else-if grammar is something that I like about the language.
@livingcoder As for data stack shenanigans I will have to write up something more concrete regards implementation. The data stack will be separate from things like the function-call stack and other allocations like lists and variables. I don’t think it will be a problem but I will work through the theory closer as a matter of course.
You can read my reasoning for designing this language here: https://mstdn.social/@kroc/110670413094213147
@kroc Thanks for linking to your reasoning. I can definitely appreciate that goal.
I like the idea of having a data stack, but I'm curious about it's scope. Does it always push data to the functions "return"? For example, does the code below output "1", "2", "3" or nothing (because the one_two and three functions have a local data stack)?
```
fn wrapper :
fn one_two :
1
2
;
fn three :
3
;
;
with wrapper :
echo !
```
@livingcoder At least for now the stack will be unbalanced, but unlike Forth, the machine won’t crash! You also have to consider that you may want such functionality, leaving something deep down on the stack to be intentionally caught further up! I’ve added `.` to drop a stack item to the doc to manually correct stack balance but with `get` being present this isn’t as critical as with Forth
@livingcoder It should be possible to keep things balanced just using parameters but the option to do trickier things is there and the language is more forgiving of it, at least in my head :P
@livingcoder Ah I also forgot the type safety! In (classic) Forth you’re just pushing and popping pointers but in Pling if you push one type of Struct and pop something else later due to stack imbalance you won’t have corrupted memory, just a different kind of object in your hand.
@kroc When you permit a system to push to global, you end up with untestable systems that become a nightmare to extend. It also makes it much harder to inject reusable code because "reusable code A" pushes to global and "reusable code B" pops from global (working together across distant parts of the system), but the existing system already manipulates the global stack and so now you have stuff like this in the stack (next post).
>before, pushing "command" struct to be executed by handlers later as they are parsed from a message queue or commandline arguments
```
push structs C
expect structs C
```
>after, pushed "command" structs and "termination notification" structs (based on data in the "command structs") along with many feature updates that do different things, polluting the global stack
```
push structs C
// pollution gap pushed to stack
// some of the pollution popped
// ! remaining pollution gap extracted
expect structs C (to create structs N)
push structs N
push structs C
// ! remaining pollution gap restored
// remaining pollution gap popped
expect structs C
expect structs N
```
As future enhancements occur and structs begin to pollute the global stack between C and N, the extraction of Cs to create Ns is going to need to be updated.
@livingcoder The stack is not the only data passing mechanism, as lists and other data types can be allocated outside of the stack.
I could remove the stack and the language would still work with a regular function flow but I feel that might be too restrictive for a language that needs to have the flexibility of ASM. I'm going to have a stab at making an implementation and see as I go.
@kroc That makes sense, that the language could still function without it. I've found that when a language makes bad design harder, more good code gets written by default. If there was a way to pass structs to a global "typed" stack, that would make it far more useful, imo. This would solve a few issues at the same time:
1. Popping from the typed stack guarantees the correct/expected type is returned
2. Functions who's purpose is to push specific data to the stack no longer have to worry about new code polluting downstream popping/usage
3. Wanting to map data from one stack to another becomes obvious/straightforward
4. Any new function that wants their data included in an existing downstream process no longer needs to pop existing structs, push their new data, and restore the existing structs in reverse order
@kroc Processes that push/pull from the stack would inevitably have their own types created to isolate their processes. This would ultimately lead to programs utilizing the Command pattern quite often (which is fine, imo, since it is easily testable).
Mapping functions (from one stack to another stack) may still want to push the popped structs back into the stack. Maybe something like your "?" idea could be used to traverse the stack without popping elements.
@livingcoder The `var` keyword, when assembling, creates a function that when called creates a new data value on the heap for the variable, which is destroyed when the variable goes out of scope. The default value comes from the second parameter.
`get` is getting the default value from the caller! Every time a function is called the current instruction pointer is pushed to a private stack. `get` uses not the current instruction pointer, but the parent’s instruction pointer!
@livingcoder The stack could be thought of as a list so I imagine that using normal list iteration on it would work. I’ll also be adding a `?` function that peeks the top of stack rather than popping it like `!` does.
@livingcoder A constant is immutable, a variable is mutable. In terms of implementation a constant is a function that pushes a value to the stack created at assemble time. A variable is trickier, it’s data has to be on the heap because a recursive function call must create a new instance of the variable.
@kroc Ooh this is an interesting design space. Reminds me somewhat of REBOL http://www.rebol.com/docs/quick-start.html