LLX > Neil Parker > Apple II > Pascal Secrets
Here are some secrets of Apple Pascal which Apple never documented. Most of this was discovered by studying source code—the source code for Apple Pascal has never been publicly available, but that of its predecessor, UCSD Pascal II.0, was released in 2006, and can be downloaded from Bitsavers.org. UCSD Pascal II.0 is definitely not the same as Apple Pascal (most importantly, II.0 had no concept of intrinsic units), but there are enough points in common that quite a lot about one can be learned by studying the other.
Warning: This is definitely not beginner-level information. Much of what follows assumes familiarity with just about everything in the Apple Pascal manuals, especially the P-machine architecture and codefile format. The most complete manual is the Apple II Pascal 1.3 manual. More important information is found in the Device and Interrupt Support Tools Manual, and its predecessor the ATTACH-BIOS Document for Apple II Pascal 1.1 (the latter is obsolete, but contains some information omitted from its replacement). Another good source of information is Randy Hyde's p-Source, which includes lots of information about the innards of Apple Pascal 1.1.
{$U-}
{$U-}
In Apple Pascal 1.0 and 1.1, SEPARATE
is a reserved word. It
was taken out in 1.2.
In UCSD Pascal II.0, you could declare a SEPARATE UNIT
. This
worked very much like a regular UNIT
, except that the
unit was given a segment type of SEPRTSEG
(rather than the
usual UNITSEG
), which is the same segment type that the
assembler gives to its output. Thus, when a SEPARATE UNIT
was
linked, it behaved like assembly-language routines instead of a regular
unit—that is, any referenced routines were extracted and added to the
caller's segment, instead of the whole unit going into a segment all
its own.
This feature might still have been functional in Apple Pascal 1.0. It was removed in 1.1, but the orphan reserved word was still reserved until 1.2.
Despite what the manual (and the Pascal standard) says, NIL
is not a reserved word in Apple Pascal. It's implemented as a
pre-defined identifier, which means that the compiler won't complain if
declare your own identifier with that name (which is, of course, not
recommended).
Not all of Apple Pascal's built-in routines are described in the manual. Here are the omitted routines:
OPENNEW(VAR f: FILE OF whatever; name: STRING)
REWRITE(f, name)
.OPENOLD(VAR f: FILE OF whatever; name: STRING)
RESET(f, name)
, except
that the name argument may not be omitted.TIME(VAR hi, lo: INTEGER)
Not all of the Apple Pascal compiler options are documented in the manual. The USCD Pascal II.0 source code reveals these additional (admittedly not very useful) options, which are still supported through Apple Pascal 1.3:
{$D+}
BPT
(breakpoint) instruction before every statement. The
BPT
instruction is intended to pass control to a debugger,
but even though a D(ebug command persisted on the command menu
through Apple Pascal 1.1, a debugger has never been available for Apple
Pascal (nor for UCSD Pascal II.0), and the BPT
instruction
does nothing. {$D+}
may be placed before any statement, and
its effect lasts until a {$D-}
occurs. The default state is
{$D-}
.{$F+}
{$F-}
.{$T+}
{$T-}
.The assembler recogizes two directives that aren't documented in the manual:
.ASECT
DSECT
directive of
Apple's DOS Toolkit assembler, or the DUM
directive of later versions of Merlin.
It starts a special source code section within which no code is generated
and no space is reserved, but any labels defined within the section are
assigned (absolute) values as if code were being generated normally. After
.ASECT
, the next directive is usually .ORG
,
followed by .BYTE
, .WORD
, .BLOCK
,
and/or .EQU
. .ASECT
must occur within a
.PROC
or .FUNC
. The special section ends with
a .PSECT
directive, after which normal code generation resumes
at the point where it left off..PSECT
.ASECT
section. Every
.ASECT
must end with a .PSECT
.Example usage:
.PROC SILLY .ASECT .ORG 300 FOO .BLOCK 8 BAR .WORD 0 BAZ .BYTE 0 .PSECT LDA FOO ;Becomes "LDA 300" LDA BAR ;Becomes "LDA 308" LDA BAZ ;Becomes "LDA 30A" RTS .END
I haven't tested these directives very much, so I can't vouch for how
reilable they are—it's possible that, like the buggy
.ALIGN
directive before Apple Pascal 1.2, they were left
undocumented for a reason.
The Apple Pascal manual shows how each code segment has a machine type, of which ten values are possible, numbered 0 through 9. But it documents only four of these types (0, 1, 2, and 7). So what are the other six?
Digging around a bit in Apple's LIBMAP.CODE reveals the full list:
Type | Processor |
---|---|
0 | Unknown |
1 | P-code (most sig. 1st) |
2 | P-code (least sig. 1st) |
3 | PDP-11 |
4 | 8080 |
5 | Z-80 |
6 | GA 440 |
7 | 6502 |
8 | 6800 |
9 | TI 9900 |
Of course only types 2 and 7 are meaningful on Apple Pascal. The others were used by other versions of UCSD Pascal. Version II.0 and earlier compilers usually didn't set the machine type at all, so their code appears to be of type 0 (Unknown).
(So what's "GA 440"? It was the General Automation 440, a mini-mainframe of the 1970's.)
{$U-}
The Apple Pascal manual documents the {$U-}
compiler option,
but only with a brief, cryptic note about how code compiled using it
behaves significantly differently than normal, and it should only be used
by people familiar with how it works. Thus "What the heck does
{$U-}
really do?" has been one of Apple Pascal's
enduring mysteries.
In brief, it switches the compiler into system mode, which is used to compile Apple Pascal itself (SYSTEM.PASCAL, and also SYSTEM.FILER, SYSTEM.EDITOR, SYSTEM.COMPILER, SYSTEM.ASSMBLER, and SYSTEM.LINKER, and probably parts of SYSTEM.LIBRARY).
Normally, the compiler produces code with these characteristics:
{$G-,I+,R+,V+}
.RBP
instruction, which
returns control to the caller.{$N+}
is used.Turning on {$U-}
does this instead:
{$G+,I-,R-,V-}
.XIT
instruction, which reboots the
computer.{$N}
setting.These may seem like minor behind-the-scenes bureaucratic changes, but they (especially the segment number assignments) have profound and far-reaching effects on how the program behaves. To understand these, we need to look at how Pascal code files are loaded into memory.
The P-code interpreter includes, as part of its SYSCOM area, an array called SEGTABLE, which has 32 entries (or 64 in the 128K system), one for each possible segment number. Each entry describes one segment of the currently-active program or library file, and consists of three pieces of information: the segment's disk unit number, the block number of its first disk block, and its length in bytes. At boot time, entries 0 through 6 of SEGTABLE are filled (by SYSTEM.APPLE) with the information for SYSTEM.PASCAL's segments. Within SYSTEM.APPLE is a routine that uses this information to read a segment from disk into the next available space on the program stack, which runs whenever a call is made to a routine that's not already in memory.
SYSTEM.PASCAL's segment 0 is its main program, and the basic routines that Pascal code calls upon to handle I/O and strings. It always remains in memory. Segments 2 through 6 of SYSTEM.PASCAL contain routines that can be swapped out when not needed, such as its startup initialization, main menu handling, error message printing, etc.
SYSTEM.PASCAL's segment 1, called USERPROGRAM, is just a stub that does nothing but print the message "No user program". As we'll see in a moment, you'll hardly ever see this message.
Whenever SYSTEM.PASCAL is asked to run a program, it opens the code file and reads its first block, which contains the segment dictionary. It uses the segment dictionary contents to fill the SEGTABLE array with the information for each of the program's segments, and each of the intrinsic units that it uses. But as it does this, it ignores any segments with segment number 0 or 2 through 6—thus all of SYSTEM.PASCAL's segments (except USERPROGRAM) are preserved in SEGTABLE, regardless of what's in the code file.
Then it closes the code file, and calls the USERPROGRAM procedure. But by this time USERPROGRAM's segment information is no longer in SEGTABLE—it's been replaced with segment 1 of the new program. So instead of SYSTEM.PASCAL's USERPROGRAM stub, the new program's segment 1 starts running.
Note how this meshes with standard compilation conventions described above. The compiler has put the program's main code in segment 1, and any additional segments are numbered starting with 7, so they won't get ignored.
Note also that this makes the main menu's U(ser restart command especially simple—it just calls USERPROGRAM again.
Now consider what all of this implies for {$U-}
mode. The
compiler puts the main code in segment 0, which is ignored, so there's no
point in having your main code be anything beyond BEGIN END
,
unless you're writing a replacement for SYSTEM.PASCAL itself (not
recommended, unless you're exceptionally ambitious). For the same reason
there's no point in declaring any normal PROCEDUREs or FUNCTIONs at the
top level.
Segment 1 will be loaded and run as the main program, so it had
better actually be your main program. It must be declared as
your first SEGMENT PROCEDURE
. SYSTEM.PASCAL passes two
word-sized arguments to it, which it must accept, but
they're useless and can be ignored by the rest of your code
(the declaration commonly looks like SEGMENT PROCEDURE
MYPROGRAM(XXX, YYY: INTEGER);
).
This procedure's local variables are in effect your global
variables, and you should nest all your PROCEDUREs and FUNCTIONs inside
it.
if you want any additional SEGMENT PROCEDUREs or SEGMENT FUNCTIONs beyond
the first not to be ignored, you'll need to skip ahead to at least
segment 7. In the old days this could only be done by declaring enough
empty dummy procedures to fill the gap (SEGMENT PROCEDURE DUMMY2;
BEGIN END; SEGMENT PROCEDURE DUMMY3; BEGIN END;
, etc.), but ever
since Apple Pascal 1.1, you can write {$NS 7}
instead.
Unfortunately, units don't play well with {$U-}
programs.
In the case of intrinsic units, they will not be automatically
preloaded, regardless of the setting of the {$N}
switch, and
their initialization routines will not be called. Using
{$R unitname}
can solve the preloading problem (provided the
unit doesn't have a data segment), but there doesn't seem to be any way to
get a {$U-}
program to call a unit's initialization routine.
This might be acceptable if the unit's initialization routine doesn't
do anything, but it means units whose initialization routines do something
important (such as the TRANSCEND or TURTLEGR unit) can't be used in a
{$U-}
program.
In the case of regular units, it's even worse. The compiler mistakenly
allocates segment 1 for the unit, pushing your main SEGMENT
PROCEDURE
to segment 2, which ruins everything. And then the linker
chokes on the mistaken segment, so the program can't be linked.
So, given the inconvenience of all those rules, what's the benefit of
compiling anything other than SYSTEM.PASCAL in {$U-}
mode?
Simple: Access to SYSTEM.PASCAL's global variables. Segment 0 is ignored
when loading a program, but the compiler doesn't know that, and
any references that you compile to top-level global variables will still
try to access segment 0's data space, and since your segment 0
never got loaded, that means SYSTEM.PASCAL's segment 0's data space.
Of course you must be very careful if you do this. You can't
just define whatever {$U-}
global variables you feel like -
you can only use variables that SYSTEM.PASCAL has already defined. And
since Apple has never published anything about these variables, doing this
has traditionally not been easy.
But now that we have the UCSD Pascal II.0 source code, it's gotten a lot easier—a little experimentation shows that the II.0 global variables are still laid out the same way in Apple Pascal. This is the topic of the next section.
First, a warning: THIS SECTION APPLIES TO THE FULL PASCAL DEVELOPMENT ENVIRONMENT ONLY. THE RUNTIME SYSTEM'S GLOBAL VARIABLES ARE DIFFERENT! If you write code intended to run under the runtime environment, it's probably best to assume that the global variables aren't available at all.
If you're still with me, the listing below shows the constant, type, and variable definitions from UCSD Pascal II.0's GLOBALS.TEXT. Comments beginning with "NP:" are my own additions, as are the numbers at the right side of data structures, which indicate the offsets from the beginning of the data structure to the field.
CONST MMAXINT = 32767; (*MAXIMUM INTEGER VALUE*) MAXUNIT = 12; (*MAXIMUM PHYSICAL UNIT # FOR UREAD*) {NP: ^^ 12 for Apple Pascal 1.0 & 1.1, 20 for 1.2 & 1.3 } MAXDIR = 77; (*MAX NUMBER OF ENTRIES IN A DIRECTORY*) VIDLENG = 7; (*NUMBER OF CHARS IN A VOLUME ID*) TIDLENG = 15; (*NUMBER OF CHARS IN TITLE ID*) MAX_SEG = 31; (*MAX CODE SEGMENT NUMBER*) {NP: ^^ 31 for the 64K system, 63 for the 128K system } FBLKSIZE = 512; (*STANDARD DISK BLOCK LENGTH*) DIRBLK = 2; (*DISK ADDR OF DIRECTORY*) AGELIMIT = 300; (*MAX AGE FOR GDIRP...IN TICKS*) EOL = 13; (*END-OF-LINE...ASCII CR*) DLE = 16; (*BLANK COMPRESSION CODE*) NAME_LEN = 23; {Length of CONCAT(VIDLENG,':',TIDLENG)} FILL_LEN = 11; {Maximum # of nulls in FILLER} TYPE IORSLTWD = (INOERROR,IBADBLOCK,IBADUNIT,IBADMODE,ITIMEOUT, ILOSTUNIT,ILOSTFILE,IBADTITLE,INOROOM,INOUNIT, INOFILE,IDUPFILE,INOTCLOSED,INOTOPEN,IBADFORMAT, ISTRGOVFL); (*COMMAND STATES...SEE GETCMD*) CMDSTATE = (HALTINIT,DEBUGCALL, UPROGNOU,UPROGUOK,SYSPROG, COMPONLY,COMPANDGO,COMPDEBUG, LINKANDGO,LINKDEBUG); (*CODE FILES USED IN GETCMD*) SYSFILE = (ASSMBLER,COMPILER,EDITOR,FILER,LINKER); (*ARCHIVAL INFO...THE DATE*) DATEREC = PACKED RECORD MONTH: 0..12; (*0 IMPLIES DATE NOT MEANINGFUL*) DAY: 0..31; (*DAY OF MONTH*) YEAR: 0..100 (*100 IS TEMP DISK FLAG*) END (*DATEREC*) ; (*VOLUME TABLES*) UNITNUM = 0..MAXUNIT; VID = STRING[VIDLENG]; (*DISK DIRECTORIES*) DIRRANGE = 0..MAXDIR; TID = STRING[TIDLENG]; FULL_ID = STRING[NAME_LEN]; FILE_TABLE = ARRAY [SYSFILE] OF FULL_ID; FILEKIND = (UNTYPEDFILE,XDSKFILE,CODEFILE,TEXTFILE, INFOFILE,DATAFILE,GRAFFILE,FOTOFILE,SECUREDIR); {NP: DIRENTRY is the format of each entry in a disk's directory.} DIRENTRY = PACKED RECORD DFIRSTBLK: INTEGER; (*FIRST PHYSICAL DISK ADDR*) {0} DLASTBLK: INTEGER; (*POINTS AT BLOCK FOLLOWING*) {2} CASE DFKIND: FILEKIND OF {4} SECUREDIR, UNTYPEDFILE: (*ONLY IN DIR[0]...VOLUME INFO*) {NP: The volume name of an Apple Pascal disk almost always has UNTYPEDFILE here. For SECUREDIR, see the description of the MISCINFO.USERKIND field under All About MISCINFO below.} (FILLER1 : 0..2048; {for downward compatibility,13 bits} DVID: VID; (*NAME OF DISK VOLUME*) {6} DEOVBLK: INTEGER; (*LASTBLK OF VOLUME*) {14} DNUMFILES: DIRRANGE; (*NUM FILES IN DIR*) {16} DLOADTIME: INTEGER; (*TIME OF LAST ACCESS*) {18} DLASTBOOT: DATEREC); (*MOST RECENT DATE SETTING*) {20} {NP: The last byte (byte 25) of the volume name entry may contain option flags. Starting in Apple Pascal 1.2, if bit 3 (the $08 bit) is set on the boot disk, Pascal will ignore any card in slot 3 and always use the 40-column screen. Other bits are used on the runtime system to control the system swapping level and the single-drive option.} XDSKFILE,CODEFILE,TEXTFILE,INFOFILE, DATAFILE,GRAFFILE,FOTOFILE: (FILLER2 : 0..1024; {for downward compatibility} STATUS : BOOLEAN; {for FILER wildcards} DTID: TID; (*TITLE OF FILE*) {6} DLASTBYTE: 1..FBLKSIZE; (*NUM BYTES IN LAST BLOCK*) {22} DACCESS: DATEREC) (*LAST MODIFICATION DATE*) {24} END (*DIRENTRY*) ; {26 total} DIRP = ^DIRECTORY; {NP: DIRECTORY is a disk's full directory: 78 DIRENTRY's, occupying blocks 2 through 5 of the disk.} DIRECTORY = ARRAY [DIRRANGE] OF DIRENTRY; (*FILE INFORMATION*) CLOSETYPE = (CNORMAL,CLOCK,CPURGE,CCRUNCH); WINDOWP = ^WINDOW; WINDOW = PACKED ARRAY [0..0] OF CHAR; FIBP = ^FIB; {NP: FIB is the structure underlying the FILE types. If by variant record trickery you can get a FIB record to overly a FILE variable, you can use this record to see inside the FILE variable. Pascal always allocates 600 bytes for a typed FILE, which is 22 bytes more than needed (the extra bytes are wasted). FILEs also need however much extra memory is required for the GET/PUT buffer.} FIB = RECORD FWINDOW: WINDOWP; (*USER WINDOW...F^, USED BY GET-PUT*) {0} FEOF,FEOLN: BOOLEAN; {4,2} FSTATE: (FJANDW,FNEEDCHAR,FGOTCHAR); {6} FRECSIZE: INTEGER; (*IN BYTES...0=>BLOCKFILE, 1=>CHARFILE*) {8} CASE FISOPEN: BOOLEAN OF {10} TRUE: (FISBLKD: BOOLEAN; (*FILE IS ON BLOCK DEVICE*) {12} FUNIT: UNITNUM; (*PHYSICAL UNIT #*) {14} FVID: VID; (*VOLUME NAME*) {16} FREPTCNT, (* # TIMES F^ VALID W/O GET*) {28} FNXTBLK, (*NEXT REL BLOCK TO IO*) {26} FMAXBLK: INTEGER; (*MAX REL BLOCK ACCESSED*) {24} FMODIFIED:BOOLEAN;(*PLEASE SET NEW DATE IN CLOSE*) {30} FHEADER: DIRENTRY;(*COPY OF DISK DIR ENTRY*) {32} CASE FSOFTBUF: BOOLEAN OF (*DISK GET-PUT STUFF*) {58} TRUE: (FNXTBYTE,FMAXBYTE: INTEGER; {62,60} FBUFCHNGD: BOOLEAN; {64} FBUFFER: PACKED ARRAY [0..FBLKSIZE] OF CHAR)) {66} END (*FIB*) ; {578 total} (*USER WORKFILE STUFF*) INFOREC = RECORD SYMFIBP,CODEFIBP: FIBP; (*WORKFILES FOR SCRATCH*) {2,0} ERRSYM,ERRBLK,ERRNUM: INTEGER; (*ERROR STUFF IN EDIT*) {8,6,4} SLOWTERM,STUPID: BOOLEAN; (*STUDENT PROGRAMMER ID!!*) {12,10} ALTMODE: CHAR; (*WASHOUT CHAR FOR COMPILER*) {14} GOTSYM,GOTCODE: BOOLEAN; (*TITLES ARE MEANINGFUL*) {18,16} WORKVID,SYMVID,CODEVID: VID; (*PERM&CUR WORKFILE VOLUMES*) {36,28,20} WORKTID,SYMTID,CODETID: TID (*PERM&CUR WORKFILES TITLE*) {76,60,44} END (*INFOREC*) ; {92 total} (*CODE SEGMENT LAYOUTS*) SEG_RANGE = 0..MAX_SEG; SEG_DESC = RECORD DISKADDR: INTEGER; (*REL BLK IN CODE...ABS IN SYSCOM^*) {0} CODELENG: INTEGER (*# BYTES TO READ IN*) {2} END (*SEGDESC*) ; {4 total} (*DEBUGGER STUFF*) BYTERANGE = 0..255; TRICKARRAY = RECORD {Memory diddling for execerror} CASE BOOLEAN OF TRUE : (WORD : ARRAY [0..0] OF INTEGER); FALSE : (BYTE : PACKED ARRAY [0..0] OF BYTERANGE) END; MSCWP = ^ MSCW; (*MARK STACK RECORD POINTER*) MSCW = RECORD STATLINK: MSCWP; (*POINTER TO PARENT MSCW*) {0} DYNLINK: MSCWP; (*POINTER TO CALLER'S MSCW*) {2} MSSEG,MSJTAB: ^TRICKARRAY; {6,4} MSIPC: INTEGER; {8} {NP: MSSP was missing in UCSD Pascal II.0.} MSSP: INTEGER {10} {NP: LOCALDATA isn't really part of MCSW...it's the first element of the routine's parameters/local variables.} LOCALDATA: TRICKARRAY END (*MSCW*) ; {12 total} (*SYSTEM COMMUNICATION AREA*) (*SEE INTERPRETERS...NOTE *) (*THAT WE ASSUME BACKWARD *) (*FIELD ALLOCATION IS DONE *) SEG_ENTRY = RECORD CODEUNIT: UNITNUM; {0} CODEDESC: SEGDESC {2} END; {6 total} SYSCOMREC = RECORD IORSLT: IORSLTWD; (*RESULT OF LAST IO CALL*) {0} XEQERR: INTEGER; (*REASON FOR EXECERROR CALL*) {2} SYSUNIT: UNITNUM; (*PHYSICAL UNIT OF BOOTLOAD*) {4} BUGSTATE: INTEGER; (*DEBUGGER INFO*) {6} GDIRP: DIRP; (*GLOBAL DIR POINTER,SEE VOLSEARCH*) {8} LASTMP,STKBASE,BOMBP: MSCWP; {14,12,10} MEMTOP,SEG,JTAB: INTEGER; {20,18,16} BOMBIPC: INTEGER; (*WHERE XEQERR BLOWUP WAS*) {22} HLTLINE: INTEGER; (*MORE DEBUGGER STUFF*) {24} BRKPTS: ARRAY [0..3] OF INTEGER; {26} RETRIES: INTEGER; (*DRIVERS PUT RETRY COUNTS*) {34} EXPANSION: ARRAY [0..8] OF INTEGER; {36} HIGHTIME,LOWTIME: INTEGER; {56,54} MISCINFO: PACKED RECORD {58} NOBREAK,STUPID,SLOWTERM, HASXYCRT,HASLCCRT,HAS8510A,HASCLOCK: BOOLEAN; USERKIND:(NORMAL, AQUIZ, BOOKER, PQUIZ); WORD_MACH, IS_FLIPT : BOOLEAN END; CRTTYPE: INTEGER; {60} CRTCTRL: PACKED RECORD RLF,NDFS,ERASEEOL,ERASEEOS,HOME,ESCAPE: CHAR; {62..67} BACKSPACE: CHAR; {68} FILLCOUNT: 0..255; {69} CLEARSCREEN, CLEARLINE: CHAR; {71,70} PREFIXED: PACKED ARRAY [0..15] OF BOOLEAN {72} END; CRTINFO: PACKED RECORD WIDTH,HEIGHT: INTEGER; {76,74} RIGHT,LEFT,DOWN,UP: CHAR; {78..81} BADCH,CHARDEL,STOP,BREAK,FLUSH,EOF: CHAR; {82..87} ALTMODE,LINEDEL: CHAR; {89,88} ALPHA_LOCK,ETX,PREFIX: CHAR; {90..92} PREFIXED: PACKED ARRAY [0..15] OF BOOLEAN {94} END; SEGTABLE: ARRAY [SEG_RANGE] OF SEG_ENTRY; {96} END (*SYSCOM*); {192 or 216 total} MISCINFOREC = RECORD MSYSCOM: SYSCOMREC END; VAR SYSCOM: ^SYSCOMREC; (*MAGIC PARAM...SET UP IN BOOT*) {0} GFILES: ARRAY [0..5] OF FIBP; (*GLOBAL FILES, 0=INPUT, 1=OUTPUT*) {2} USERINFO: INFOREC; (*WORK STUFF FOR COMPILER ETC*) {14} EMPTYHEAP: ^INTEGER; (*HEAP MARK FOR MEM MANAGING*) {106} INPUTFIB,OUTPUTFIB, (*CONSOLE FILES...GFILES ARE COPIES*) {114,112} SYSTERM,SWAPFIB: FIBP; (*CONTROL AND SWAPSPACE FILES*) {110,108} SYVID,DKVID: VID; (*SYSUNIT VOLID & DEFAULT VOLID*) {124,116} THEDATE: DATEREC; (*TODAY...SET IN FILER OR SIGN ON*) {132} DEBUGINFO: ^INTEGER; (*DEBUGGERS GLOBAL INFO WHILE RUNIN*) {134} STATE: CMDSTATE; (*FOR GETCOMMAND*) {136} PL: STRING; (*PROMPTLINE STRING...SEE PROMPT*) {138} IPOT: ARRAY [0..4] OF INTEGER; (*INTEGER POWERS OF TEN*) {220} FILLER: STRING[FILL_LEN]; (*NULLS FOR CARRIAGE DELAY*) {230} DIGITS: SET OF '0'..'9'; {242} UNITABLE: ARRAY [UNITNUM] OF (*0 NOT USED*) {250} RECORD UVID: VID; (*VOLUME ID FOR UNIT*) CASE UISBLKD: BOOLEAN OF TRUE: (UEOVBLK: INTEGER) END (*UNITABLE*) ; {NP: Because Apple Pascal 1.2 changed MAXUNIT from 12 to 20, everything after this point is 96 bytes farther up in memory starting in 1.2: The next field is at offset 406 in 1.0 and 1.1, and offset 502 in 1.2 and 1.3. Rather than maintain two offset lists, I'll restart the count at 0.} FILENAME : FILE_TABLE; {0} {NP: THis is the end of the II.0 globals. The variables listed below were added in Apple Pascal. The functions of these were worked out by Dave Tribby, and are described in files on Tribby's PSYS disk, from which the following was copied.} config_char: SET OF CHAR; { Configuration chars from MISCINFO } {120} chain_name: STRING[23]; { "Next File" from SETCHAIN } {136} chain_msg: STRING; { "Message" from SETCVAL } {160} what_f: ARRAY [1..2] OF INTEGER; { both elements are the same } {242} { 0 until EXEC file opened, then 3370 } { or 4394, depending upon bottom of heap } exec_ch_num: INTEGER; { Num of next EXEC char in disk buffer } {246} what_g: ARRAY [1..2] OF INTEGER; {248} { 0 until EXEC file opened, then 511,2 } r_exec_flg: BOOLEAN; { Reading an EXEC file flag } {252} w_exec_flg: BOOLEAN; { Writing an EXEC file flag } {254} swap_on: BOOLEAN; { System swapping on? } {256} swap_1_on: BOOLEAN; { Level 1 swapping on? } {258} swap_2_on: BOOLEAN; { Level 2 swapping on? } {260} just_boot: BOOLEAN; { Just booted flag } {262} what_h: ARRAY [1..2] OF INTEGER; { 0,0 } {264} exec_in_ch: CHAR; { Last char read from EXEC file } {268} exec_term: CHAR; { EXEC file termination character } {270} what_i: ARRAY [1..7] OF INTEGER; {272} { #2: 1 when EXEC file closed, 0 when open } { #3: 1 when EXEC file closed, 0 when open } { #6: 0 when EXEC file closed, 1 when open } { #7: 1 after EXEC file opened } exec_unit: INTEGER; { EXEC volume unit number } {286} exec_vol: VID; { EXEC file volume name } {288} exec_size: INTEGER; { # blocks in EXEC file } {296} what_j: ARRAY [1..3] OF INTEGER; {298} { #1: 0 when EXEC file closed, 3 when reading, 2 when writing. } exec_fentry: DIRENTRY; { Directory entry for EXEC file } {304} what_k: ARRAY [1..11] OF INTEGER; {330} dline_str: STRING[7]; { Printed for each char to delete line } {352} { (usually single char backspace) } bspace_str: STRING[7]; { Printed to backspace and blank out a char } {360} { (usually bspace, blank, bspace) } run_vol: VID; { Run (%) volume } {368} {NP: run_vol did not exist prior to Apple Pascal 1.2.} what_l: ARRAY [1..4] OF INTEGER; {376} {NP: End of Apple Pascal global variables.}
A {$U-}
program can use these directly...just include them
in your program's top-level declaration section. Even though SYSTEM.PASCAL
won't load the top-level segment, the compiler will see the
declarations and compile the correct code to access them.
A normal (non-{$U-}
) program cannot directly access these
variables—they live at nesting level -1, which a normal program, whose
nesting level starts at 0, can't reach. But there are ways that a
normal program can get to these indirectly, via some trickery which will be
discussed later.
But first some remarks about the variables themselves. These are not all equally interesting or useful...here are some of the highlights.
SYSCOM is probably the most important global variable. The data
structure it points to (the system communication area) is actually
inside SYSTEM.APPLE. When SYSTEM.APPLE first loads SYSTEM.PASCAL at boot
time, it puts a pointer to this area in the first word of SYSTEM.PASCAL's
global data block—thus VAR SYSCOM: ^SYSCOMREC
must be SYSTEM.PASCAL's first global variable.
SYSCOM^.IORSLT
contains the error code of the most recent
I/O operation. The IORESULT
function returns its value.
If a program crashes with an execution error, SYSCOM^.XEQERR
will contain the execution error code number, SYSCOM^.BOMBP
will hold a pointer to the markstack of the routine that crashed, and
SYSCOM^.BOMBIPC
will hold a pointer to the instruction that
crashed.
SYSCOM^.SYSUNIT
is the unit number of the boot disk. Its
value will probably always be 4.
On systems that support a debugger, the debugger is activated by the
BPT
instruction. SYSCOM^.BUGSTATE
controls how
the BPT
instruction responds (3 means call a breakpoint trap
on every BPT
; any other value means to use the
BRKPTS
array), SYSCOM^.HLTLINE
receives the
argument of the BPT
instruction, and if the BPT
is at any of the address in SYSCOM^.BRKPTS
, the breakpoint
trap is called. Apple Pascal doesn't support a debugger—the
BPT
instruction does nothing, and the debugger variables are
not used.
Whenever a disk file is opened, SYSTEM.PASCAL allocates a temporary
buffer on the heap to hold the disk directory, and stores a pointer to it
in SYSCOM^.GDIRP
. Any use of NEW
,
MARK
, or RELEASE
frees the buffer and sets
GDIRP
to NIL
.
SYSCOM^.STKBASE
, SYSCOM^.LASTMP
,
SYSCOM^.JTAB
, and SYSCOM^.SEG
are for storing
the current BASE, MP, JTAB, and SEG pointers. Apple Pascal keeps the live
versions of these in zero page instead.
SYSCOM^.MEMTOP
holds the highest memory address available
to SYSTEM.PASCAL.
SYSCOM^.RETRIES
is available for device drivers to store a
retry count.
On systems that use a heartbeat interrupt to implement the
TIME
procedure, SYSCOM^.HIGHTIME
and
SYSCOM^.LOWTIME
are available for use as the time counter.
Apple Pascal doesn't use these variables, and TIME
always
returns 0 in both arguments.
SYSCOM^.MISCINFO
, SYSCOM^.CRTTYPE
,
SYSCOM^.CRTCTRL
, and SYSCOM^.CRTINFO
are
described in detail below under All About
MISCINFO.
SYSCOM^.SEGTABLE
is the list of disk units and block numbers
and segment lengths used to load segments for the currently-running program.
It is describe above under The Truth About
{$U-}
.
The USERINFO
record contains useful information about the
current workfile. If USERINFO.GOTSYM
is true, then a text
workfile exists and is named CONCAT(USERINFO.SYMVID, ':',
USERINFO.SYMTID)
, and likewise, if USERINFO.GOTCODE
is
true, then a code workfile exists and is named
CONCAT(USERINFO.CODEVID, ':', USERINFO.CODETID)
. If the
workfile has a permanent name (something other than SYSTEM.WRK.TEXT /
SYSTEM.WRK.CODE), the permanent name is
CONCAT(USERINFO.WORKVID, ':', USERINFO.WORKTID)
.
The C(ompile and A(seemble commands open the source file
as USERINFO.SYMFIBP
and the object file as
USERINFO.CODEFIBP
before running the compiler or assembler.
After the compiler or assembler exits, if an error occurred,
USERINFO.ERRNO
is the error number, and its position in the
source code is given by USERINFO.ERRBLK
and
USERINFO.ERRSYM
.
The bottom of the system heap is in the pointer EMPTYHEAP
.
SYSTEM.PASCAL does a MARK(EMPTYHEAP)
after it's finished
initializing itself, and RELEASE(EMPTYHEAP)
whenever a program
exits. You can permanently reserve space at the bottom of the heap by
increasing EMPTYHEAP
(I suspect this is how
SYSTEM.ATTACH protects the memory that it loads device drivers into). Of
course this must be done before allocating any ordinary
(non-permanent) dynamic variables with NEW
, and before
manipulating EMPTYHEAP
, it's a good idea to first call
MARK
on some pointer variable—this causes the system to
discard the disk directory buffer from the heap, if there is one. After
changing EMPTYHEAP
, calling RELEASE(EMPTYHEAP)
sets the heap pointer to EMPTYHEAP
, protecting everything
underneath it.
INPUTFIB
, OUTPUTFIB
, and SYSTERM
are the FIBP
versions of the standard files INPUT
,
OUTPUT
, and KEYBOARD
. If the file
SYSTEM.SWAPDISK exists on the boot disk, it will be opened as
SWAPFIBP whenver SYSTEM.PASCAL needs to swap out memory to make room for
a disk directory.
SYVID
contains the name of the boot disk. You can inspect
this name, but don't try to change it—this isn't the only place where
the system remembers the boot disk, so changing this name can only lead
to chaos and misery.
DKVID
contains the name of the current prefix disk. You can
change the prefix by changing this variable—something normally only
possible by exiting and using the Filer's P(refix command.
THEDATE
contains the current system date. You can change it,
but doing so won't write the new date to the boot disk—normally only
the Filer does that.
UNITABLE
gives, for each unit, the unit's name (or the disk's
name if it's a disk), whether or not it's a block device, and if it's a
block device, the number of blocks on it.
FILENAME
is an array of strings, giving the full name
("DISK:FILENAME") of the assembler, filer, editor, compiler, and linker,
in that order.
Note that all of the file variables above are declared as
FIBP
.
If you want to call normal file operations on them, you can trick Pascal
into doing so by replacing the FIBP
definition with something
like this:
TYPE XFILE = FILE {for disk files, or TEXT for character devices}; FIBP = ^XFILE;
and then, if X
is one of the FIBP
variables,
you can call BLOCKREAD
and BLOCKWRITE
on
X^
if it's a disk file, or READ[LN]
and
WRITE[LN]
if it's a character device.
If you only care about a few of these variables, you don't have to declare all of them. You can completely omit all the variables after the last you're accessing, and fill the space before the first variable you're looking at with dummy arrays. Of course this means you have to count the sizes of the skipped variables carefully to make the declarations you're interested in land in the right place. For example, if you just want to access the current prefix and nothing else,
VAR DUMMY: ARRAY[1..58] OF INTEGER; {Skip over 116 bytes} DKVID: STRING[7];
If you want to access the FILENAME
array or anything after
it, and you don't want your program to depend on a specific
version of Apple Pascal, then static declarations won't work for you -
you'll need to search for the variables at run time, using techniques
like those in the next section.
{$U-}
If you're not a {$U-}
program, you can still access
SYSTEM.PASCAL's global variables, but you have to do it indirectly.
First, declare a record to hold the global varlabies. If you want access
to the FILENAME
array or anything after it, you'll need to
delcare two records, due to the fact that the UNITABLE
array sits right in the middle, and its length changed in Apple Pascal
1.2.
CONST MAXUNIT = 20; { Assume the longer case } MAX_SEG = 63; { Assume the longer case } TYPE TFILE = TEXT; { For treating FIBPs as ordinary files: } PTFILE = ^TFILE; { Replace character device FIBPs with this } BFILE = FILE; PBFILE = ^BFILE; { Replace disk file FIBPs with this } GREC1 = RECORD SYSCOM: ^SYSCOMREC; GFILES: ARRAY[0..5] OF PTFILE; USERINFO: INFOREC; { Don't forget this has disk FIBPs in it } { etc., but no farther than UNITABLE } END; GREC2 = RECORD FILENAME: FILE_TABLE; config_char: SET OF CHAR; { etc. } END; VAR VERSION: INTEGER; {Apple Pascal version: 0=1.0, 2=1.1, 3=1.2, 4=1.3} UNITMAX, SEGMAX: INTEGER; { Actual number of units and segments } GBL1P: ^GREC1; GBL2P: ^GREC2;
Now we need to find the global variables. This can be done by starting at a convenient markstack record and chasing the static links until you reach the topmost, which can be recognized by the fact that its static link points to itself. The globals start twelve bytes after the topmost markstack record (a markstack record is twelve bytes long).
A good place to start chasing links is your own global markstack record, which is pointed to by the BASE pointer, which lives at memory location 80 (this and the other magic addresses used here are described below under Memory, and Addresses of Useful Things).
PROCEDURE FINDGLOBALS; TYPE PAB = PACKED ARRAY[0..1] OF 0..255; VAR TRIX: RECORD CASE INTEGER OF 0: (I: INTEGER); 1: (P: ^INTEGER); 2: (B: ^PAB); 3: (G1: ^GREC1); 4: (G2: ^GREC2) END; BEGIN TRIX.I := -16607; { Version byte } VERSION := TRIX.B^[0]; TRIX.I := 80; { Start at BASE } WHILE TRIX.I <> TRIX.P^ DO TRIX.I := TRIX.P^; { Chase MSSTAT pointers to top } TRIX.I := TRIX.I + 12; { Skip past markstack } GBL1P := TRIX.G1; TRIX.I := TRIX.I + 406; { 1st part 406 bytes long in 1.0 and 1.1 } IF (VERSION = 3) OR (VERSION = 4) THEN TRIX.I := TRIX.I + 96; { 96 bytes longer in 1.2 and 1.3 } GBL2P := TRIX.G2 UNITMAX := 12; SEGMAX := 31; IF (VERSION = 3) OR (VERSION = 4) THEN BEGIN UNITMAX := 20; TRIX.I := -16606; IF ORD(ODD(TRIX.B^[0]) AND 96) = 64 THEN {128K} SEGMAX := 63; END END;
If you only want to access things in the SYSCOM area, you don't need to go through all the trouble of finding SYSTEM.PASCAL's globals. A pointer to SYSCOM is kept in memory location -16609, so you can just follow that:
VAR SYSCOM: ^SYSCOMREC; PROCEDURE FINDSYSCOM; VAR TRIX: RECORD CASE BOOLEAN OF FALSE: (I: INTEGER); TRUE: (P: ^SYSCOMREC) END; BEGIN TRIX.I := -16609; SYSCOM := TRIX.P END;
Here's an assembly-language routine that can be used to find the address of any variable of any type. Using this and an appropriate variant record, you can point a pointer variable at any other variable.
; FUNCTION ADDRESSOF(VAR X): INTEGER; EXTERNAL; ; Returns the address of any variable. ; (This really just returns its argument unchanged, and is about the ; closest you can get to a generic identity function.) .FUNC ADDRESSOF,1 PLA ;Save return address in X, Y TAX PLA TAY PLA ;Discard passed result space PLA PLA PLA TYA ;Restore return address PHA ;(leaving argument as result) TXA PHA RTS .END
Then, in your main program, declare (as the comment indicates)
FUNCTION ADDRESSOF(VAR X): INTEGER; EXTERNAL;
, and link the
assembled routine to your main code.
(The Apple Pascal 1.3 manual lists almost the same routine, but this version, which uses the X and Y registers to save the return address instead of a pair of zero-page addresses, is slightly shorter and faster. Honestly, looking at the manual's assembly-language examples, I sometimes think the people who wrote them forgot that the 6502 has registers.)
Example usage:
{Assuming FIB and FIBP are declared as shown above} VAR TRIX: RECORD CASE BOOLEAN OF FALSE: (I: INTEGER); TRUE: (F: FIBP) END; F: FIBP; {...} TRIX.I := ADDRESSOF(INPUT); F := TRIX.F;
Then you can use F^
to see what's going on inside the
standard file INPUT
.
The MISCINFO
, CRTTYPE
, CRTCTRL
, and
CRTINFO
fields of SYSCOM are filled at boot time from the
file SYSTEM.MISCINFO. This file is large enough to contain the entire
SYSCOM area (on the 64K system with MAX_SEG = 31), but only the four fields
just named are used—the rest of the file is ignored.
Here are all the values read from SYSTEM.MISCINFO, their byte positions in the file (and offsets from the start of SYSCOM), and for fields that have them, their names in the SETUP.CODE utility.
Byte[:bit] | Identifier | SETUP Name | Normal value | Meaning |
---|---|---|---|---|
58:0 | MISCINFO.HASCLOCK | HAS CLOCK | FALSE | TRUE if a clock is available. If FALSE, SYSTEM.PASCAL always checks
whether the right disk is still in the right drive before writing a
directory entry. If TRUE, it skips this check if less that 5 seconds
have passed since the last successful check of the same disk (the
DLOADTIME field of the disk's directory is used to remember
the last check time). |
58:1 | MISCINFO.HAS8510A | HAS 8510A | FALSE | TRUE if Terak 8510/a-compatible video hardware is available. (The Terak 8510/a was a PDP-11-based computer of the late 1970's and early 1980's.) |
58:2 | MISCINFO.HASLCCRT | HAS LOWER CASE | TRUE if lowercase is available. | |
58:3 | MISCINFO.HASXYCRT | HAS RANDOM CURSOR ADDRESSING | TRUE | TRUE if a working GOTOXY is available. |
58:4 | MISCINFO.SLOWTERM | HAS SLOW TERMINAL | FALSE | Should be TRUE if the display operates at 600 baud or slower. Causes system prompts to be abbreviated. No longer functional starting in Apple Pascal 1.2. |
58:5 | MISCINFO.STUPID | STUDENT | FALSE | If TRUE, always enter the editor after the compiler detects an error
in the source code, without offering the user any choice. In UCSD Pascal
II.0, it also caused the compiler to insert USES
TURTLEGRAPHICS into every program. |
58:6 | MISCINFO.NOBREAK | (none) | FALSE | If TRUE, prevent the BREAK key (ctrl-@) from working. Doesn't seem to be functional on the Apple II. This bit was very version-dependent even in UCSD Pascal II.0—it worked in the PDP-11 version, but not the 8080 version. |
58:7 (lo) & 59:0 (hi) | MISCINFO.USERKIND | (none) | 0 (USER) | 0 (USER) for normal users, 1 (AQUIZ) or 3 (PQUIZ) for quiz system
users, or 2 (BOOKER) for quiz system bookkeepers.
Once supported an electronic quiz system, where normal users and
quiz users couldn't access each other's disks, but the quiz bookkeeper
could access any disk (this was accomplished by checking the disk's
DIRENTRY[0].DFKIND , the file type of the volume
name—UNTYPEDFILE means a USER disk, and
SECUREDIR means an AQUIZ or PQUIZ disk).
If 1 (AQUIZ), the system reboots after the startup program
terminates. |
59:1 | MISCINFO.IS_FLIPT | (none) | FALSE | If TRUE, the system error message routine assumes the machine has big-endian byte order. |
59:2 | MISCINFO.WORD_MACH | (none) | FALSE | Apparently unused. |
60 (lo) & 61 (hi) | CRTTYPE | (none) | Reserved (but not actually used) for a number identifying the type of terminal in use. | |
62 | CRTCTRL.ESCAPE | LEAD-IN TO SCREEN | CHR(0), ^@ | If there are two-character screen control codes, the first character of the sequence. |
63 | CRTCTRL.HOME | MOVE CURSOR HOME | CHR(25), ^Y | Control code to move the cursor to the upper left corner of the screen. |
64 | CRTCTRL.ERASEEOS | ERASE TO END OF SCREEN | CHR(11), ^K | Control code to clear from the cursor position to the end of the screen. |
65 | CRTCTRL.ERASEEOL | ERASE TO END OF LINE | CHR(29), ^] | Control code to clear from the cursor position to the end of the current line. |
66 | CRTCTRL.NDFS | MOVE CURSOR RIGHT | CHR(28), ^\ | Control code to move the cursor one character to the right, non-destructively. |
67 | CRTCTRL.RLF | MOVE CURSOR UP | CHR(31), ^_ | Control code to move the cursor one line up. |
68 | CRTCTRL.BACKSPACE | BACKSPACE | CHR(8), ^H | Control code to move the cursor one character to the left, non-destructively. |
69 | CRTCTRL.FILLCOUNT | VERTICAL MOVE DELAY | 0 | Number of nulls to send for delay purposes after a vertical move. |
70 | CRTCTRL.CLEARLINE | ERASE LINE | CHR(0), ^@ | Control code to clear the entire line the cursor is on. |
71 | CRTCTRL.CLEARSCREEN | ERASE SCREEN | CHR(12), ^L | Control code to clear the entire screen. |
72:0 | CRTCTRL.PREFIXED[0] | PREFIXED[MOVE CURSOR UP] | FALSE | If TRUE, send CRTCTRL.ESCAPE before CRTCTRL.RLF. |
72:1 | CRTCTRL.PREFIXED[1] | PREFIXED[MOVE CURSOR RIGHT] | FALSE | If TRUE, send CRTCTRL.ESCAPE before CRTCTRL.NDFS. |
72:2 | CRTCTRL.PREFIXED[2] | PREFIXED[ERASE TO END OF LINE] | FALSE | If TRUE, send CRTCTRL.ESCAPE before CRTCTRL.ERASEEOL. |
72:3 | CRTCTRL.PREFIXED[3] | PREFIXED[ERASE TO END OF SCREEN] | FALSE | If TRUE, send CRTCTRL.ESCAPE before CRTCTRL.ERASEEOS. |
72:4 | CRTCTRL.PREFIXED[4] | PREFIXED[MOVE CURSOR HOME] | FALSE | If TRUE, send CRTCTRL.ESCAPE before CRTCTRL.HOME. |
72:5 | CRTCTRL.PREFIXED[5] | PREFIXED[DELETE CHARACTER] | FALSE | If TRUE, send CRTCTRL.ESCAPE before deleting a character. But no "delete a character" code is defined in SYSCOM, and this bit appears not to be used anywhere. |
72:6 | CRTCTRL.PREFIXED[6] | PREFIXED[ERASE SCREEN] | FALSE | If TRUE, send CRTCTRL.ESCAPE before CRTCTRL.CLEARSCREEN. |
72:7 | CRTCTRL.PREFIXED[7] | PREFIXED[ERASE LINE] | FALSE | If TRUE, send CRTCTRL.ESCAPE before CRTCTRL.CLEARLINE. |
74 (lo) & 75 (hi) | CRTINFO.HEIGHT | SCREEN HEIGHT | 24 | The height of the screen. |
76 (lo) & 77 (hi) | CRTINFO.WIDTH | SCREEN WIDTH | 79 or 80 | The width of the screen. |
78 | CRTINFO.UP | KEY TO MOVE CURSOR UP | CHR(15), ^O; or CHR(11), ^K | Key to move up a line in the editor. |
79 | CRTINFO.DOWN | KEY TO MOVE CURSOR DOWN | CHR(12), ^L; or CHR(10), ^J | Key to move down a line in the editor. |
80 | CRTINFO.LEFT | KEY TO MOVE CURSOR LEFT | CHR(8), ^H | Key to move one space left in the editor. |
81 | CRTINFO.RIGHT | KEY TO MOVE CURSOR RIGHT | CHR(21), ^U | Key to move one space right in the editor. |
82 | CRTINFO.EOF | KEY TO END FILE | CHR(3), ^C | Key to indicate end-of-file on the console. |
83 | CRTINFO.FLUSH | KEY FOR FLUSH | CHR(6), ^F | Key to cancel console output. |
84 | CRTINFO.BREAK | KEY FOR BREAK | CHR(0), ^@ | Key to terminate the current program. |
85 | CRTINFO.STOP | KEY FOR STOP | CHR(19), ^S | Key to suspend/resume console output. |
86 | CRTINFO.CHARDEL | KEY TO DELETE CHARACTER | CHR(8), ^H | Key to backspace. |
87 | CRTINFO.BADCH | NON-PRINTING CHARACTER | ? | Character to print instead of non-printing characters. Appears to be unused. |
88 | CRTINFO.LINEDEL | KEY TO DELETE LINE | CHR(24), ^X | Key to erase current input line. |
89 | CRTINFO.ALTMODE | EDITOR "ESCAPE" KEY | CHR(27), ESC | Key to cancel actions in editor. |
90 | CRTINFO.PREFIX | LEAD-IN FROM KEYBOARD | CHR(0), ^@ | If keys send two-byte sequences, lead-in character. |
91 | CRTINFO.ETX | EDITOR "ACCEPT" KEY | CHR(3), ^C | Key to accept actions in editor. |
92 | CRTINFO.ALPHA_LOCK | (none) | Apparently unused. | |
94:0 | CRTINFO.PREFIXED[0] | PREFIXED[KEY FOR MOVING CURSOR RIGHT] | FALSE | If TRUE, CRTINFO.RIGHT key sends CRTINFO.PREFIX code first. |
94:1 | CRTINFO.PREFIXED[1] | PREFIXED[KEY FOR MOVING CURSOR LEFT] | FALSE | If TRUE, CRTINFO.LEFT key sends CRTINFO.PREFIX code first. |
94:2 | CRTINFO.PREFIXED[2] | PREFIXED[KEY FOR MOVING CURSOR DOWN] | FALSE | If TRUE, CRTINFO.DOWN key sends CRTINFO.PREFIX code first. |
94:3 | CRTINFO.PREFIXED[3] | PREFIXED[KEY FOR MOVING CURSOR UP] | FALSE | If TRUE, CRTINFO.UP key sends CRTINFO.PREFIX code first. |
94:4 | CRTINFO.PREFIXED[4] | PREFIXED[NON-PRINTING CHARACTER] | FALSE | If TRUE, CRTINFO.BADCH sends CRTINFO.PREFIX code first. |
94:6 | CRTINFO.PREFIXED[6] | PREFIXED[KEY FOR STOP] | FALSE | If TRUE, CRTINFO.STOP key sends CRTINFO.PREFIX code first. The Apple Pascal CONSOLE:/SYSTERM: driver assumes that the STOP key is not prefixed. |
94:7 | CRTINFO.PREFIXED[7] | PREFIXED[KEY FOR BREAK] | FALSE | If TRUE, CRTINFO.BREAK key sends CRTINFO.PREFIX code first. The Apple Pascal CONSOLE:/SYSTERM: driver assumes that the BREAK key is not prefixed. |
95:0 | CRTINFO.PREFIXED[8] | PREFIXED[KEY FOR FLUSH] | FALSE | If TRUE, CRTINFO.FLUSH key sends CRTINFO.PREFIX code first. The Apple Pascal CONSOLE:/SYSTERM: driver assumes that the FLUSH key is not prefixed. |
95:1 | CRTINFO.PREFIXED[9] | PREFIXED[KEY TO END FILE] | FALSE | If TRUE, CRTINFO.EOF key sends CRTINFO.PREFIX code first. |
95:2 | CRTINFO.PREFIXED[10] | PREFIXED[EDITOR 'ESCAPE' KEY] | FALSE | If TRUE, CRTINFO.ALTMODE key sends CRTINFO.PREFIX code first. |
95:3 | CRTINFO.PREFIXED[11] | PREFIXED[KEY TO DELETE LINE] | FALSE | If TRUE, CRTINFO.LINEDEL key sends CRTINFO.PREFIX code first. |
95:4 | CRTINFO.PREFIXED[12] | PREFIXED[KEY TO DELETE CHARACTER] | FALSE | If TRUE, CRTINFO.CHARDEL key sends CRTINFO.PREFIX code first. |
95:5 | CRTINFO.PREFIXED[13] | PREFIXED[EDITOR "ACCEPT" KEY] | FALSE | If TRUE, CRTINFO.ETX key sends CRTINFO.PREFIX code first. |
Anyone looking at Apple Pascal's list of device numbers has probably noticed, and maybe wondered about, the missing device #3:.
It does exist, and can even be opened (under either the name
#3:
or GRAPHIC:
). There isn't much point in doing
so, though—reading from it causes an error, and writing to it just
throws away whatever was written. On an unpatched system, the only
action on it that actually does anything is UNITCLEAR(3)
, which
switches the screen to text mode (the code is just "LDA $C051;
RTS
"). Hooks exists for a SYSTEM.ATTACH driver
to accept writes to it (but not reads from it), or change the behavior of
UNITCLEAR(3)
.
So why is it there, and why does it have the suggestive name
GRAPHIC:
?
In UCSD Pascal II.0, it was originally meant for controlling the graphics
screen on a Terak 8510/A computer, and it's closely connected with the
SYSCOM^.MISCINFO.HAS8510A
bit described above under
All About MISCINFO. If HAS8510A
is
set, then it's assumed that UNITWRITE(3, BUFFER, LEN)
will
store the address of BUFFER
in the video address register, and
the value of LEN
in the video control register. Of course
that makes no sense on any computer other than a Terak 8510/A, which is
why HAS8510A
is always off in Apple Pascal. (Note also that
the nonstandard use of the UNITWRITE
paramaters means that on a
Terak 8510/A, you can't use WRITE
or WRITELN
on
the device, because they expect UNITWRITE
to do the normal
things with its parameters.)
On the Terak 8510/A, SYSTEM.PASCAL uses UNITWRITE(3, ...)
to
load SYSTEM.CHARSET into the video controller's character set RAM, and
display a graphic on the startup screen.
Except in the 48K runtime version, the Apple Pascal P-code interpreter
and device drivers reside mostly in the language card. The interpreter is
in bank 0 (the one you get by "LDA $C08B; LDA $C08B
"), and the
device drivers are mostly in bank 1 ("LDA $C083; LDA $C083
"),
between $D000 and $DFFF. The language card is always kept write-enabled.
Here are the addresses of some useful things. Most of the I/O-related addresses were added in Apple Pascal 1.1, and don't exist in 1.0. (Many of these are actually documented, but not all in one place.)
Note that some of these addresses are marked as only being available in some versions. This does not mean that they're free in other versions—most of them are used for other internal purposes in other versions.
Address (decimal) | Address (hex) | Size (bytes) | Contents | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | 54 | Free memory, available to machine-language routines for temporary variables. Also used heavily for temporary variables by the P-code interpreter and device drivers, so contents should not be expected to persist after a routine returns, or across a call to a device driver. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
80 | 50 | 2 | The interpreter's BASE pointer: Points to the MCSW ("markstack") record of the main program. Global variables are found by indexing off of this pointer. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
82 | 52 | 2 | The interpreter's MP pointer: Points to the MCSW ("markstack") record of the currently-running procedure or function. Paramaters, function return values, and local variables are indexed off of this pointer. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
84 | 54 | 2 | The interpreter's JTAB pointer: Points to the jump table of the currently-running procedure. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
86 | 56 | 2 | The interpreter's SEG pointer: Points to the segment dictionary of the segment containing the currently-running procedure. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
88 | 58 | 2 | The interpreter's IPC pointer: Points to the P-code instruction currently being interpreted. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
90 | 5A | 2 | The interpreter's NP pointer: Points to the current top of the
heap. Starts in low memory and grows upward when NEW is
used; trimmed back by RELEASE . |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
92 | 5C | 2 | The interpreter's KP pointer: Points to the current top of the program stack. Starts in high memory and grows downward as code segments are loaded and activation records created; trimmed back as procedures exit and segments are unloaded. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
94 | 5E | 2 | The interpreter's STRP pointer (128K system only): Points to a linked list of strings and packed arrays copied from the code segment in the auxiliary bank to the program stack in the main bank. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
96 | 60 | 2 | The interpreter's CODEP pointer (128K system only): The lowest address in auxiliary memory used by currently used by P-code routines. Starts in high memory and grows downward. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
98 | 62 | 2 | The interpreter's CODELOW pointer (128K system only): The lowest address in auxiliary memory that can be used for P-code routines. Normally contains 2048 ($800); can be increased to reserve auxiliary-bank memory. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
228 | E4 | 2 | RTPTR: Points to READTBL, a table of 8 2-byte entries, one for each device from #1: to #8:. Each entry points to the corresponding character device's read routine, or 0 if the device is not readable or not a character device. Each pointer points to an entry in the BIOS table (see below). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
230 | E6 | 2 | WTPTR: Points to WRITTBL, a table of 8 2-byte entries, one for each device from #1: to #8:. Each entry points to the corresponding character device's write routine, or 0 if the device is not writeable or not a character device. Each pointer points to an entry in the BIOS table (see below). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
232 | E8 | 2 | UDJVP: Points to UDJMPVEC, a table of 16 3-byte entries, one for each
user-defined device from #128: to #143:. Each entry is a JMP instruction
that jumps to the driver for the corresponding device. All entries
default to JMP 0 —the devices are only usable if drivers
for them have been loaded (by SYSTEM.ATTACH, for example). Apple supplied
a Profile hard drive driver that uses #128:, a mouse driver that uses
#129:, and a fancy console driver that uses #130:. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
234 | EA | 2 | DISKNUMP: Points to DISKNUM, a table of 12 (Apple Pascal 1.1) or 20 (Apple Pascal 1.2 and 1.3) 2-byte entries, one for each device from #1: to #12: or #20:. If an entry's high byte is 255 ($FF), the device is not a disk drive, or no driver for it is installed. If the high byte is 0, the low byte is the drive number for the built-in disk driver. If the high byte is any other value, the entry is a pointer (minus 1!) to the address of a loaded (by SYSTEM.ATTACH, for example) driver for the disk drive. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
236 | EC | 2 | JVBFOLD: Points to BIOS, a table of 21 3-byte entries, each a JSR instruction to one of the available device drivers. There's an entry for each permissible combination of READ, WRITE, STATUS, and INIT for the CONSOLE:/SYSTERM: driver, the PRINTER: driver, the REMIN:/REMOUT: driver, the GRAPHIC: driver, and the disk driver, plus a few utility routines. The interpreter does all its I/O by calling entries in this table. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
238 | EE | 2 | JVAFOLD: Points to BIOSAF, a table of 21 3-byte entries, each a JMP instruction to one of the available device drivers. These correspond exactly to the BIOS entries described above—each BIOS entry just bank-switches the language card to make the device drivers available, calls the corresponding BIOSAF entry, then bank-switches back to the interpreter and returns. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
244 | F4 | 1 | CH: Horizontal cursor position (0-79). (40-column mode only?) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
245 | F5 | 1 | CV: Vertical cursor position (0-23). (40-column mode only?) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
248 | F8 | 2 | FSYSCOM: Pointer to SYSCOM area. Refreshed whenever
UNITCLEAR(1) or UNITCLEAR(2) is called. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
945 | 3B1 | 78 or 64 | CONBUF: The keyboard typeahead buffer. Holds 78 bytes in Apple Pascal 1.0 and 1.1, 64 bytes in 1.2 and 1.3. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
-16630 | BF0A | 4 | CONCKVECTOR: A machine-language routine can JSR here to check the keyboard, and if a character is available, add it to the keyboard typeahead buffer. Should not be called from a device driver—it overwrites the device driver's return address. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
-16626 | BF0E | 1 | SCRMODE: 0 if the screen is in 40-column mode, 4 in 80-column mode. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
-16625 | BF0F | 1 | LFFLAG: If this byte's high bit is set, the PRINTER: device will not transmit line feed characters to the printer. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
-16623 | BF11 | 1 | EORCHAR: If this byte is 128 ($80), characters read from the keyboard will have their high bits set if the open-apple key or paddle/joystick button 0 is pressed. If this byte is 0, keyboard characters will always have their high bit clear. Only available in Apple Pascal 1.2 and 1.3. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
-16621 | BF13 | 2 | RANDL and RANDH: Randon number seed. Repeatedly incremented while waiting for keyboard input. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
-16618 | BF16 | 2 | BREAK: Pointer to a routine that force-quits the currently-running
program. Has the same effect as typing the BREAK key (normally ctrl-@)
on the keyboard. This pointer is refreshed whenever
UNITCLEAR(1) or UNITCLEAR(2) is called. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
-16616 | BF18 | 1 | RPTR: Offset into CONBUF of the character most recently removed from the buffer. If RPTR is equal to WPTR (see below), the buffer is empty (i.e., no unread characters are available), otherwise the next character can be read by adding 1 to RTPR (modulo the buffer size) and getting the byte at CONBUF+RPTR. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
-16615 | BF19 | 1 | WPTR: Offset into CONBUF of the character most recently added to the buffer. If WPTR is one less than RPTR (modulo the buffer size), then the buffer is full, otherwise a character can be added to the buffer by adding 1 to WPTR (modulo the buffer size) and storing the character at CONBUF+WPTR. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
-16612 | BF1C | 1 | SPCHAR: If the low bit of this byte is set, the 40-column text screen manipulation keys (ctrl-A, ctrl-K, ctrl-W) are not acted on, but returned as ordinary characters. If the next-to-lowest bit is set, the STOP, BREAK, and FLUSH keys (as found in MISCINFO, normally ctrl-S, ctrl-@, and ctrl-F) are not acted on, but returned as normal characters. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
-16611 | BF1D | 2 | IBREAK: Pointer to a routine that force-quits the currently-running program. Has the same effect as typing the BREAK key (normally ctrl-@) on the keyboard. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
-16609 | BF1F | 2 | ISYSCOM: Pointer to SYSCOM area. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
-16607 | BF21 | 1 | VERSION: Apple Pascal version number. 0=1.0, 2=1.1, 3=1.2, 4=1.3. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
-16606 | BF22 | 1 | FLAVOR: Indicates whether this is a full version of Apple Pascal or a
runtime version, and what runtime features are available. The meaning
depends on the Apple Pascal version: 1.0: Always 0. 1.1:
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
-16601 | BF27 | 8 | SLTTYPS: 8 bytes, one for each slot, giving the type of card in the
slot:
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
-16593 | BF2F | 2 | XITLOC: Pointer to the routine that handles the XIT instruction, which reboots the computer. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
-16591 | BF31 | 1 | IIEFLAG: Indicates what model of computer Pascal is running on. Only
available in Apple Pascal 1.2 and 1.3.
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
-12288 | D000 | 256 | (in the interpreter bank; not for 48K runtime version) A list of 128 2-byte pointers to the routines that handle P-code instructions 128 through 255 (P-codes 0 through 127, which just push constants on the stack, are recognized separately by the main interpreter loop). These routines don't return to their caller—they jump back to the main interpreter loop. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
-12032 | D100 | 82 | (in the interpreter bank; not for 48K runtime version) A list of 41 2-byte pointers to the routines that handle the P-code CSP (Call Special Procedure) instruction. There's one entry for each of the 41 possible CSP arguments. These routines don't return to their caller—they jump back to the main interpreter loop. |
Those last two address are interesting because they can be used to usurp
control of any P-code instruction (except SLDC
, which has no
list entry) or any CSP
procedure by patching a new address
into the appropriate list entry. Of course this must be done very
carefully—if the replacement routine doesn't do at least
what the original routine did, the results are likely to be unpleasant at
best. And if the replacement routine can be unloaded from memory by
anything short of a reboot, care must be taken to restore the original
routine pointer before unloading.
One way a replacement P-code routine could preserve the original functionality is by saving the original routine pointer before patching it, and then, after doing whatever special extra stuff it does, jumping to the saved address.
For a replacement that doesn't exit by jumping to the saved original
address, passing control back to the interpreter can be a challenge. You
need to point the IPC at the next instruction and jump back to the main
intperpreter loop, but you don't know the loop's address—it's
different in every version, and there's no easy way to find it. An
approach that I've had success with is to advance the IPC to the last byte
of the current instruction (i.e., one byte before the next instruction),
and JMP ($D0AE)
. $DOAE
is the pointer to the
handler for the P-code $D7
(NOP
), which advances
the IPC by one byte and jumps back to the main loop.
Of course any program that patches a P-code routine won't work on the 48K runtime version, because the pointer tables are in a different location.
Starting in Apple Pascal 1.1, a machine-language routine can do I/O by calling a device driver. The driver entry point must be looked up in the BIOS table (pointed to by the pointer at $EC, $ED)—the offset into the table and the calling parameters are described in the Device and Interrupt Support Tools Manual and the ATTACH-BIOS Document for Apple II Pascal 1.1.
Device driver calls may clobber the scratchpad memory locations from 0 through $35.
TIME
procedure to the system clock
of a IIGSOn the GSCLOCK disk is a program (with source code) which, on an Apple
IIGS, patches the TIME
procedure to return values based on the
IIGS system clock. Since the IIGS clock has a 1-second resolution, it
multiplies the clock time by 60, throwing away enough high-order bits to
make the result fit in 32 bits.
When a working TIME
is available, the Pascal compiler will
report the amount of time it took to compile a program, and the average
number of lines per second that it processed. Since the granularity of the
routine installed by GSCLOCK is 1 second instead of 1/60th of a second, the
compiler's report probably won't be very accurate for short programs.
The source code illustrates how to write a {$U-}
program, and
how to permanently (until the next reboot) protect machine code under
EMPTYHEAP
.
The GSCLOCK program does not try to fiddle with the
SYSCOM^.MISCINFO.HASCLOCK
bit. I'm not convinced that
turning it on wouldn't increase the risk of damaging a disk.
LLX > Neil Parker > Apple II > Pascal Secrets
Original: September 13, 2021
Modified: October 30, 2021—Added some additional notes, including lots
of details about SYSCOM
; added GSCLOCK disk image.
Modified: April 28, 2022—Noted that {$U-}
programs never
call the initialization routines of intrinsic units.
Modified: October 8, 2022—Fixed GSCLOCK to disable interrupts while
CPU is in native mode, because Pascal doesn't provide a sane native-mode
interrupt vector.
Modified: October 9, 2022—Fixed error in FINDGLOBALS
routine; improved wording in a couple of places.
Modified: February 27, 2023—Added sections about #3:
(GRAPHIC:
) and memory locations.
Modified: July 21, 2023—Noted that the "40 columns only" bit in the
boot disk directory only works in 1.2 and later, and that this probably
accounts for the value 255 in the SLTTYPS list. Noted that memory
locations used only in some versions are not free in other
versions.