PONYλM2Modula-2

Fortran.CodeCompared.To/Forth

An interactive executable cheatsheet comparing Fortran and Forth

Fortran 2018 (GCC 16.2) Forth 2012 (WAForth 0.20.1)
Output & Running It
Hello, World
Fortran needs a program unit for the linker to find. Forth has no entry point: the file is read top to bottom and each line runs as it is read, so defining HELLO and calling it are two separate acts.
program hello implicit none print "(a)", "Hello, World!" end program hello
: HELLO ." Hello, World!" CR ; HELLO
🚨 The format "(a)" is deliberate — list-directed print *, emits a leading space before the text, which is why every Fortran column on this page names its format explicitly.
Printing A Number
The i0 edit descriptor prints an integer in the minimum width it needs. Forth's . prints the cell on top as a signed number followed by one space, and there is no format to choose.
program show_number implicit none print "(i0)", 6 * 7 end program show_number
6 7 * . CR
🚨 print *, 42 would right-align the number in a ten-column field, padding it with nine spaces. Forth's . has the opposite habit — one trailing space, always — which is why a number with anything after it has to be built with pictured output instead.
Two Numbers On One Line
Fortran's format string names each field and the separator between them — 1x is one space. Forth pushes both numbers and prints them with two .s, which emit their own trailing space each.
program show_pair implicit none print "(i0,1x,i0)", 3, 4 end program show_pair
3 4 . . CR
The two columns print the same text by completely different means: one describes the line and then supplies values, the other emits values and lets the spacing fall out. Note that . prints the top first, so the 4 is printed before the 3 unless they were pushed in this order.
Where Forth Came From
The Telescope Job
Forth's first real job was pointing radio telescopes at the National Radio Astronomy Observatory — work that was otherwise done in Fortran and assembly. Charles Moore wrote it because the alternatives were too slow to iterate with on a machine sitting at the telescope.
program track_source implicit none integer :: step integer :: azimuth azimuth = 0 do step = 1, 4 azimuth = azimuth + 15 print "(i0)", azimuth end do end program track_source
VARIABLE AZIMUTH 0 AZIMUTH ! : STEP-ON ( -- ) AZIMUTH @ 15 + DUP AZIMUTH ! . CR ; : TRACK ( n -- ) 0 ?DO STEP-ON LOOP ; 4 TRACK
The Fortran column is a batch program: write it, compile it, run it, read the output. The Forth column is a set of words you can call one at a time from a prompt on the instrument, which is the whole reason the language exists. Everything else on this page follows from that one difference in purpose.
There Is No Compile Step
EVALUATE takes an address and a length and interprets the text as Forth source immediately, using the same interpreter that read the rest of the file. The compiler is an ordinary word and is present on the target machine.
program no_eval implicit none character(len=5) :: source source = "2 + 3" print "(a)", "cannot run: " // source end program no_eval
: RUN-SOURCE S" 2 3 + . CR" EVALUATE ; RUN-SOURCE
A Fortran program is finished before it runs — that is what made it wrong for a telescope being commissioned, where the next thing to try was decided by what the last thing did. Being able to type a new word at the instrument is the capability Moore was buying.
Words Instead Of Subroutines
FUNCTION Versus :
A Fortran function declares its result type, its argument types, and the intent of each. SQUARE declares none of that — DUP * copies whatever is on top and multiplies.
program use_square implicit none print "(i0)", square(9) contains integer function square(value) integer, intent(in) :: value square = value * value end function square end program use_square
: SQUARE ( n -- n*n ) DUP * ; 9 SQUARE . CR
The intent(in) that tells the Fortran compiler this argument will not be modified has no counterpart at all: a Forth word may consume, replace, or leave extra cells, and only the comment says which. What you get back is a definition that fits on one line.
intent(out) Versus Leaving A Cell
Fortran returns several results by taking intent(out) arguments the caller has already declared. A Forth word simply leaves two cells where it found its arguments — there is nothing to declare and nowhere to put it.
program use_divide implicit none integer :: quotient, remainder call divide(17, 5, quotient, remainder) print "(i0,1x,i0)", quotient, remainder contains subroutine divide(numerator, denominator, quotient, remainder) integer, intent(in) :: numerator, denominator integer, intent(out) :: quotient, remainder quotient = numerator / denominator remainder = mod(numerator, denominator) end subroutine divide end program use_divide
: DIVIDE ( numerator denominator -- remainder quotient ) /MOD ; 17 5 DIVIDE . . CR
The saving is real and so is the loss: the Fortran caller names each result at the call site, and the compiler checks the count and the types. Here the comment says -- remainder quotient because /MOD leaves the quotient on top, and nothing verifies that claim.
Recursion
Older Fortran needed the recursive keyword to permit a function to call itself; since Fortran 2018 it is the default. Forth needs RECURSE for a different reason: the word is not in the dictionary until ; runs, so its own name does not yet mean anything.
program use_factorial implicit none print "(i0)", factorial(5) contains recursive integer function factorial(n) result(answer) integer, intent(in) :: n if (n > 1) then answer = n * factorial(n - 1) else answer = 1 end if end function factorial end program use_factorial
: FACTORIAL ( n -- n! ) DUP 1 > IF DUP 1 - RECURSE * ELSE DROP 1 THEN ; 5 FACTORIAL . CR
The result(answer) clause exists because a recursive Fortran function cannot use its own name as the result variable. Forth has the mirror-image problem and the mirror-image answer — writing FACTORIAL inside the body would find an earlier word of that name, which is occasionally exactly what you want.
There Is No implicit none
implicit none is the first line of every modern Fortran program because without it a misspelled name silently becomes a new variable typed by its first letter. Forth has no implicit anything: an unknown word is an error when the line is read.
program typo_caught implicit none integer :: counter counter = 1 ! countr = 2 ! without implicit none this would be a new REAL variable print "(i0)", counter end program typo_caught
VARIABLE COUNTER 1 COUNTER ! COUNTER @ . CR .( COUNTR would be "undefined word", not a new variable ) CR
This is one of the few places Forth is the safer of the two by default. It comes from the same property that makes everything else risky — there is one flat dictionary and a name is either in it or it is not, so there is no declaration to have been skipped.
DIMENSION Versus CREATE … ALLOT
Declaring An Array
CREATE makes a word that pushes its own address and , lays one cell down after it. There is no dimension, no element type and no bound — the array is an address, and the count is a separate constant you keep correct.
program sum_readings implicit none integer, dimension(3) :: readings readings = [10, 20, 30] print "(i0)", sum(readings) end program sum_readings
CREATE READINGS 10 , 20 , 30 , 3 CONSTANT #READINGS : SUM-READINGS ( -- total ) 0 #READINGS 0 ?DO READINGS I CELLS + @ + LOOP ; SUM-READINGS . CR
Fortran knows the shape, so sum works on the whole array and the compiler can check conformance. Here the loop is written out and #READINGS is a promise: change the initialiser without changing the constant and nothing complains.
Indexing Starts At Zero
Fortran arrays start at 1 by default, and you can declare any lower bound you like with dimension(0:2). A Forth index is an offset from the start of the array, so the first element is at 0 and there is no bound to declare.
program first_two implicit none integer, dimension(3) :: readings readings = [10, 20, 30] print "(i0,1x,i0)", readings(1), readings(2) end program first_two
CREATE READINGS 10 , 20 , 30 , : READING ( index -- addr ) CELLS READINGS + ; 0 READING @ . 1 READING @ . CR
Both columns print the same two numbers, and the indices that produced them are 1, 2 on one side and 0, 1 on the other — which is the whole row. Nothing checks the Forth index, so 3 READING quietly reads whatever was defined next.
There Is No Array Section
Fortran's values(2:3) is a first-class array section that sum can take directly. Forth has no section and no whole-array operation, so the range becomes two numbers passed into a loop.
program show_slice implicit none integer, dimension(5) :: values values = [3, 1, 4, 1, 5] print "(i0)", sum(values(2:3)) end program show_slice
CREATE VALUES 3 , 1 , 4 , 1 , 5 , : SUM-RANGE ( start count -- total ) 0 SWAP 0 ?DO OVER I + CELLS VALUES + @ + LOOP NIP ; 1 2 SUM-RANGE . CR
Array sections are the thing Fortran is best at and the thing Forth has least of — no shape, no conformance checking, and no elemental operations. A numerical program that leans on them has no idiomatic translation here at all.
Two Dimensions By Hand
Fortran declares the shape and computes the address for you, in column-major order. Forth has one flat region, so the row-and-column arithmetic is a word you write — and J is how the body of an inner loop reaches the outer loop's index.
program show_grid implicit none integer, dimension(2,3) :: grid integer :: row, column do row = 1, 2 do column = 1, 3 grid(row, column) = row * 10 + column end do end do print "(i0,1x,i0)", grid(1,3), grid(2,1) end program show_grid
3 CONSTANT COLUMNS CREATE GRID 2 COLUMNS * CELLS ALLOT : CELL-AT ( row column -- addr ) SWAP COLUMNS * + CELLS GRID + ; : FILL-GRID 2 0 DO COLUMNS 0 DO I 1 + J 1 + 10 * + J I CELL-AT ! LOOP LOOP ; FILL-GRID 0 2 CELL-AT @ . 1 0 CELL-AT @ . CR
🚨 The layout here is row-major because that is what row * COLUMNS + column means, while Fortran stores columns first. That difference is invisible until a program walks memory in order and gets the slow traversal, which is the classic Fortran performance trap in reverse.
COMMON Versus The Dictionary
Shared State Without A Block
A module is how modern Fortran shares state — and before it, a common block, which laid variables out in a block each program unit described for itself. VARIABLE makes a word that pushes an address, and any word can read or write it.
module shared_state implicit none integer :: azimuth = 0 end module shared_state program use_shared use shared_state implicit none azimuth = azimuth + 15 azimuth = azimuth + 15 print "(i0)", azimuth end program use_shared
VARIABLE AZIMUTH 0 AZIMUTH ! : BUMP ( -- ) AZIMUTH @ 15 + AZIMUTH ! ; BUMP BUMP AZIMUTH @ . CR
Every Forth definition is effectively in one common block: the dictionary is flat, there are no modules, and a name is visible to everything compiled after it. The old Fortran hazard of two units describing a common block differently has no counterpart, because there is only one description.
One Flat Dictionary
Fortran's use … only: imports exactly the names you ask for, and a module can keep the rest private. Forth has no namespaces in the core language: a word is in the dictionary or it is not, so the prefix is the namespace.
module geometry implicit none contains integer function area(side) integer, intent(in) :: side area = side * side end function area end module geometry program use_geometry use geometry, only: area implicit none print "(i0)", area(4) end program use_geometry
: GEOMETRY-AREA ( side -- area ) DUP * ; 4 GEOMETRY-AREA . CR
Naming conventions carry the whole weight on a large Forth program, and systems that outgrow them add vocabularies — a mechanism this implementation does not have. It is the clearest scaling limitation on the page.
Fixed Types Versus One Cell
KIND Versus One Width
Fortran lets you name the storage size of every integer with kind, and the compiler keeps track of it. Forth has one width — the cell, 32 bits here, which 1 CELLS 8 * reports — and no way to ask for another.
program show_kinds implicit none integer(kind=2) :: small integer(kind=4) :: medium small = 32767 medium = 2147483647 print "(i0,1x,i0)", small, medium end program show_kinds
32767 . 2147483647 . CR 1 CELLS 8 * . CR
A program that needs 16-bit storage packs two values into a cell by hand, and one that needs 64 keeps two cells and does the carries itself. The width is a property of the system, so the same source prints 64 on a desktop Forth.
No REAL At All
Fortran is the language floating point was designed alongside, and real and double precision are its natural types. This Forth has no floating point at all, so a fractional quantity is stored scaled and only given a decimal point when it is printed.
program show_volts implicit none integer :: millivolts millivolts = 3300 print "(i0,a,i0)", millivolts / 1000, ".", mod(millivolts, 1000) end program show_volts
: .VOLTS ( millivolts -- ) S>D <# # # # [CHAR] . HOLD #S #> TYPE ; 3300 .VOLTS CR
The <# … #> sequence is pictured numeric output, building the text right to left one digit per #. For a reader whose working life is numerical, this is the single biggest thing missing — and it is missing from this implementation, not from Forth generally, which has an optional floating-point word set.
Division And Rounding
Fortran's / on integers truncates toward zero and mod follows it, while modulo gives the floored answer. Forth names the arithmetic rather than the intent: SM/REM is symmetric, FM/MOD is floored.
program show_division implicit none print "(i0,1x,i0)", -7 / 2, mod(-7, 2) print "(i0,1x,i0)", floor(-7.0 / 2.0), modulo(-7, 2) end program show_division
-7 2 /MOD . . CR : FLOORED ( n d -- remainder quotient ) >R S>D R> FM/MOD ; -7 2 FLOORED . . CR
🚨 /MOD's rounding is implementation-defined: this Forth truncates and prints -3 -1, while gforth floors and prints -4 1 for the same line. Code that cares must say SM/REM or FM/MOD and supply the double-cell dividend that S>D makes.
Both Languages Have A DO Loop
Two DO Loops, Different Bounds
Both languages spell the counted loop DO, and they disagree about the bounds. Fortran's do index = 0, 4 is inclusive of 4; Forth's 5 0 DO is half-open and stops before 5.
program count_up implicit none integer :: index do index = 0, 4 write(*, "(i0,1x)", advance="no") index end do print * end program count_up
: COUNT-TO ( limit -- ) 0 DO I . LOOP CR ; 5 COUNT-TO
🚨 The limit is also pushed first in Forth — 5 0 DO, not 0 5 DO — which is the reverse of how the range reads aloud. Between the two differences this is the likeliest place for a Fortran reader to produce an off-by-one that still runs.
The Zero-Trip Loop
Fortran's do has been zero-trip since FORTRAN 77: do index = 1, 0 runs no iterations. Forth's plain DO does not test first — 0 0 DO runs the body once and then wraps all the way round the cell.
program zero_trip implicit none integer :: index do index = 1, 0 write(*, "(i0,1x)", advance="no") index end do print "(a)", "done" end program zero_trip
: SAFE-COUNT ( limit -- ) 0 ?DO I . LOOP ." done" CR ; 0 SAFE-COUNT
?DO is the version that tests before the first iteration, and it is what you want whenever the count comes from a variable. A Fortran reader has not had to think about this since 1977, which is exactly why it catches people.
EXIT And CYCLE
Fortran's exit leaves the loop and cycle skips to the next iteration. Forth has LEAVE for the first; there is no CYCLE, and skipping means wrapping the rest of the body in an IF.
program scan_readings implicit none integer, dimension(4) :: readings integer :: index readings = [3, 8, 15, 4] do index = 1, 4 if (readings(index) > 10) exit write(*, "(i0,1x)", advance="no") readings(index) end do print * end program scan_readings
CREATE READINGS 3 , 8 , 15 , 4 , : SCAN ( -- ) 4 0 DO READINGS I CELLS + @ DUP 10 > IF DROP LEAVE THEN . LOOP CR ; SCAN
The DROP before LEAVE is required because the reading is still on the stack and nothing else will take it. Fortran's exit needs no such care, because the value was in a declared variable that simply stops being read.
SELECT CASE Versus A Table
🚨 This Forth has no CASE statement. The idiom once there are more than two or three branches is a table of execution tokens: ' pushes a word's address, , lays it down, and EXECUTE runs whichever one the index selects.
program dispatch implicit none integer :: command do command = 0, 2 select case (command) case (0) print "(a)", "stop" case (1) print "(a)", "start" case default print "(a)", "reset" end select end do end program dispatch
: STOP ." stop" CR ; : START ." start" CR ; : RESET ." reset" CR ; CREATE COMMANDS ' STOP , ' START , ' RESET , : DISPATCH ( index -- ) CELLS COMMANDS + @ EXECUTE ; : RUN-ALL 3 0 DO I DISPATCH LOOP ; RUN-ALL
That is the jump table a select case on small integers usually compiles into, built by hand and visible. There is no case default and no bounds check: an index of 3 reads the cell after the table and executes it.
Strings Carry Their Length
CHARACTER(len=) Versus Address And Length
A Fortran character(len=14) is a fixed-width field, blank-padded to its declared length. S" leaves an address and a length as two separate cells, and the length is whatever the text actually is.
program show_text implicit none character(len=14) :: text text = "borrowed slice" print "(i0)", len(text) print "(a)", text end program show_text
: TEXT ( -- addr len ) S" borrowed slice" ; : SHOW TEXT NIP . CR TEXT TYPE CR ; SHOW
🚨 Fortran's blank padding is the trap in the other direction: assigning a shorter string leaves trailing spaces, which is why trim appears everywhere in real code. Forth has no padding and no trim, because there is no declared width to pad to.
Substrings
Fortran's substring notation is built in and 1-based, so text(10:14) is the last five characters. Forth adjusts the address and the length instead — and /STRING is not in this implementation, so the row defines it.
program show_slice implicit none character(len=14) :: text text = "borrowed slice" print "(a)", text(10:14) end program show_slice
: /STRING ( addr len n -- addr+n len-n ) DUP >R - SWAP R> + SWAP ; : TEXT ( -- addr len ) S" borrowed slice" ; TEXT 9 /STRING TYPE CR
Defining a missing word is ordinary Forth practice rather than a workaround, and it is one line because the operation really is just "add to the address, subtract from the length". No copy is made in either column; Fortran's substring is also a reference into the original.
Joining Text
Fortran's // concatenates and the result's length is known at compile time. Forth has no operator: you reserve a buffer and copy bytes into it with MOVE, tracking how far you have got in a VARIABLE.
program join_text implicit none print "(a)", "hello" // " " // "world" end program join_text
CREATE JOINED 32 ALLOT VARIABLE FILLED 0 FILLED ! : APPEND ( addr len -- ) DUP >R JOINED FILLED @ + SWAP MOVE R> FILLED +! ; : BUILD S" hello" APPEND S" " APPEND S" world" APPEND ; BUILD JOINED FILLED @ TYPE CR
Doing it by hand makes the cost visible — one buffer, one copy per piece — and makes the buffer's size your problem. The 32 ALLOT is a promise nothing checks, where Fortran would have rejected an over-long assignment at compile time.
Passing A Procedure
Passing A Procedure
Fortran needs an explicit interface block so the compiler knows the shape of the procedure being passed. Forth pushes an execution token with ' and runs it with EXECUTE — a token is just a number, so there is nothing to declare.
program apply_both implicit none print "(i0,1x,i0)", apply_to(5, double), apply_to(5, triple) contains integer function double(value) integer, intent(in) :: value double = value * 2 end function double integer function triple(value) integer, intent(in) :: value triple = value * 3 end function triple integer function apply_to(value, operation) integer, intent(in) :: value interface integer function operation(argument) integer, intent(in) :: argument end function operation end interface apply_to = operation(value) end function apply_to end program apply_both
: DOUBLE ( n -- n ) 2 * ; : TRIPLE ( n -- n ) 3 * ; : APPLY-TO ( n xt -- n ) EXECUTE ; 5 ' DOUBLE APPLY-TO . 5 ' TRIPLE APPLY-TO . CR
Twenty lines against five, and the twenty buy a compile-time guarantee that the procedure takes one integer and returns one. A token that was never a token executes anyway and does something undefined.
Choosing The Procedure Later
DEFER creates a word whose behavior is chosen later, and IS installs an execution token into it. Every caller already compiled against EMIT follows the new one, with no recompilation.
program retarget implicit none call emit_serial call emit_silent contains subroutine emit_serial print "(a)", "serial" end subroutine emit_serial subroutine emit_silent print "(a)", "silent" end subroutine emit_silent end program retarget
: SERIAL ." serial" CR ; : SILENT ." silent" CR ; DEFER EMIT ' SERIAL IS EMIT EMIT ' SILENT IS EMIT EMIT
Fortran's nearest equivalent is a procedure pointer, which needs a declared interface and an explicit type. This is how a Forth driver is retargeted on a running instrument — the capability the telescope job was really about.
Memory You Address Yourself
ALLOCATABLE Versus ALLOT
Fortran can size an array at run time and free it afterwards. ALLOT reserves bytes at the moment the source is read, so the size is fixed when the word is defined and nothing is ever freed.
program allocate_demo implicit none integer, dimension(:), allocatable :: buffer allocate(buffer(4)) buffer = 7 print "(i0,1x,i0)", buffer(1), buffer(4) deallocate(buffer) end program allocate_demo
CREATE BUFFER 4 CELLS ALLOT : FILL-BUFFER ( value -- ) 4 0 DO DUP I CELLS BUFFER + ! LOOP DROP ; 7 FILL-BUFFER BUFFER @ . BUFFER 3 CELLS + @ . CR
There is no allocator to fail, fragment, or need a heap size chosen up front — memory use is decided when the program is loaded. That is a real property for an instrument that must not stop, and a real limitation for anything whose size depends on the data.
Reading And Writing A Cell
Fortran's pointers need target on the thing pointed at and => to associate them. VARIABLE reserves one cell and defines a word that pushes its address; ! stores and @ fetches.
program store_demo implicit none integer, target :: slot integer, pointer :: reference slot = 0 reference => slot reference = 42 print "(i0)", slot end program store_demo
VARIABLE SLOT 42 SLOT ! SLOT @ . CR
The two symbols read as "store" and "fetch", and getting their argument order backwards writes to the value instead of the address. Fortran's pointer is checked — it knows what it may point at and whether it is associated — while an address here is a number like any other.
A Derived Type Is An Offset Table
A Fortran derived type declares its components and the compiler chooses the layout. In Forth the layout is the definition: >X and >Y are words that adjust an address, and the record is the convention that the two cells belong together.
program point_demo implicit none type :: point_type integer :: x integer :: y end type point_type type(point_type) :: point point = point_type(3, 4) print "(i0,1x,i0)", point%x, point%y end program point_demo
CREATE POINT 2 CELLS ALLOT : >X ( addr -- addr ) ; : >Y ( addr -- addr ) CELL+ ; 3 POINT >X ! 4 POINT >Y ! POINT >X @ . POINT >Y @ . CR
Nothing records that these two words address one object, and nothing stops another word from writing something unrelated through >Y. What Fortran calls a component reference is here an addition that has been given a name.
Errors Without iostat
iostat Versus A Flag
Fortran's iostat= turns a failure into a status code instead of stopping the program. >NUMBER is the interpreter's own digit accumulator: it returns whatever it could not convert, and a leftover of zero means it consumed everything.
program read_number implicit none integer :: value, status character(len=4) :: text text = "1234" read(text, *, iostat=status) value if (status == 0) then print "(i0)", value + 1 else print "(a)", "could not parse" end if end program read_number
: PARSED ( -- n true | false ) 0 0 S" 1234" >NUMBER NIP 0= IF DROP TRUE ELSE 2DROP FALSE THEN ; : REPORT PARSED IF 1 + . ELSE ." could not parse" THEN CR ; REPORT
Both are the same design — report failure in a value the caller must look at. The difference is that Fortran's read without iostat halts the program, while >NUMBER has no halting mode at all and always returns.
STOP Versus ABORT"
Fortran's stop ends the program with an optional message, and error stop sets a failing exit status. ABORT" throws away both stacks and stops with a message, which is the nearest equivalent.
program checked_value implicit none integer :: reading reading = 12 if (reading > 25) then print "(a)", "out of range" else print "(i0)", reading end if end program checked_value
: CHECKED ( n -- n ) DUP 25 > ABORT" out of range" ; : SAFE ( n -- ) DUP 25 > IF ." out of range" ELSE DUP . THEN DROP CR ; 12 SAFE
CHECKED is defined to show the shape but is not run, because 30 CHECKED would end the program and nothing after it would print. This Forth has no CATCH, so there is no way for a caller to survive an abort — which is why a Forth library returns a flag instead.
The Preprocessor Versus IMMEDIATE
PARAMETER Versus CONSTANT
A Fortran parameter is evaluated at compile time, and what may appear in that expression is restricted. CONSTANT consumes whatever is on the stack and defines a word that pushes it back — and the line runs as the source is read.
program window_size implicit none integer, parameter :: rate = 48000 integer, parameter :: milliseconds = 20 integer, parameter :: window = rate * milliseconds / 1000 print "(i0)", window end program window_size
: SAMPLES-PER-WINDOW ( rate ms -- samples ) * 1000 / ; 48000 20 SAMPLES-PER-WINDOW CONSTANT WINDOW WINDOW . CR
Because the value is computed by ordinary Forth, the expression can be anything a word can do, including reading a file if one were available. There is no separate constant-expression language with its own rules, because the same interpreter is running either way.
The Preprocessor Versus IMMEDIATE
An IMMEDIATE word runs while the word containing it is being compiled, so it can emit whatever it likes into the definition. POSTPONE LITERAL is how it plants a value there, since a bare number is not a word that could be postponed.
program macro_free implicit none ! Fortran's preprocessor is cpp: a separate text-substitution pass that ! knows nothing about Fortran and runs before the compiler sees the file. integer, parameter :: half_the_answer = 21 print "(i0)", half_the_answer * 2 end program macro_free
: HALF-THE-ANSWER 21 POSTPONE LITERAL ; IMMEDIATE : ANSWER HALF-THE-ANSWER 2 * ; ANSWER . CR
Fortran's metaprogramming is cpp, a text substitution pass that knows nothing about the language it is preprocessing. A Forth macro is a word, running on the same stack, able to read ahead in the source and add control structures to the compiler — the same material as everything else.
Gotchas For Fortran Developers
Case Matters Here
🚨 Fortran is case-insensitive: VALUE and value are the same name, which is why the anchor column compiles. This Forth is case-sensitive and its built-in words are capitals — CR is a word and cr is "undefined word".
program case_demo implicit none integer :: value VALUE = 42 print "(i0)", value end program case_demo
: SHOUT ." forty-two" CR ; SHOUT 42 . CR
This is the difference most likely to stop a Fortran reader on their first line. Your own definitions keep whatever case you gave them, so : Greet does not define GREET — and most desktop Forths fold case, so an example copied from a book may work there and fail here.
Arguments Arrive In Stack Order
Fortran names the arguments, so their order in the declaration is the order at the call site and nothing else depends on it. In Forth the arguments are pushed left to right and the last one pushed is on top.
program subtract_demo implicit none print "(i0)", subtract(10, 3) contains integer function subtract(left, right) integer, intent(in) :: left, right subtract = left - right end function subtract end program subtract_demo
: SUBTRACT ( left right -- difference ) - ; 10 3 SUBTRACT . CR
That is why - subtracts the top of the stack from the cell beneath it — it is the spelling that makes 10 3 - mean what a reader expects. Getting the order wrong produces a perfectly valid program that computes the wrong thing, with no keyword arguments to fall back on.
Nothing Checks A Bound
Fortran can be compiled with -fcheck=bounds, which turns an out-of-range subscript into a diagnostic. Forth has no bound to check against: 3 READING is a valid address, and it is whatever was defined next.
program bounds_demo implicit none integer, dimension(3) :: readings readings = [10, 20, 30] ! readings(4) would be caught by gfortran -fcheck=bounds print "(i0)", readings(3) end program bounds_demo
CREATE READINGS 10 , 20 , 30 , 999 , : READING ( index -- addr ) CELLS READINGS + ; 2 READING @ . CR 3 READING @ . CR
🚨 The second line prints 999 — the next cell laid down, which belongs to nothing at all. Nothing is corrupted and nothing is reported; the program simply reads something that was never an element, which is the failure mode a bounds check exists to prevent. A bare 999 , is used rather than a second CREATE because CREATE would lay a dictionary header in between, and index 3 would land in the middle of it.
Nobody Is Counting
Fortran checks the argument count at compile time and names the missing one. The 99 here is left over from earlier work, the way a cell often is, and 5 TAKES-TWO supplies one argument and takes the 99 as the other.
program arity_demo implicit none print "(i0)", takes_two(1, 2) ! takes_two(1) is rejected: "Missing actual argument" contains integer function takes_two(first, second) integer, intent(in) :: first, second takes_two = first + second end function takes_two end program arity_demo
: TAKES-TWO ( a b -- sum ) + ; 99 1 2 TAKES-TWO . CR 5 TAKES-TWO . CR
It answers 104 without complaint. Checking the stack with .S after any word you are unsure of is the habit this produces, because a wrong answer is the only symptom — and on a program of any size that is a very long way from the mistake.
Redefining Does Not Reach Back
Fortran has no redefinition at all — two procedures need two names, which is why the anchor column has both spelled out. Defining SCALE a second time adds a new dictionary entry and hides the old one from anything compiled afterwards.
program scale_demo implicit none print "(i0)", apply_scale(10) print "(i0,1x,i0)", apply_scale(10), scale_by_hundred(10) contains integer function scale_by_two(value) integer, intent(in) :: value scale_by_two = value * 2 end function scale_by_two integer function scale_by_hundred(value) integer, intent(in) :: value scale_by_hundred = value * 100 end function scale_by_hundred integer function apply_scale(value) integer, intent(in) :: value apply_scale = scale_by_two(value) end function apply_scale end program scale_demo
: SCALE ( n -- n ) 2 * ; : APPLY-SCALE ( n -- n ) SCALE ; 10 APPLY-SCALE . CR : SCALE ( n -- n ) 100 * ; 10 APPLY-SCALE . 10 SCALE . CR
APPLY-SCALE was compiled earlier and still calls the first SCALE, so the two columns print the same numbers by quite different means. It also means fixing a bug in a low-level word does not fix its callers until they are recompiled.

Thank you — anything else?