7 ms·
As I like to say: "C is a language that solves a million problems. Forth is a million languages that solve almost nothing." :-P I've been reading about Forth f
by bxparks 1y ago
As I like to say: "C is a language that solves a million problems. Forth is a million languages that solve almost nothing." :-P
I've been reading about Forth for 30-40 years. The dual stack is easy to understand. My problem is that I cannot see how control flow works in Forth, e.g. a simple if-then-else.
I think that something as fundamental as an if-then-else should be obvious in a useful language. Heck, it's obvious in assembly language. But not in Forth.
- alexisread 1y agoHave you tried looking at Sectorforth? In most languages branching is a fundamental construct, it's created here (with comments) https://github.com/cesarblum/sectorforth/blob/master/examples/01-helloworld.f https://github.com/cesarblum/sectorforth/blob/master/example... Effectively IF compiles a 0= ie. If false and then a dummy target address. THEN (aka ENDIF) compiles the real target address over the dummy one, which is the address after THEN.
- kamaal 1y ago>>Forth is a million languages that solve almost nothing." :-P That brings us to the question, when it was invented and people did use it. What kind of problems were they solving with it?
- jonjacky 1y agoForth was invented around 1970 for controlling equipment in an astronomical observatory, running on a PDP-11, a 16-bit computer with up to 64 Kbytes of memory. Its heyday was the 1970s and 80s, when it was mostly used for small embedded systems on 8- or 16- bit processors with 8 kb -- 64 kb of memory. It was possible to run an entire Forth development system along with the application on these small targets without resorting to a bigger computer for cross-development. The usual alternative to Forth on those systems was assembly language.
- EasyMark 1y agoI was dropped into one such system after it failed after like 20 years and learned forth on the fly while managers breathed down my neck lol. Not a fun experience, and it took about 3X as long as I thought it would take, so Forth is not my favorite language, but I do see why it was useful at the time, and as an exercise in a way of thinking about languages rather than the mode I usually operate in c/c++/rust/little javascript
- failingforward 1y agoForth started as Chuck Moore’s solution to the problem of how to bring up an interactive programming environment on hardware with limited memory. The base of the system used a small number of primitives written in assembler or machine code upon which more complex functions were built. The genius of the system was that you could easily bring it up on different hardware by translating the primitives, which was quite helpful at a time when software was frequently customized to the hardware (which itself was not as standardized as today). Nowadays Forth is probably most useful on embedded systems.
- bxparks 1y ago> Nowadays Forth is probably most useful on embedded systems. Nowadays almost nothing is written in Forth. That's the problem with Forth. Even on 8-bit Arduino microcontrollers with 2kiB of RAM, we write programs in C++ (with a little bit of C, on top of hand-coded AVR assembly).
- s-macke 1y agoThe Starflight role-playing game for DOS [0] was written in its own Forth dialect. Even today, it remains one of the games with the strongest Star Trek feeling. [0] https://en.wikipedia.org/wiki/Starflight https://en.wikipedia.org/wiki/Starflight
- andsoitis 1y ago> My problem is that I cannot see how control flow works in Forth, e.g. a simple if-then-else. Hopefully this is helpful: https://www.forth.com/starting-forth/4-conditional-if-then-statements/ https://www.forth.com/starting-forth/4-conditional-if-then-s...
- bxparks 1y agoThanks for that. I kinda know how it "works" at the user-level. I meant to say, I don't know how it is implemented. My mental model of Forth is that there is a simple parser that consumes space-delimited keywords. The interpreter looks up that keyword in a dictionary, which gives the address of the machine code that handles that word. The interpreter either makes a subroutine call to that address (subroutine threaded), or jumps to that address (called "direct threaded" if I recall, where the handler jumps back to the interpreter instead of executing a RET). But that's where my mental model of Forth breaks down, because IF-THEN-ELSE cannot be implemented in that model. So there must be something else fundamental in the Forth interpreter that I don't understand.
- addaon 1y ago> So there must be something else fundamental in the Forth interpreter that I don't understand. The missing bit is IMMEDIATE mode. Words can be tagged as IMMEDIATE, which means that they get executed at compile time (or parse time, for an interpreter), rather than a call to them getting compiled (executed at run time, for an interpreter). IF/ELSE/THEN are then "just" IMMEDIATE mode words -- but you can add your own. The "special sauce" is that IF compiles (easier to talk about for a compiler; generalize as needed) a conditional branch to an unknown address, and puts the address of that branch instruction (or an equivalent) on the /compile time/ data stack; THEN then looks at the address on the /compile time/ data stack and patches that instruction to branch to the correct address. Plenty of subtlety possible, but the basic primitive of IMMEDIATE mode is the key.
- bxparks 1y agoAh I see, this is peeling back a few layers of obscurity about the Forth interpreter for me. Let's stick with a Forth interpreter because that seems easier to think about for me. Are you saying that the Forth interpreter is a 2-pass interpreter? Or does the interpreter go into a special IMMEDIATE mode upon hitting the IF keyword, then it just consumes subsequent tokens without doing any dispatching, until it hits the THEN token? It sounds like nested IF-THEN-ELSE becomes tricky to handle. How does the FORTH interpreter handle loops? Does the interpreter hit the WHILE token, goes into IMMEDIATE mode, remembers the location of the WHILE, then dispatches all the subsequent code, until it hits the REPEAT token, then branches back to the WHILE?
- kragen 1y agoIt's funny that you say that, because from my point of view, control flow in Forth is one of its strong points over assembly language, because it has properly nesting control-flow structures. The syntax is a little strange; the literal translation of if (siz != 0) *d = '\0'; is (untested) siz @ 0<> if 0 d @ c! then when what you'd normally expect is something like if siz @ 0<> then 0 d @ c! fi but that's sort of a question of RPN. At least else comes where you expect it, even though that makes then even stranger; we can translate if (key < tree->key) { tree++; } else { tree = tree->right; } literally as (untested) key @ tree ->key @ < if kvpairsize tree +! else tree ->right @ tree ! then Now, if you were comparing Forth to Lua or C here, I would agree: this is far less obvious, in the sense that it's completely unfamiliar and requires you to learn Forth's idiosyncratic way of writing things. But in assembly language? GCC generates, in context, the following assembly code for the C if-else statement above: addhi r0, r0, #8 bhi .L5 ldr r0, [r0, #4] b .L6 This is using the flags from a preceding equality comparison, and the branch target labels are earlier in the enclosing loop. You can not tell me that this is "obvious" if you don't know ARM assembly! From a certain point of view, Forth is just what you get if you take assembly language and look for the simplest way to add properly nesting expressions, properly nesting control structures, subroutines with arguments and return values, arbitrary compile-time computation, an interactive debugging environment, a scripting language, disk storage, and multithreading.
- bxparks 1y agoI kinda know how it works at the user-level, I meant to write that I don't understand how control-flow is implemented in Forth. I say more about that in my reply to a sibling comment: https://news.ycombinator.com/item?id=45334556 https://news.ycombinator.com/item?id=45334556
- kragen 1y agoOh! I see. I've commented there.
- andrewla 1y agoA million times this. The syntax is not difficult to see, but the action seems mysterious. The article gives an example > : buzz? 5 mod 0 = IF ." Buzz" THEN ; Seems to work okay. What about immediate mode? > 10 5 mod 0 = IF ." Buzz" THEN ; action is not a function Well, I guess that does it for me. Factor, another stack-based languages, has a more legible version of this, where you can push an anonymous lambda onto the stack. As I recall from my days of programming HP-48's, that used a similar mechanism. (Not checking my syntax here) > 5 mod 0 = << "buzz" print >> if Would have a similar effect. Each entry makes sense -- the << switches from immediate mode to store mode (or some similar concept), and everything ends up on the stack. "If" is just a function that takes a boolean and a closure: > 10 level: 0 ; stack: [10] > 5 level: 0 : stack: [10 5] > mod level: 0 ; stack [0] > 0 level: 0 ; stack [0 0] > = level: 0 ; stack [true] > << level: 1 ; stack [true] [] > "buzz" level: 1 ; stack [true] ["buzz"] > print level: 1 ; stack [true] ["buzz" print] > >> level: 0 ; stack [true pointer_to_function] > if "buzz" level: 0 ; stack [] But I don't understand what Forth is doing.
- kragen 1y agoForth control structures don't work in interpret state because control structures involve executing code out of order: jumping from the end of a loop back to its beginning, etc. In interpret state there's nowhere to jump to. So you have to put control structures inside a colon definition for them to work. Also true of strings in traditional Forths, but GForth just dynamically allocates some memory and leaks it instead.
- kabdib 1y ago> My problem is that I cannot see how control flow works in Forth, e.g. a simple if-then-else. figuring this out for my own FORTH interpreter was a moment i still remember, nearly 50 years later. quite a revelation
- bxparks 1y agoIn my opinion, a language that requires a programmer to have a "revelation" to understand basic control flow is not a language that is useful or practical for solving real world problems. I would prefer to write in assembly language than write in Forth. Which is what I have done with one of my current projects. With assembly language, there is a good chance that a random person with some minimal programming skills would understand my program if I were hit by a bus. With Forth, I think the chances of that are close to zero.
- talideon 1y agoIf you're coding, you don't have to understand how to implement control flow. The average C programmer hasn't a clue how the underlying control flow is implemented. It's an _implementor_ of an interpreter or compiler who needs to understand this. Forth is no different from C or any other language in this regard, except that, in Forth, control flow can be implemented directly rather than relying on the compiler or interpreter to understand them. Immediate words are essentially a kind of macro, if it makes things easier for you.
- vdupras 1y agoI remember, many years ago, when I was learning programming. When I grokked recursion, it was a revelation to me. Could I be a programmer without that revelation? Yeah, kind of, but I'd be a lesser one.
- kragen 1y agoIt's a revelation to understand how basic control flow is implemented in any compiler. From your described preference to not learn things that aren't generally known, it's a safe bet that you don't understand CPS or SSA either, or know what a basic block is.
- lebuffon 1y agoThe hard part I think is grokking the fact that the FORTH can compile and interpret inside the same definition. Also one must understand the operation of the Forth primitives HERE and comma (,) I will take a run at explaining IF ENDIF (endif is the Fig Forth term, used here to avoid confusion) ?BRANCH is an instruction in the virtual machine. It jumps if top of stack=0 . The offset (or address)that it jumps to is the memory word right after the ?BRANCH token. Like this: <?BRANCH><number> Forth definition of IF : IF COMPILE ?BRANCH HERE 0 , ; IMMEDIATE At compile time IF "compiles" the token for ?BRANCH but then interprets "HERE 0 ," (IF is an IMMEDIATE word that executes even if the compiler is turned on) HERE is like $ in Assembler, ie: the address where code is being laid down. It is simply left on the data stack. HERE is the address where the <number> will be stored... later. 0 is a zero, that is pushed onto the data stack. "Comma" (,) pops the zero and puts it in memory address HERE but! it advances the system memory pointer 1 integer width. The zero is now a place holder in memory to be filled in by ENDIF. : ENDIF( addr -- ) HERE OVER - SWAP ! ; IMMEDIATE ENDIF needs that address left behind by IF shown in comment as addr. ENDIF gets the new value of HERE which of course is different because we will have compiled some code after the IF keyword. All we need to do is do NEWHERE-OLDHERE to get the offset for ?BRANCH. That is covered by the forth code ( oldhere-on-stack) HERE OVER - This will make the data stack be: ( OLDHERE offset ) If we do a SWAP we just need the store operator '!' to put the offset into memory. For the morbidly curious here is the definition of ELSE. :-) : ELSE COMPILE BRANCH HERE 0 , SWAP [COMPILE] ENDIF ; IMMEDIATE So loops are just more of the same... (all loops jump back to BEGIN. BRANCH is an unconditional jump instruction) : BEGIN HERE ; IMMEDIATE : AGAIN COMPILE BRANCH HERE - , ; IMMEDIATE : UNTIL COMPILE ?BRANCH HERE - , ; IMMEDIATE : WHILE [COMPILE] IF SWAP ; IMMEDIATE : REPEAT [COMPILE] AGAIN [COMPILE] ENDIF ; IMMEDIATE
- zovirl 1y ago> My problem is that I cannot see how control flow works in Forth, e.g. a simple if-then-else. What made it click for me was http://www.exemark.com/FORTH/eForthOverviewv5.pdf http://www.exemark.com/FORTH/eForthOverviewv5.pdf, specifically sections 2.3 "Loops and Branches" and 5.3 "Structures". With a slight simplification, if/else/then branching is defined in 7 words. Two primitive words, branch and ?branch (in python because I know it better than assembly): def branch(): """ branch is followed by an address, which it unconditionally jumps to.""" ip = code[ip] # Get address from next cell in code, jump to it. def branch_if_zero(): """ ?branch is followed by an address. ?branch either jumps to that address, or skips over the address & continues executing, depending on the value on the stack.""" if stack.pop() == 0: # Pop flag off stack ip = code[ip] # Branch to address held in cell after ?branch else: ip += 1 # Don't branch, skip over address & keep executing Two helper words for forward branching. >MARK adds a placeholder branch address to code and pushes the address of the placeholder. >RESOLVE resolves the branch by replacing the placeholder with the address from the stack. : >MARK ( -- A ) HERE 0 , ; : >RESOLVE ( A -- ) HERE SWAP ! ; And then the actual IF, ELSE, and THEN words. IF puts ?branch and a placeholder address in code. ELSE puts branch and a placeholder address in code, then updates the preceding branch address (from an IF or ELSE) to land after the ELSE. THEN updates the preceding branch address to land after the THEN. : IF ( -- A ) COMPILE ?branch >MARK ; IMMEDIATE : ELSE ( A -- A ) COMPILE branch >MARK SWAP >RESOLVE ; IMMEDIATE : THEN ( A -- ) >RESOLVE ; IMMEDIATE
- bitwize 1y agoHint: IF, THEN, and ELSE are words that are partially evaluated at compile time. IF remembers its own location and reserves space for a jump; THEN compiles a conditional jump if NOT true to its location, in the place where IF was, unless there was an intervening ELSE, in which case it will compile such a conditional jump to ELSE's location at IF, and an unconditional jump to then just before ELSE. So a full use might look like: : TEST > IF ." greater" ELSE ." less or equal" THEN ; Which is the equivalent of: void test(x,y) { if(x > y) { printf("greater"); } else { printf("less or equal to"); } } THEN works differently in Forth than in, like, BASIC. It marks the end of the whole conditional block, not the start of the consequent.