Bootstrapping WEB and compiling TeX using Free Pascal
April 2024
Introduction
TeX and , two of the many computer programs published by Donald E. Knuth (1984), were written in the Pascal programming language. As we can see in their source code, Knuth originally developed his software stack on a PDP-10 mainframe computer using a compiler he calls Pascal-H, which—apparently—implemented the standard of the language as described by Jensen and Wirth (1974) (later to become the ISO 7185 standard). The author found himself in need of compiling both programs from source on one of his platforms and decided to bootstrap the WEB system (Knuth 1989) used as the preprocessor for the TeX and source files. This article shows how to reproduce this process and obtain tex.pas and mf.pas Pascal source files and how to compile them using a modern Pascal distribution. The author has chosen the Free Pascal compiler (Van Canneyt and Klämpfl 2021), as it is available on multiple platforms and can target multiple platforms, including the ones the author is interested in.
The described techniques can also be used to compile both programs using an entirely different compiler, provided it supports either the ISO standard and the required extensions or provides its own replacements for the unsupported parts and system-dependent interfaces.
Getting started
The only prerequisites for the reader are the Free Pascal compiler (version 3.2.2, other versions may or may not work), the official TeX/ source distribution and the tangle.pas file (seal).
The WEB system allows us to supply a change file for every source file with necessary changes needed for our particular Pascal installation (cf. patches generated by diff(1)). This way, the original source file is kept unchanged. The change files should be, by convention, named after the desired program with a .ch file extension and contain the necessarry changes. The first part of this article will let us prepare a local self-hosting WEB system instance which we will later use to compile TeX and .
Since the TANGLE program has to be used to process the TANGLE program’s source code, an already processed file is provided above. It should compile on every platform supported by the Free Pascal compiler, as it does not contain any architecture-dependent or operating system-dependent changes. The reader should supply the -Miso switch to the compiler to force it to use the ISO standard dialect of the language.
TANGLE and WEAVE
As mentioned above, we will start with bootstrapping the WEB system, particularly the TANGLE program.
The first needed change is something that will required by most programs. Back when TeX was written, there was no standard way to define the default case for the case statement. Apparently complier used by Knuth defined the keyword others: for this purpose, which is different from the keyword else used by the Free Pascal compiler. Fortunately, we do not have to change its every occurence, because it is declared as othercases in the WEB file. We start our change file with the following lines.
@x @d othercases == others: {default for cases not listed explicitly} @y @d othercases == else {default for cases not listed explicitly} @z
The content between @x and @y will be replaced by the content between @y and @z.
Next, we can adjust some of the parameters used by TANGLE. The Free Pascal compiler supports lines up to 255 characters and identiiers up to 127 characters, so we can reflect this fact in the definitions.
@x @!line_length=72; {lines of \PASCAL\ output have at most this many characters} @!out_buf_size=144; {length of output buffer, should be twice |line_length|} @!stack_size=50; {number of simultaneous levels of macro expansion} @!max_id_length=12; {long identifiers are chopped to this length, which must not exceed |line_length|} @!unambig_length=7; {identifiers must be unique if chopped to this length} {note that 7 is more strict than \PASCAL's 8, but this can be varied} @y @!line_length=255; {lines of \PASCAL\ output have at most this many characters} @!out_buf_size=510; {length of output buffer, should be twice |line_length|} @!stack_size=50; {number of simultaneous levels of macro expansion} @!max_id_length=127; {long identifiers are chopped to this length, which must not exceed |line_length|} @!unambig_length=127; {identifiers must be unique if chopped to this length} {note that 7 is more strict than \PASCAL's 8, but this can be varied} @z
Later in the source file we find a definition of the text_file type. Our Pascal implementation already defines this type, so we simply remove it.
@x @d last_text_char=255 {ordinal number of the largest element of |text_char|} @<Types...@>= @!text_file=packed file of text_char; @y @d last_text_char=255 {ordinal number of the largest element of |text_char|} @z
Knuth usually represents the terminal output as the term_out variable. We can simply use the standard output as the sink for the diagnostic and progress messages, but the following change should be adjusted to adhere to local system conventions. Free Pascal always lets us use the output variable for this purpose.
@x end @<Globals...@>= @!term_out:text_file; {the terminal as an output file} @ Different systems have different ways of specifying that the output on a certain file will appear on the user's terminal. Here is one way to do this on the \PASCAL\ system that was used in \.{TANGLE}'s initial development: @^system dependencies@> @<Set init...@>= rewrite(term_out,'TTY:'); {send |term_out| output to the terminal} @y end @d term_out == o@&u@&t@&p@&u@&t {standard output} @z
(We had to use the WEB coding trick in this case, as described by Knuth in the TeX source file.)
The last needed change is th terminal output flush routine.
@x @d update_terminal == break(term_out) {empty the terminal output buffer} @y @d update_terminal == flush(term_out) {empty the terminal output buffer} @z
All of the changes for TANGLE (aside from the constant redefinitions) are also applicable for WEAVE, so we can also create a similar weave.ch file.
We can now bootstrap TANGLE with the following command line.
tangle tangle.web tangle.ch tangle.pas
The resulting Pascal source file should be virtually identical to the one provided with this article, since the described changes are exactly the same the author has used to generate it.
Generating a source file for WEAVE is obviously similar.
tangle weave.web weave.ch weave.pas
Compiling WEAVE also requires us to pass the -Miso switch to the compiler. After complinig it, we can use WEAVE in pretty much the same way as TANGLE.
weave weave.web weave.ch weave.tex
We are now in a position where we can generate Pascal and TeX source files for every WEB file in the TeX source distribution.
TeX
TeX is way more complicated than the preceding two programs, but it is possible to properly compile it against the Free Pascal runtime library. This time, we will not be using the -Miso switch, so our changes will have to reflect it.
First of all, we are going to be using the Free Pascal units system, so we need to include it in the program header.
@x @f type==true {but `|type|' will not be treated as a reserved word} @p @t\4@>@<Compiler directives@>@/ program TEX; {all file names are defined dynamically} @y @f type==true {but `|type|' will not be treated as a reserved word} @d uses==u@&s@&e@&s @f uses==label @p @t\4@>@<Compiler directives@>@/ program TEX; {all file names are defined dynamically} uses @<Units in the outer block@>@/ @z
We also make it so that our newly defined keyword is properly formatted in the source documentation. The Units in the outer block are going to expand to the section we will define at the end of the file where system-dependent sections are expected to be located.
The following section is only needed for the production version of TeX. It is going to be necessary after we obtain the Plain TeX format file. Reader discretion is advised.
@x @d init== {change this to `$\\{init}\equiv\.{@@\{}$' in the production version} @d tini== {change this to `$\\{tini}\equiv\.{@@\}}$' in the production version} @y @d init==@{ @d tini==@} @z
Free Pascal compiler directives are incompatible with the Pascal-H compiler, so we have to redifine them. For now, we need to ignore runtime errors when we open non-existent files, since TeX has its own way of dealing with it. Omitting the -Miso switch made us lose the standard I/O interface, but fortunately we can force it to appear. There is also a single non-local goto, so we make it possible to compile such a potentially disastrous entity.
@x @{@&$C-,A+,D-@} {no range check, catch arithmetic overflow, no debug overhead} @!debug @{@&$C+,D+@}@+ gubed {but turn everything on when debugging} @y @{$i-@} {no file check} @{$mode_switch iso_io@} {for file buffers} @{$mode_switch non_local_goto@} {self-explanatory} @z
As with the other programs, we need a default case keyword.
@x @d othercases == others: {default for cases not listed explicitly} @y @d othercases == e@&l@&s@&e {default for cases not listed explicitly} @z
Fine-tuning TeX when it comes to memory allocation requires some insight, so properly configuring TeX (and the INITEX program) is left as an excercise for the reader. Refering to documentation (i.e., the WEB file, possibly after feeding WEAVE with it) will be necessary to understand how TeX allocates memory. For now, an example change is illustrated below.
@x @!mem_max=30000; {greatest index in \TeX's internal |mem| array; @y @!mem_max=65534; {greatest index in \TeX's internal |mem| array; @z
Some other constants we will probably need to change appear a few lines later. The changes should obviously reflect the limits and the file system hierarchy of the target operating system. The following example reflects the configuration of one of the author’s environments.
@x @!file_name_size=40; {file names shouldn't be longer than this} @!pool_name='TeXformats:TEX.POOL '; {string of length |file_name_size|; tells where the string pool appears} @y @!file_name_size=255; {file names shouldn't be longer than this} @!pool_name='/app/dek/tex-3.141592653/formats/tex.pool'; {tells where the string pool appears} @z
The alert reader will surely notice the original pool_name variable contained trailing spaces. The comment immediately after it even mentions what the length of the string should apparently be. The trailing spaces should not pose a problem for some operating systems (like DOS, etc.), but other systems might not be so forgiving, so we do not have to abide by this rule here. Case-sensitivity is also important at this point (depending at the operating system’s indifference to it), so proper care should be taken when handling the pool file (and other files as well). The file I/O routines will later be modified to allow us to have variable-length paths to files, so this approach will be compatible with both kinds of operating systems. One thing to notice is the pool file is only read by the INITEX and later included in the format file, so it does not have to permanently reside in the defined location. The formats directory is simply chosen to maintain compatibility with the original definition.
There is one more change needed to the memory constants. As before, the user should refer to the documentation to properly compile INITEX and their production TeX program.
@x @d mem_top==30000 {largest index in the |mem| array dumped by \.{INITEX}; @y @d mem_top==65534 {largest index in the |mem| array dumped by \.{INITEX}; @z
We redefine the alpha_file type with the type provided by the Free Pascal runtime.
@x @!alpha_file=packed file of text_char; {files that contain textual data} @y @!alpha_file=text_file; {files that contain textual data} @z
Since we are changing the way file paths are handled, we need to change the type of the name_of_file variable. We can safely use a variable-length string, since we do not have to constrain ourselves to the ISO standard.
@x @!name_of_file:packed array[1..file_name_size] of char;@;@/ {on some systems this may be a \&{record} variable} @y @!name_of_file:string[file_name_size];@; @z
TeX uses a lot of system-dependent I/O routines, so we basically have to redefine all of them. We also change the definitions of the reset_OK and rewrite_OK macros, since our runtime does not include the erstat function, but we can replace it with the io_result variable.
@x @d reset_OK(#)==erstat(#)=0 @d rewrite_OK(#)==erstat(#)=0 @p function a_open_in(var f:alpha_file):boolean; {open a text file for input} begin reset(f,name_of_file,'/O'); a_open_in:=reset_OK(f); end; @# function a_open_out(var f:alpha_file):boolean; {open a text file for output} begin rewrite(f,name_of_file,'/O'); a_open_out:=rewrite_OK(f); end; @# function b_open_in(var f:byte_file):boolean; {open a binary file for input} begin reset(f,name_of_file,'/O'); b_open_in:=reset_OK(f); end; @# function b_open_out(var f:byte_file):boolean; {open a binary file for output} begin rewrite(f,name_of_file,'/O'); b_open_out:=rewrite_OK(f); end; @# function w_open_in(var f:word_file):boolean; {open a word file for input} begin reset(f,name_of_file,'/O'); w_open_in:=reset_OK(f); end; @# function w_open_out(var f:word_file):boolean; {open a word file for output} begin rewrite(f,name_of_file,'/O'); w_open_out:=rewrite_OK(f); end; @y @d reset_OK(#)==io_result=0 @d rewrite_OK(#)==io_result=0 @p function a_open_in(var f:alpha_file):boolean; {open a text file for input} begin assign(f,name_of_file); reset(f); a_open_in:=reset_OK(f); end; @# function a_open_out(var f:alpha_file):boolean; {open a text file for output} begin assign(f,name_of_file); rewrite(f); a_open_out:=rewrite_OK(f); end; @# function b_open_in(var f:byte_file):boolean; {open a binary file for input} begin assign(f,name_of_file); reset(f); b_open_in:=reset_OK(f); end; @# function b_open_out(var f:byte_file):boolean; {open a binary file for output} begin assign(f,name_of_file); rewrite(f); b_open_out:=rewrite_OK(f); end; @# function w_open_in(var f:word_file):boolean; {open a word file for input} begin assign(f,name_of_file); reset(f); w_open_in:=reset_OK(f); end; @# function w_open_out(var f:word_file):boolean; {open a word file for output} begin assign(f,name_of_file); rewrite(f); w_open_out:=rewrite_OK(f); end; @z
The next change is the first of probably the two most complicates ones. The input_ln function uses (as described in the documentation) only the standard routines for input, but it operates on the ISO file buffers and does not properly handle various system-dependent end-of-line markers. It also gets characters one by one instead of an entire line. The documentation says “finer tuning is often possible at well-developed Pascal sites” and our Free Pascal setup can probably be considered one, so the following lines show a simple change we can do to read an entire line with a single read_ln.
@x @p function input_ln(var f:alpha_file;@!bypass_eoln:boolean):boolean; {inputs the next line or returns |false|} var last_nonblank:0..buf_size; {|last| with trailing blanks removed} begin if bypass_eoln then if not eof(f) then get(f); {input the first character of the line into |f^|} last:=first; {cf.\ Matthew 19\thinspace:\thinspace30} if eof(f) then input_ln:=false else begin last_nonblank:=first; while not eoln(f) do begin if last>=max_buf_stack then begin max_buf_stack:=last+1; if max_buf_stack=buf_size then @<Report overflow of the input buffer, and abort@>; end; buffer[last]:=xord[f^]; get(f); incr(last); if buffer[last-1]<>" " then last_nonblank:=last; end; last:=last_nonblank; input_ln:=true; end; end; @y @p function input_ln(var f:alpha_file;@!bypass_eoln:boolean):boolean; {inputs the next line or returns |false|} var last_nonblank:0..buf_size; {|last| with trailing blanks removed} line:string; k:long_int; begin last:=first; {cf.\ Matthew 19\thinspace:\thinspace30} if eof(f) then input_ln:=false else begin read_ln(f,line); last_nonblank:=first; for k:=1 to long_int(line[0]) do begin if last>=max_buf_stack then begin max_buf_stack:=last+1; if max_buf_stack=buf_size then @<Report overflow of the input buffer, and abort@>; end; buffer[last]:=xord[line[k]]; incr(last); if buffer[last-1]<>" " then last_nonblank:=last; end; last:=last_nonblank; input_ln:=true; end; end; @z
As with TANGLE and WEAVE, term_in and term_out can simply point to Free Pascal’s input and output variables.
@x @<Glob...@>= @!term_in:alpha_file; {the terminal as an input file} @!term_out:alpha_file; {the terminal as an output file} @y @d term_in == i@&n@&p@&u@&t {standard input} @d term_out == o@&u@&t@&p@&u@&t {standard output} @z
The standard input and output files are always open, so we can make the macros opening them do nothing.
@x @d t_open_in==reset(term_in,'TTY:','/O/I') {open the terminal for text input} @d t_open_out==rewrite(term_out,'TTY:','/O') {open the terminal for text output} @y @d t_open_in == do_nothing {open the terminal for text input} @d t_open_out == do_nothing {open the terminal for text output} @z
We also have to change the terminal handling routines.
@x @d update_terminal == break(term_out) {empty the terminal output buffer} @d clear_terminal == break_in(term_in,true) {clear the terminal input buffer} @y @d update_terminal == flush(term_out) {empty the terminal output buffer} @d clear_terminal == do_nothing {clear the terminal input buffer} @z
The following change is the second of probably the two most complicated ones. TeX itself does not have a way to deal with the external command line and always prompts the user for it, but we can use the Free Pascal’s param_str and param_count variables. Unfortunately, newer versions of Free Pascal do not allow us to get an entire “command tail,” so we will have to manually add each argument to the buffer. We will also have to use the length function to properly measure the arguments.
@x @p function init_terminal:boolean; {gets the terminal input started} label exit; begin t_open_in; loop@+begin wake_up_terminal; write(term_out,'**'); update_terminal; @.**@> if not input_ln(term_in,true) then {this shouldn't happen} begin write_ln(term_out); write(term_out,'! End of file on the terminal... why?'); @.End of file on the terminal@> init_terminal:=false; return; end; loc:=first; while (loc<last)and(buffer[loc]=" ") do incr(loc); if loc<last then begin init_terminal:=true; return; {return unless the line was all blank} end; write_ln(term_out,'Please type the name of your input file.'); end; exit:end; @y @p function init_terminal:boolean; {gets the terminal input started} label exit; var i,c:long_int; begin t_open_in; if param_count>0 then begin last:=first; for c:=1 to l@&e@&n@&g@&t@&h(param_str(1)) do begin buffer[last]:=ASCII_code(param_str(1)[c]); inc(last); end; if param_count>1 then for i:=2 to param_count do begin buffer[last]:=32; inc(last); for c:=1 to l@&e@&n@&g@&t@&h(param_str(i)) do begin buffer[last]:=ASCII_code(param_str(i)[c]); inc(last); end; end; loc:=first; init_terminal:=true; return; end; loop@+begin wake_up_terminal; write(term_out,'**'); update_terminal; @.**@> if not input_ln(term_in,true) then {this shouldn't happen} begin write_ln(term_out); write(term_out,'! End of file on the terminal... why?'); @.End of file on the terminal@> init_terminal:=false; return; end; loc:=first; while (loc<last)and(buffer[loc]=" ") do incr(loc); if loc<last then begin init_terminal:=true; return; {return unless the line was all blank} end; write_ln(term_out,'Please type the name of your input file.'); end; exit:end; @z
The above change lets us supply TeX with arguments from the command line, but it will still prompt us if we decide to run it without any arguments.
The following change supplies TeX with current date and time. The function and type we need are in the sys_utils unit, so we will have to include it later.
@x Since standard \PASCAL\ cannot provide such information, something special is needed. The program here simply assumes that suitable values appear in the global variables \\{sys\_time}, \\{sys\_day}, \\{sys\_month}, and \\{sys\_year} (which are initialized to noon on 4 July 1776, in case the implementor is careless). @p procedure fix_date_and_time; begin sys_time:=12*60; sys_day:=4; sys_month:=7; sys_year:=1776; {self-evident truths} time:=sys_time; {minutes since midnight} day:=sys_day; {day of the month} month:=sys_month; {month of the year} year:=sys_year; {Anno Domini} end; @y @p procedure fix_date_and_time; var st:t_system_time; begin date_time_to_system_time(now, st); sys_time:=st.hour*60+st.minute; sys_day:=st.d@&a@&y; sys_month:=st.m@&o@&n@&t@&h; sys_year:=st.y@&e@&a@&r; time:=sys_time; {minutes since midnight} day:=sys_day; {day of the month} month:=sys_month; {month of the year} year:=sys_year; {Anno Domini} end; @z
The following two changes provide TeX with default locations of input files and fonts. They obviously reflect the author’s file system, so the changes may be necessary (unless the reader uses the exact same scheme).
@x @d TEX_area=="TeXinputs:" @y @d TEX_area=="/app/dek/tex-3.141592653/inputs/" @z
@x @d TEX_font_area=="TeXfonts:" @y @d TEX_font_area=="/app/dek/tex-3.141592653/fonts/" @z
The next change is highly dependent on the way the target operating system handles file paths. TeX needs to know what the directory separator is to properly extract the file name from a full path. The default setting might be appropriate if we are going to run TeX on a Lisp machine, but most readers will probably have to redefine it to something else. An example change might look like the following one.
@x if (c=">")or(c=":") then @y if (c="/") then @z
A DOS target will require the following change.
@x if (c=">")or(c=":") then @y if (c="\")or(c=":") then @z
The condition should include all of the characters that can appear before the actual file name. Systems where the entire hierarchy is governed by a single separator are the easiest, but a system like a VMS using the ODS-2 syntax would probably require a condition like (c="]")or(c=">")or(c=":") (and a handler for the version suffix, since—later in that routine—TeX only checks for ., but—in this case—it would also have to check for ;). The reader should carefully examine the routine and make the requried modifications, so that the file names can be handled properly.
Since we are using variable-length strings, we will need to make a few more changes. TeX fills the entire file specification with white space by default and passes a string like that to the I/O routines (the same way it handles the pool file, which we changed before). Instead of clearing it, we can simply change the string length by assigning it to the zeroth element of the string (with a necessary cast).
@x for k:=name_length+1 to file_name_size do name_of_file[k]:=' '; @y name_of_file[0]:=c@&h@&a@&r(name_length); @z
TeX looks for the format files in a default area. All of the constants governing the parts of the specification (and the name of the default format file) are fortunately defined in one place. As always, the following change reflects the author’s file system and should be modified by the reader.
@x @d format_default_length=20 {length of the |TEX_format_default| string} @d format_area_length=11 {length of its area part} @d format_ext_length=4 {length of its `\.{.fmt}' part} @d format_extension=".fmt" {the extension, as a \.{WEB} constant} @<Glob...@>= @!TEX_format_default:packed array[1..format_default_length] of char; @ @<Set init...@>= TEX_format_default:='TeXformats:plain.fmt'; @y @d format_default_length=42 {length of the |TEX_format_default| string} @d format_area_length=33 {length of its area part} @d format_ext_length=4 {length of its `\.{.fmt}' part} @d format_extension=".fmt" {the extension, as a \.{WEB} constant} @<Glob...@>= @!TEX_format_default:packed array[1..format_default_length] of char; @ @<Set init...@>= TEX_format_default:='/app/dek/tex-3.141592653/formats/plain.fmt'; @z
Another change is required later to properly handle the variable-length strings, but it is the exact same we already did.
@x for k:=name_length+1 to file_name_size do name_of_file[k]:=' '; @y name_of_file[0]:=c@&h@&a@&r(name_length); @z
Free Pascal allows us to set a break handler (in case we want to interrupt our program), but it has to be set up before we want to use it, so we use this chance to add the required procedure among the other initializations.
@x history:=fatal_error_stop; {in case we quit during initialization} t_open_out; {open the terminal for output} @y history:=fatal_error_stop; {in case we quit during initialization} sys_set_ctrl_break_handler(@@handle_ctrl_break);@/ t_open_out; {open the terminal for output} @z
At last we arrive at the point where we can define all our changes we made references to. We have to define the unit for our date and time handling, but we also need to make a reference to the iso_7185 unit, since it contains the I/O routines that will not be included by default (since this time we are omitting the -Miso switch). Another change is our break handler, which will turn the interrupt knob on.
@x This section should be replaced, if necessary, by any special modifications of the program that are necessary to make \TeX\ work at a particular installation. It is usually best to design your change file so that all changes to previous sections preserve the section numbering; then everybody's version will be consistent with the published program. More extensive changes, which introduce new sections, can be inserted here; then only the index itself will get a new section number. @y @ @<Units in the outer block@>= iso_7185, sys_utils; {needed for ISO input/output and date/time routines} @ @<Last-minute...@>= function handle_ctrl_break(ctrl_break:boolean):boolean; begin interrupt:=1; handle_ctrl_break:=true; end; @z
The above changes should be saved as tex.ch (or possibly initex.ch). During initialization, TeX requires a pool file, so we should generate one with TANGLE. We can simply pass it as an additional argument.
tangle tex.web tex.ch tex.pas tex.pool
The pool file should then be moved to the location declared in the change file. As mentioned before, TeX should be compiled without the -Miso switch.
Unfortunately, all of the work we just did will not allow us to set up the TeX environment from scratch yet. It turns out the Plain TeX format requires the font metric files, which are not included in the source distribution. Not only that, we are still missing an important one important program, the one that will—coincidentally—generate the font metric files for us.
METAFONT
A lot of the changes we just did are compatible with , but there are some differences.
The following change resembles its TeX counterpart.
@x @f type==true {but `|type|' will not be treated as a reserved word} @p @t\4@>@<Compiler directives@>@/ program MF; {all file names are defined dynamically} @y @f type==true {but `|type|' will not be treated as a reserved word} @d uses==u@&s@&e@&s @f uses==label @p @t\4@>@<Compiler directives@>@/ program MF; {all file names are defined dynamically} uses @<Units in the outer block@>@/ @z
The reader should, as before, bear in mind the consequences of the following change.
@x @d init== {change this to `$\\{init}\equiv\.{@@\{}$' in the production version} @d tini== {change this to `$\\{tini}\equiv\.{@@\}}$' in the production version} @y @d init==@{ @d tini==@t@>@} @z
The next following changes should feel familiar.
@x @{@&$C-,A+,D-@} {no range check, catch arithmetic overflow, no debug overhead} @!debug @{@&$C+,D+@}@+ gubed {but turn everything on when debugging} @y @{$i-@} {no file check} @{$mode_switch iso_io@} {for file buffers} @{$mode_switch non_local_goto@} {self-explanatory} @z
@x @d othercases == others: {default for cases not listed explicitly} @y @d othercases == e@&l@&s@&e {default for cases not listed explicitly} @z
@x @!mem_max=30000; {greatest index in \MF's internal |mem| array; @y @!mem_max=65534; {greatest index in \MF's internal |mem| array; @z
provides an online display with the showit command.We can try to make all of the display routines use the graph unit included in the Free Pascal runtime, but the reader should properly read the documentation to make sure their operating system and display method are supported. All of the changes regarding the online display are completely optional and the reader can safely omit them from inclusion in their change file.
The following constants govern the screen size.
@x @!screen_width=768; {number of pixels in each row of screen display} @!screen_depth=1024; {number of pixels in each column of screen display} @y @!screen_width=1024; {number of pixels in each row of screen display} @!screen_depth=768; {number of pixels in each column of screen display} @z
As before, pool file name reflects the author’s setup. The pool file resides in the bases directory for the sole reason of compatibility with the original source.
@x @!file_name_size=40; {file names shouldn't be longer than this} @!pool_name='MFbases:MF.POOL '; @y @!file_name_size=255; {file names shouldn't be longer than this} @!pool_name='/app/dek/mf-2.71828182/bases/mf.pool'; @z
@x @!alpha_file=packed file of text_char; {files that contain textual data} @y @!alpha_file=text_file; {files that contain textual data} @z
@x @!name_of_file:packed array[1..file_name_size] of char;@;@/ {on some systems this may be a \&{record} variable} @y @!name_of_file:string[file_name_size];@; @z
@x @d reset_OK(#)==erstat(#)=0 @d rewrite_OK(#)==erstat(#)=0 @p function a_open_in(var @!f:alpha_file):boolean; {open a text file for input} begin reset(f,name_of_file,'/O'); a_open_in:=reset_OK(f); end; @# function a_open_out(var @!f:alpha_file):boolean; {open a text file for output} begin rewrite(f,name_of_file,'/O'); a_open_out:=rewrite_OK(f); end; @# function b_open_out(var @!f:byte_file):boolean; {open a binary file for output} begin rewrite(f,name_of_file,'/O'); b_open_out:=rewrite_OK(f); end; @# function w_open_in(var @!f:word_file):boolean; {open a word file for input} begin reset(f,name_of_file,'/O'); w_open_in:=reset_OK(f); end; @# function w_open_out(var @!f:word_file):boolean; {open a word file for output} begin rewrite(f,name_of_file,'/O'); w_open_out:=rewrite_OK(f); end; @y @d reset_OK(#)==io_result=0 @d rewrite_OK(#)==io_result=0 @p function a_open_in(var @!f:alpha_file):boolean; {open a text file for input} begin assign(f,name_of_file); reset(f); a_open_in:=reset_OK(f); end; @# function a_open_out(var @!f:alpha_file):boolean; {open a text file for output} begin assign(f,name_of_file); rewrite(f); a_open_out:=rewrite_OK(f); end; @# function b_open_out(var @!f:byte_file):boolean; {open a binary file for output} begin assign(f,name_of_file); rewrite(f); b_open_out:=rewrite_OK(f); end; @# function w_open_in(var @!f:word_file):boolean; {open a word file for input} begin assign(f,name_of_file); reset(f); w_open_in:=reset_OK(f); end; @# function w_open_out(var @!f:word_file):boolean; {open a word file for output} begin assign(f,name_of_file); rewrite(f); w_open_out:=rewrite_OK(f); end; @z
@x @p function input_ln(var @!f:alpha_file;@!bypass_eoln:boolean):boolean; {inputs the next line or returns |false|} var @!last_nonblank:0..buf_size; {|last| with trailing blanks removed} begin if bypass_eoln then if not eof(f) then get(f); {input the first character of the line into |f^|} last:=first; {cf.\ Matthew 19\thinspace:\thinspace30} if eof(f) then input_ln:=false else begin last_nonblank:=first; while not eoln(f) do begin if last>=max_buf_stack then begin max_buf_stack:=last+1; if max_buf_stack=buf_size then @<Report overflow of the input buffer, and abort@>; end; buffer[last]:=xord[f^]; get(f); incr(last); if buffer[last-1]<>" " then last_nonblank:=last; end; last:=last_nonblank; input_ln:=true; end; end; @y @p function input_ln(var @!f:alpha_file;@!bypass_eoln:boolean):boolean; {inputs the next line or returns |false|} var @!last_nonblank:0..buf_size; {|last| with trailing blanks removed} line:string; k:long_int; begin last:=first; {cf.\ Matthew 19\thinspace:\thinspace30} if eof(f) then input_ln:=false else begin read_ln(f,line); last_nonblank:=first; for k:=1 to long_int(line[0]) do begin if last>=max_buf_stack then begin max_buf_stack:=last+1; if max_buf_stack=buf_size then @<Report overflow of the input buffer, and abort@>; end; buffer[last]:=xord[line[k]]; incr(last); if buffer[last-1]<>" " then last_nonblank:=last; end; last:=last_nonblank; input_ln:=true; end; end; @z
@x @<Glob...@>= @!term_in:alpha_file; {the terminal as an input file} @!term_out:alpha_file; {the terminal as an output file} @y @d term_in == i@&n@&p@&u@&t {standard input} @d term_out == o@&u@&t@&p@&u@&t {standard output} @z
@x @d t_open_in==reset(term_in,'TTY:','/O/I') {open the terminal for text input} @d t_open_out==rewrite(term_out,'TTY:','/O') @y @d t_open_in==do_nothing {open the terminal for text input} @d t_open_out==do_nothing @z
@x @d update_terminal == break(term_out) {empty the terminal output buffer} @d clear_terminal == break_in(term_in,true) {clear the terminal input buffer} @d wake_up_terminal == do_nothing {cancel the user's cancellation of output} @y @d update_terminal == flush(term_out) {empty the terminal output buffer} @d clear_terminal == do_nothing {clear the terminal input buffer} @d wake_up_terminal == do_nothing {cancel the user's cancellation of output} @z
@x @p function init_terminal:boolean; {gets the terminal input started} label exit; begin t_open_in; loop@+begin wake_up_terminal; write(term_out,'**'); update_terminal; @.**@> if not input_ln(term_in,true) then {this shouldn't happen} begin write_ln(term_out); write(term_out,'! End of file on the terminal... why?'); @.End of file on the terminal@> init_terminal:=false; return; end; loc:=first; while (loc<last)and(buffer[loc]=" ") do incr(loc); if loc<last then begin init_terminal:=true; return; {return unless the line was all blank} end; write_ln(term_out,'Please type the name of your input file.'); end; exit:end; @y @p function init_terminal:boolean; {gets the terminal input started} label exit; var i,c:long_int; begin t_open_in; if param_count>0 then begin last:=first; for c:=1 to l@&e@&n@&g@&t@&h(param_str(1)) do begin buffer[last]:=ASCII_code(param_str(1)[c]); inc(last); end; if param_count>1 then for i:=2 to param_count do begin buffer[last]:=32; inc(last); for c:=1 to l@&e@&n@&g@&t@&h(param_str(i)) do begin buffer[last]:=ASCII_code(param_str(i)[c]); inc(last); end; end; loc:=first; init_terminal:=true; return; end; loop@+begin wake_up_terminal; write(term_out,'**'); update_terminal; @.**@> if not input_ln(term_in,true) then {this shouldn't happen} begin write_ln(term_out); write(term_out,'! End of file on the terminal... why?'); @.End of file on the terminal@> init_terminal:=false; return; end; loc:=first; while (loc<last)and(buffer[loc]=" ") do incr(loc); if loc<last then begin init_terminal:=true; return; {return unless the line was all blank} end; write_ln(term_out,'Please type the name of your input file.'); end; exit:end; @z
@x Since standard \PASCAL\ cannot provide such information, something special is needed. The program here simply assumes that suitable values appear in the global variables \\{sys\_time}, \\{sys\_day}, \\{sys\_month}, and \\{sys\_year} (which are initialized to noon on 4 July 1776, in case the implementor is careless). Note that the values are |scaled| integers. Hence \MF\ can no longer be used after the year 32767. @p procedure fix_date_and_time; begin sys_time:=12*60; sys_day:=4; sys_month:=7; sys_year:=1776; {self-evident truths} internal[time]:=sys_time*unity; {minutes since midnight} internal[day]:=sys_day*unity; {day of the month} internal[month]:=sys_month*unity; {month of the year} internal[year]:=sys_year*unity; {Anno Domini} end; @y Note that the values are |scaled| integers. Hence \MF\ can no longer be used after the year 32767. @p procedure fix_date_and_time; var st:t_system_time; begin date_time_to_system_time(now, st); sys_time:=st.hour*60+st.minute; sys_day:=st.d@&a@&y; sys_month:=st.m@&o@&n@&t@&h; sys_year:=st.y@&e@&a@&r; internal[time]:=sys_time*unity; {minutes since midnight} internal[day]:=sys_day*unity; {day of the month} internal[month]:=sys_month*unity; {month of the year} internal[year]:=sys_year*unity; {Anno Domini} end; @z
This is a new required change. Apparently, there are issues with overloaded operators in the Free Pascal runtime, so we need to explicitly cast the value to a type we want to use.
@x @d valid_range(#)==(abs(#-4096)<4096) {is |#| strictly between 0 and 8192?} @y @d valid_range(#)==(abs(long_int(#-4096))<4096) {is |#| strictly between 0 and 8192?} @z
The following three changes apply only to versions with online display.
Whenever screen needs initialization, calls init_screen.The values for the driver and mode should be adjusted to reflect the target operating system and/or display. The reader should be careful, because using Free Pascal’s graph unit on DOS (and possibly other systems) causes the graphics adapter to switch out of the text mode. It should not be a problem on a high-end DOS workstation with a separate physical text terminal for the CON device (as the I/O can continue while the preview of the work is truly online), but on most personal computers it will leave the user trapped in the display mode. A possible remedy would be to redefine the update_screen routine, so that it waits for a key press (using, e.g., the read_key routine provided in the crt unit) and calls close_graph afterwards. It might allow the user to be able to call showit, examine their work and go back to the interactive mode after a key press, but properly implementing it and solving the name collisions resulting from using the crt unit is left as an excercise for the reader (in case they really need such a configuration). If used on a platform such as Microsoft Windows, the graph unit can actually use a detection mechanism to allocate a rather large window—the reader should consult the documentation to see how to use the detection mechanism.
@x @p function init_screen:boolean; begin init_screen:=false; end; @y @p function init_screen:boolean; var gd,gm:small_int; begin gd:=d16bit; gm:=cga_hi; init_graph(gd,gm,''); init_screen:=true; end; @z
The next change implements a mechanism for drawing blank rectangles on the screen. The 65535 should be replaced with the value representing color white (or any other desired background color) in the mode actually in use.
@x @p procedure blank_rectangle(@!left_col,@!right_col:screen_col; @!top_row,@!bot_row:screen_row); var @!r:screen_row; @!c:screen_col; begin @{@+for r:=top_row to bot_row-1 do for c:=left_col to right_col-1 do screen_pixel[r,c]:=white;@+@}@/ @!init wlog_cr; {this will be done only after |init_screen=true|} wlog_ln('Calling BLANKRECTANGLE(',left_col:1,',', right_col:1,',',top_row:1,',',bot_row:1,')');@+tini end; @y @p procedure blank_rectangle(@!left_col,@!right_col:screen_col; @!top_row,@!bot_row:screen_row); var @!r:screen_row; @!c:screen_col; begin set_fill_style(solid_fill, 65535); bar(left_col, top_row, right_col-1, bot_row-1); @!init wlog_cr; {this will be done only after |init_screen=true|} wlog_ln('Calling BLANKRECTANGLE(',left_col:1,',', right_col:1,',',top_row:1,',',bot_row:1,')');@+tini end; @z
The following change implements the actual way to paint on the screen. The last argument to set_fill_style should be replaced with an expression evaluating to either black or white (or rather foreground and background) color depending on the value of b.
@x @p procedure paint_row(@!r:screen_row;@!b:pixel_color;var @!a:trans_spec; @!n:screen_col); var @!k:screen_col; {an index into |a|} @!c:screen_col; {an index into |screen_pixel|} begin @{@+k:=0; c:=a[0]; repeat incr(k); repeat screen_pixel[r,c]:=b; incr(c); until c=a[k]; b:=black-b; {$|black|\swap|white|$} until k=n;@+@}@/ @!init wlog('Calling PAINTROW(',r:1,',',b:1,';'); {this is done only after |init_screen=true|} for k:=0 to n do begin wlog(a[k]:1); if k<>n then wlog(','); end; wlog_ln(')');@+tini end; @y @p procedure paint_row(@!r:screen_row;@!b:pixel_color;var @!a:trans_spec; @!n:screen_col); var @!k:screen_col; {an index into |a|} @!c:screen_col; {an index into |screen_pixel|} begin k:=0; c:=a[0]; repeat incr(k); set_fill_style(solid_fill, (b xor 1)*65535); bar(r,c,r,a[k]-1); c:=a[k]; b:=black-b; {$|black|\swap|white|$} until k=n; @!init wlog('Calling PAINTROW(',r:1,',',b:1,';'); {this is done only after |init_screen=true|} for k:=0 to n do begin wlog(a[k]:1); if k<>n then wlog(','); end; wlog_ln(')');@+tini end; @z
The reader is welcome to use Free Pascal’s foreign function interface to write a more sophisticated mechanism for the online display. The peculiarities of various systems are beyond the scope of this article.
The following change the default location of the input files and is not unlike its TeX counterpart.
@x @d MF_area=="MFinputs:" @y @d MF_area=="/app/dek/mf-2.71828182/inputs/" @z
The remarks regarding the path specification separators apply to the following change as well.
@x else begin if (c=">")or(c=":") then @y else begin if (c="/") then @z
@x for k:=name_length+1 to file_name_size do name_of_file[k]:=' '; @y name_of_file[0]:=c@&h@&a@&r(name_length); @z
As before, the reader should replace the author’s directory with a desired default location for the format files.
@x @d base_default_length=18 {length of the |MF_base_default| string} @d base_area_length=8 {length of its area part} @d base_ext_length=5 {length of its `\.{.base}' part} @d base_extension=".base" {the extension, as a \.{WEB} constant} @<Glob...@>= @!MF_base_default:packed array[1..base_default_length] of char; @ @<Set init...@>= MF_base_default:='MFbases:plain.base'; @y @d base_default_length=39 {length of the |MF_base_default| string} @d base_area_length=29 {length of its area part} @d base_ext_length=5 {length of its `\.{.base}' part} @d base_extension=".base" {the extension, as a \.{WEB} constant} @<Glob...@>= @!MF_base_default:packed array[1..base_default_length] of char; @ @<Set init...@>= MF_base_default:='/app/dek/mf-2.71828182/bases/plain.base'; @z
@x for k:=name_length+1 to file_name_size do name_of_file[k]:=' '; @y name_of_file[0]:=c@&h@&a@&r(name_length); @z
@x history:=fatal_error_stop; {in case we quit during initialization} t_open_out; {open the terminal for output} @y history:=fatal_error_stop; {in case we quit during initialization} sys_set_ctrl_break_handler(@@handle_ctrl_break);@/ t_open_out; {open the terminal for output} @z
@x This section should be replaced, if necessary, by any special modifications of the program that are necessary to make \MF\ work at a particular installation. It is usually best to design your change file so that all changes to previous sections preserve the section numbering; then everybody's version will be consistent with the published program. More extensive changes, which introduce new sections, can be inserted here; then only the index itself will get a new section number. @y @ @<Units in the outer block@>= iso_7185, sys_utils; {needed for ISO input/output and date/time routines} @ @<Last-minute...@>= function handle_ctrl_break(ctrl_break:boolean):boolean; begin interrupt:=1; handle_ctrl_break:=true; end; @z
TeX. also requires a pool file, so it should be generated the same way as it was for As was the case with TeX, does not require the -Miso switch.
The reader should now be able to compile and properly set up TeX format and properly set up TeX. , generate all of the font metric files required by the Plain A complete setup, as described by Knuth, also requires dumping the core image of the running TeX process with the Plain TeX format loaded in memory, but this procedure (along with a way to run such a core image) is so system-dependent it would probably require a separate article for each and every platform, so it is not described in this article. The user can run TeX without any format preloaded and specify the Plain TeX format in the command line.
tex "&plain" story.tex
Preparing a TeX installation with the Plain TeX format preloaded is left as an excercise for the reader, only if they feel adventurous enough.
DVItype
The user might want to run the TRIP test for their newly prepared TeX installation, but it would require several other programs in the source distribution. The entire procedure is rather simple and most of the used programs are rather simple, but compiling DVItype is somewhat tricky, so the required changes will also be provided in this article.
As it is usually the case, we start with defining the default case mechanism.
@x @d othercases == others: {default for cases not listed explicitly} @y @d othercases == e@&l@&s@&e {default for cases not listed explicitly} @z
We remove the text_file definition.
@x @!text_file=packed file of text_char; @y @z
We do not have the reset procedure capable of specifying the file name for the file, but we can combine it with the assign procedure.
@x procedure open_tfm_file; {prepares to read packed bytes in |tfm_file|} begin reset(tfm_file,cur_name); end; @y procedure open_tfm_file; {prepares to read packed bytes in |tfm_file|} begin assign(tfm_file,cur_name); reset(tfm_file); end; @z
Since we are going to be compiling the resulting Pascal source file with a -Miso switch again, we cannot use the string type for the file name, but we can make do with a short_string.
@x @!cur_name:packed array[1..name_length] of char; {external name, @y @!cur_name:short_string; {external name, @z
The following change implements the random access mechanism required for the DVItype.
@x @p function dvi_length:integer; begin set_pos(dvi_file,-1); dvi_length:=cur_pos(dvi_file); end; @# procedure move_to_byte(n:integer); begin set_pos(dvi_file,n); cur_loc:=n; end; @y @p function dvi_length:integer; begin dvi_length:=file_size(dvi_file); end; @# procedure move_to_byte(n:integer); begin seek(dvi_file,n); cur_loc:=n; end; @z
Now comes the unusual part. DVItype actually uses the regular output variable for the description of the input file, but it also uses a separate file to communicate with the user’s terminal. This poses a problem, as most readers probably use an operating system with their interactive terminal running over their operating system’s standard input and output files. This time we will have to have two files to separate the actual program output and the user prompt, but fortunately we can make do with another file possibly connected to our terminal—namely the standard error file. The term_in and term_out files are declared without initialization in this WEB file, but we can remove the declarations and define them as the input and std_err files respectively. The following two changes show the proper way to do it.
@x @ The |input_ln| routine waits for the user to type a line at his or her terminal; then it puts ASCII-code equivalents for the characters on that line into the |buffer| array. The |term_in| file is used for terminal input, and |term_out| for terminal output. @^system dependencies@> @y @ The |input_ln| routine waits for the user to type a line at his or her terminal; then it puts ASCII-code equivalents for the characters on that line into the |buffer| array. The |term_in| file is used for terminal input, and |term_out| for terminal output. @^system dependencies@> @d term_in == i@&n@&p@&u@&t {standard input} @d term_out == std_err {standard output} @z
@x @!buffer:array[0..terminal_line_length] of ASCII_code; @!term_in:text_file; {the terminal, considered as an input file} @!term_out:text_file; {the terminal, considered as an output file} @y @!buffer:array[0..terminal_line_length] of ASCII_code; @z
As was the case in several other programs, we have to change the update_terminal definition.
@x @d update_terminal == break(term_out) {empty the terminal output buffer} @y @d update_terminal == flush(term_out) {empty the terminal output buffer} @z
We subject the input_ln procedure to a treatment similar to its more complicated TeX and counterpart.
@x @p procedure input_ln; {inputs a line from the terminal} var k:0..terminal_line_length; begin update_terminal; reset(term_in); if eoln(term_in) then read_ln(term_in); k:=0; while (k<terminal_line_length)and not eoln(term_in) do begin buffer[k]:=xord[term_in^]; incr(k); get(term_in); end; buffer[k]:=" "; end; @y @p procedure input_ln; {inputs a line from the terminal} var k:0..terminal_line_length; line:short_string; begin update_terminal; read_ln(term_in,line); k:=0; while (k<long_int(line[0])) do begin buffer[k]:=xord[line[k+1]]; incr(k); end; buffer[k]:=" "; end; @z
Applying rewrite to std_err can prove fatal in Free Pascal (as it would cause it to lose its file descriptor and be assigned to standard output), so we simply remove the procedure call.
@x begin rewrite(term_out); {prepare the terminal for output} write_ln(term_out,banner); @y begin write_ln(term_out,banner); @z
The default font directory and its length should be changed to reflect the target file system and operating system’s conventions.
@x @d default_directory_name=='TeXfonts:' {change this to the correct name} @d default_directory_name_length=9 {change this to the correct length} @y @d default_directory_name=='/app/dek/tex-3.141592653/fonts/' @d default_directory_name_length=31 @z
We also need to change the way we handle files (just as we did in TeX and ), so we implement the same mechanism we have used before.
@x for k:=1 to name_length do cur_name[k]:=' '; @y; cur_name[0]:=char(font_name[nf+1]-font_name[nf]+4); if p=0 then cur_name[0]:=char(long_int(cur_name[0])+default_directory_name_length); @z
Funnily enough, this program actually contains a normalization procedure to force all of the paths to appear in upper case. Unless this behavior is desired, we can simply remove the translation and cause the attached file extension to appear in lower case.
@x if (names[k]>="a")and(names[k]<="z") then cur_name[r]:=xchr[names[k]-@'40] else cur_name[r]:=xchr[names[k]]; @y cur_name[r]:=xchr[names[k]]; @z
@x cur_name[r+1]:='.'; cur_name[r+2]:='T'; cur_name[r+3]:='F'; cur_name[r+4]:='M' @y cur_name[r+1]:='.'; cur_name[r+2]:='t'; cur_name[r+3]:='f'; cur_name[r+4]:='m' @z
Conclusions
The reader should now be able to run legacy TeX formats and read the TeXbook with the output resembling the examples in the book as close as it is possible. The resulting programs should pass their respective tests (and the reader is more than welcome to try them) and can possibly be tweaked even more to suit the particular installation.
It would also probably be a good idea to check if the documentation files generated by WEAVE still make sense after our changes.
References
Jensen, Kathleen, and Wirth, Niklaus. 1974. Pascal User Manual and Report. New York: Springer-Verlag. https://seriouscomputerist.atariverse.com/media/pdf/book/Pascal - Manual & Report.pdf.
Knuth, Donald. E. 1984. Computers and Typesetting. 5 vols. Reading, Massachusetts: Addison-Wesley.
Knuth, Donald. E. 1989. The WEB System of Structured Documentation.
Van Canneyt, Michaël, and Klämpfl, Florian. 2021. Free Pascal User’s guide. https://downloads.freepascal.org/fpc/docs-pdf/user.pdf.