I took that, and built a simple forth-like system, in golang following the original recipe and breaking it down into simple steps for learning-purposes:
It was a lot of fun for me to reimplement this in MIPS assembler on CI20 [0]
JonesForth could be more clear about the high-level logic of its interpreter part. I tried to make this part as clean as possible, hopefully did not miss anything.
Maybe I will make a RISCV version in my copious free time in the future.
I want to write one for bare metal (non-Linux) raspberry pi (ARMv6 32 bit on Raspberry Pi 1 and Zero; ARMv7 and ARMv8 on higher models and also supports 64 bit). I want to have no dependencies required though so was thinking of bootstrapping it with nothing but machine code (determined initially with the help of an assembler and documentation of course). Someone has already ported jonesforth the Raspberry Pi[1] but using serial i/o as the user interface and it has dependecies to build it, but I should be able to get ideas from how they coded their assembly parts compared to the original jonesforth. I want to be able to use HDMI for the screen (already tried it out with some bare metal tutorials in assembly so that's do-able) and again, with no dependencies. And I want to show people how to do it themselves, not just have it be something to run that they don't understand fully. It should also be possible to have the forth kernel build/assemble itself if needed, or cross-target another platform.
I know I'm all talk right now, like you say, I need to manage my free time so that I would have the "copious free time" to work on this.
The only reason I was thinking Raspberry Pi was I wanted something that would be readily available to anyone (although, at the moment it's out of stock due to supply chain issues). I was actually looking at whether anything similar was available with RISC-V (like you mentioned) to target, since that would be interesting.
The same thing but for the PyBadge (because I have one) is on my long list of ideas. Mainly to learn how forth works. But I'm playing around with other stuff at the moment (dependently typed languages).
When I see FORTH stuff like this, I cannot avoid thinking that in a post apocalyptic world, we would be scavenging computer parts and writing FORTHs to use them :)
The idea is to make something like (or a port of) CollapseOS, but with a 32-bit machine. The Oberon RISC chip is big enough to be more useful than 8-bit systems yet simple enough that you could build one out of discreet components if you had to. It's really really simple.
“Forth isn't particularly fast either. As an interpreted language, it is blazing fast, but the speed tax of the threaded model still has to be paid, so it will generally be slower than code produced by a C compiler.”
There are a few issues here. Running threaded code interacts badly with branch prediction. Essentially every "word" (operation) is followed by a branch to the next word, and those seem hard to predict (or at least with 2010-era hardware they were very hard to predict, maybe more modern branch prediction can do better?). The reason is that a popular word, say, +/add, might be called from all over the program. Only one copy of the word exists and the indirect jump to the next word could go anywhere.
However Forth does allow you to "inline" words, basically form a new word by chaining together existing words, which avoids this overhead. This doesn't optimise adjacent words, but it still gets semi-reasonable performance. (Inlining in JONESFORTH: https://github.com/nornagon/jonesforth/blob/d97a25bb0b06fb58...)
Modern Forths just have regular optimising compilers so none of this stuff applies, but they don't have the simple purity of one that you write and fully understand yourself.
Just an anecdotal experience, but I’m currently writing a JVM in Rust with a naive, interpreter loop. OpenJDK’s interpreter is a so-called template interpreter which looks at the method’s byte code and generates a given machine code snippet for each instruction, puts them serially next to each other and executes this buffer. The advantage of the latter is that in the interpreter loop model, one jumps back to the switch statement which will jump to the next instruction’s implementation, etc, while OpenJDK’s solution just continues execution serially, taking advantage of the branch predictor.
In my benchmarks that are definitely not suitable to infer much, OpenJDK without JIT can perform 1.5-8x better, though of course the whole implementation is different so don’t read too much into that.
It's amazing to me in general how much of a divergence in expectations of performance vs reality that branch prediction misses causes in modern software.
So many techniques that would improve performance on a 1980s processor can be woefully inefficient on a 21st century one. It's so easy to be still holding the computing model of the former in your head.
What is absolutely crazy is that many “primitive computation” is basically free. Like, there is more than likely some memory stalls either way and they just happen in the pauses not causing longer execution time.
I learned it the hard way when I was writing an emulator for a retro machine. Say you're blitting sprites to the screen. As you're drawing a line, each pixel either has some portion of a sprite, or doesn't. Your instinct is to do just like the hardware and 'chase the beam' and check at each pixel, or even just for each line, whether a sprite is present.
Super wrong. You're far better off precomputing the whole sprite bitmap -- even if you don't end up using it and doing a bulk operation to display it or not display it. Because doing that "if sprite is here?" in a loop is super super expensive, more expensive than just blitting the splits and not using them.
Most efficient ended up being precomputing things into boolean bitset vectors and using SIMD operations to do things based on those.
Even if not doing GPU stuff, the fastest way to compute these days is to think of everything in bulk bulk bulk. The hardware we have now is super efficient at vector and matrix operations, so try to take advantage of it.
(After doing this I have a hunch that a lot of the classic machine emulators <VICE, etc.> that are out there could be made way faster if they were rewritten in this way. )
Theaded code always interests me, but there definitely is a price to it. Should be noted though that a lot of newer Forths diverge from the classic threaded code model. Because it is a concatenative language, it is possible to have one that compiles native code by copying inlined subroutines (https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.56...).
For example to compile : SQUARE DUP + ; all the compiler has to do is copy the machine code of DUP to the place where SQUARE is being compiled, remove the ret instruction at the end of it, and copy the machine code of + after it. It can also do some small optimizations to remove redundant instructions.
You can do this with other languages, but concatenative languages can make it as simple as literally concatenating bits of code.
As with all languages, the compiled/interpreted distinction is a thing of convention/existing tools. Interpreted Forth is small, and tolerable speed-wise for tiny systems. But Forth can also be compiled directly to native machine code.
Relatively easily, you can get rid of the interpreter overhead by writing blocks of machine code that do each Forth word's action, instead of bytecode to dispatch the interpreter to each of those routines. Forth can be adapted for that easily enough, and some Forths do support this on a per-word basis, allowing you to pick how you want your words compiled.
Forth can also get the whole optimizing compiler treatment. Some optimizing Forth compilers have been released, but I don't know how good they were/are. Certainly never needed that kind of speed myself. I don't know if any Forth yet benefited from it, but a lot of work was put into Java, on approaches for optimizing stack machine-style code to reasonably fast native code for register machines.
For a different apocalypse poem, allow everything existing to be used, but the coming population collapse demands a full stack replacement. To preserve existing capabilities and progress, but supported by far fewer people.
We've had environments with far greater individual programmer productivity than is currently common. But while forth starts out nicely with simple systems, costs escalate unhappily with goal complexity. Similarly with smalltalk, lispms, prolog, haskell. The perennial "we'll easily do a full-stack rewrite in our wonderful X" efforts... haven't.
Past/current efforts avoid implausible goals, like LLVM-quality compilation has to become a one-person project. Or slather on viability constraints, like must run on M in time T, and be accessible to population P. Or "we don't know how to compile that leverage efficiently and consistently - that paper hasn't been written yet - so you can't have it".
But there are increasingly opportunities to do things differently now, if narrower goals demanded it. Community-wide compilation caches - "it ok that the type proof / bulk code translation / whatever sometimes takes days, it only needs to happen once". Compiler runs spinning up 10000 cloud instances. And so on.
What might it look like, to attempt not merely a bootstrap, but a subsuming breakaway?
I think the current system would be supportable with a collapse. You have fewer developers... but also fewer hackers, so I would guess security stays the same, other than if the collapse created extra crime incentive.
We could just freeze all the fundamentals. If it was a real emergency, we could mostly stop working on new niche languages,Stop adding language features, stop deprecating stuff, and just focus on essential applications with only the tech we have.
We'd obviously want some level or new work on the fundamentals and language level stuff, but we'd just focus on things that help you code with less people and poorly trained people(Replacing C with Rust could be the big project of the century).
Tech is pretty great at the moment, seems like we could just get by for centuries, totally long enough to rebuild, if we just had what we have now, and enough hardware infrastructure to keep the fabs running. We don't really need a new kernel or a new graphics card architecture until society is stable again.
> the current system would be supportable with a collapse [...] Tech is pretty great at the moment [...] we could just get by for centuries
Oops, my bad - I was more trying to shape a peculiar apocalypse to highlight seemingly neglected opportunities for progress. Software tech has indeed tremendously improved over these last few decades - we're living years of dreams. But in some respects... it feels like US general aviation - systemic dysfunction leaving society crippled and stuck using decades-obsolete tech. Spewing cognitively-impairing poison (leaded avgas). Take a 1980's person familiar with smalltalk and lisp machines and OT factor and KMS hypertext, and set them down in front of modern hardware, describe it's power, then turn it on, and... one can imagine a reaction with at least some element of "WTF - such awesome power, but why is it so strikingly crippled?". Picture, I think it was, an early Rails demo talk at a conference, with an audience largely of Java-based web devs... and they were just stunned, awed by how much more productive it was. I suggest we've been repeatedly failing to execute on opportunities for such transitions. "Tech is pretty great[...] we could just get by for centuries"... :) Ha, it sometimes feels like we're working towards that. A story of "I crave a stack with these <empowering features> I've seen separately... but fear I will retire first, years from now, never having had it available". Another decade plus will be a half century since the '80s. You only get so many patent expiration and monopoly turnover cycles per century. But maybe a 2020's "Great AR Rewrite" might pressurize progress???
Some time ago I wrote an implementation of Forth that runs on the TI-84+ and TI-83+ calculators directly inspired by Jonesforth.[0] It runs under TI-OS as well, but the amount of available space is somewhat limited. On another implementation I wrote[1] you have full access to the calculator hardware and memory from Forth.
[SSP] Assembly is of course nice, but not always practical. For those interested in a compact, embeddable and portable little Forth written in C, take a peek at zForth: https://github.com/zevv/zForth.
The only thing anyone ever seems to do with forths is write another forth. Why is this?
Unrelatedly (perhaps) I tried writing some basic software in forth and it was pretty horrific. Even something as simple as representing a variable length array feels like fighting the language
I've made a Forth dialect and uses it for somewhat specific stuff. I have a program that works for me 24/7 and that I have tweaked and adapted to the evolution of my needs for a decade.
It would be a PITA to strip it from its business-related stuff in order to publish it, so you will probably never see it online. I guess it's the same for other "serious" users.
> Unrelatedly (perhaps) I tried writing some basic software in forth and it was pretty horrific. Even something as simple as representing a variable length array feels like fighting the language
Yeah, don't fight the language, that's the key. And don't do stuff because that's what every other language is doing. "Solve the problem you've got!" Moore said. It could seem dead obvious, but it's actually difficult to prevent yourself from trying to solve problems you think you will have tomorrow, or from solving problems others have.
More specifically don't try to avoid using Forth variables because they are global and you've been taught that globals are absolute evil madness that eat kitties for breakfast. Instead, pick perhaps a couple of values that travel everywhere, causing "stack juggling" in your program, and make them global variables. Maybe hide those global variables behind regular words if you really really really intend to reuse parts of that program.
But really, solve the problem you have now. This also means you should make programs that will actually be useful for you. It can be difficult because this generally involves complicated stuff like connecting to a web server or GUI. Just be pragmatic and use native libraries (or whatever you can find that looks decent, gets the job done, and can be used by the Forth system you're using) to deal with the complicated crap others have been doing for decades. Forth hates complexity, any cheat is fair game.
Feel free. I somehow got the mistaken idea that you were asking questions, but it turns out that your were stating opinions that should not be questioned.
It is the community that surrounds forth, and the type of hobbyists that are attracted to forth, as well as what they are attracted to.
It’s a bit like saying the only thing anyone ever seems to do with Common Lisp is to make macros which redefine the syntax of language. How frustrating! We’ll if they didn’t want to make use of macros, why were they using lisp in the first place?
Part of the appeal of forth is making your own forth environment. That is the whole answer I think.
Edit: notably there are applications of forth which don’t involve reinventing the wheel, like Postscript and Bitcoin Script. In these domains devs aren’t so likely to engage in this activity.
Well there's an active community of people writing their own operating systems. Why do that when Linux is available? I don't think a further reason is required for the hobbyist.
But there are various actual products which result from efforts to create a minimal forth that are interesting. One of my favorites is the J1 CPU: https://www.excamera.com/sphinx/fpga-j1.html
Also I think many of these personal forths have explored interesting areas of programming language design, and when I have read about those which have been made public I often learn something new. It's like code poetry.
> Unrelatedly (perhaps) I tried writing some basic software in forth and it was pretty horrific.
It might be instructive to compare FORTH to e.g. the previously discussed language K, which is similarly terse in some ways. It turns out that a few convenience features - including first-class- and higher-order- functions, neither of which really sit well within the FORTH stack-focused model - can make a real difference.
It is not the only thing, but rather the first thing that people often write. There is nothing particularly wrong with that, as forth is the kind of language where intimate knowledge of its internals is quite useful, perhaps even necessary, while simultaneously being easy to obtain (e.g. by writing your own forth). And of course, the number of people who complete one project is necessarily greater than or equal to the number of people who do more than one project.
The last thing that causes the phenomenon you describe to happen is likely the culture around forth, likely as a result of the relative ease and utility of making domain-specific abstractions compared to general abstractions in forth. The community seems to not appreciate libraries and reusable code to the same extent as other ecosystems, preferring instead to share ideas, which each developer can then adapt to fit their particular project in a very minimalist manner instead of adopting some convoluted general solution.
Here is some interesting Forth software, including a Space Invaders game and an IP networking stack including IP, ARP, UDP, DNS and DHCP [1]. It runs on the J1 custom Forth CPU built on an FPGA [2]. The author says it was originally designed to run an array of Ethernet cameras on a robot.
Because forth is an exercise in minimalism, its answer to most features is YAGNI. The coolest thing about it is how easy something as weird as forth maps to the quite different different underlying (register-based) hardware, so you can use a forth to bootstrap something with more sugar.
A concrete slab is a basic building block of a pavement, but it's not particularly useful or appropriate for a wristwatch.
I think there's a gap between what you're trying to solve and what you think you're trying to solve, and that's giving a massive impedance mismatch between what you're trying to do and how you're trying to do it.
How would you go about allowing the user to enter and store data at runtime without knowing the size of the data in advance then? I don’t agree with you at all. A variable amount of data that can change at runtime is universal across all programs. This isn’t something specific to a certain coding style
I must mention Factor, which is a modern concatenative, stack-based programming language. It turns out Forth was just the first concatenative language discovered/constructed.
It's strange that Lisp became so popular, and Forth - not so much! I've enjoyed GraFORTH on Apple ][ and then using it to generate it in PostScript to generate logos and complex graphics programmatically not using an editor.
What a cool thing and this thread also appears to be full of gems. I'm on a Lisp journey for the next little while, but I'll eventually get to Forth. Filing all this away for later...
I would recommend going the other way as it will probably be easier to appreciate both that way, just from my own experience both learning and writing interpreters for both.
Lisp is sort of the next step after Forth if you ask me, adding slightly more syntax to get better abstractions.
I'm inclined to agree with you, just chiming in to say that historically, Chuck Moore was a student of John McCarthy, so Forth was one of many 'next steps' after Lisp. Which is a very old language.
https://github.com/matematikaadit/jombloforth
If you like forth there's an awesome series of comments here on hacker news on building a simple variant in a few simple steps:
https://news.ycombinator.com/item?id=13082825
I took that, and built a simple forth-like system, in golang following the original recipe and breaking it down into simple steps for learning-purposes:
https://github.com/skx/foth