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_number6 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_pair3 4
. . CRThe 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_sourceVARIABLE AZIMUTH
0 AZIMUTH !
: STEP-ON ( -- ) AZIMUTH @ 15 + DUP AZIMUTH ! . CR ;
: TRACK ( n -- ) 0 ?DO STEP-ON LOOP ;
4 TRACKThe 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-SOURCEA 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 . CRThe
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
. . CRThe 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 . CRThe
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_caughtVARIABLE COUNTER
1 COUNTER !
COUNTER @ . CR
.( COUNTR would be "undefined word", not a new variable ) CRThis 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_readingsCREATE READINGS 10 , 20 , 30 ,
3 CONSTANT #READINGS
: SUM-READINGS ( -- total )
0 #READINGS 0 ?DO READINGS I CELLS + @ + LOOP ;
SUM-READINGS . CRFortran 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_twoCREATE READINGS 10 , 20 , 30 ,
: READING ( index -- addr ) CELLS READINGS + ;
0 READING @ . 1 READING @ . CRBoth 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_sliceCREATE 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 . CRArray 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_grid3 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_sharedVARIABLE AZIMUTH
0 AZIMUTH !
: BUMP ( -- ) AZIMUTH @ 15 + AZIMUTH ! ;
BUMP BUMP
AZIMUTH @ . CREvery 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 . CRNaming 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_kinds32767 . 2147483647 . CR
1 CELLS 8 * . CRA 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 CRThe
<# … #> 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_readingsCREATE READINGS 3 , 8 , 15 , 4 ,
: SCAN ( -- )
4 0 DO
READINGS I CELLS + @
DUP 10 > IF DROP LEAVE THEN
.
LOOP CR ;
SCANThe
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-ALLThat 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 CRDefining 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_textCREATE 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 CRDoing 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 . CRTwenty 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
EMITFortran'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_demoCREATE BUFFER 4 CELLS ALLOT
: FILL-BUFFER ( value -- )
4 0 DO DUP I CELLS BUFFER + ! LOOP DROP ;
7 FILL-BUFFER
BUFFER @ . BUFFER 3 CELLS + @ . CRThere 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_demoVARIABLE SLOT
42 SLOT !
SLOT @ . CRThe 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_demoCREATE POINT 2 CELLS ALLOT
: >X ( addr -- addr ) ;
: >Y ( addr -- addr ) CELL+ ;
3 POINT >X ! 4 POINT >Y !
POINT >X @ . POINT >Y @ . CRNothing 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 ;
REPORTBoth 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 SAFECHECKED 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 . CRBecause 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 . CRFortran'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 . CRThis 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 . CRThat 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_demoCREATE 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 . CRIt 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 . CRAPPLY-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.