LLX > Neil Parker > Apple II > Pascal Secrets

Undocumented Secrets of Apple Pascal

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.

Reserved Words

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).

Builtins

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)
This is an alternate name for REWRITE(f, name).
OPENOLD(VAR f: FILE OF whatever; name: STRING)
This is an alternate name for RESET(f, name), except that the name argument may not be omitted.
TIME(VAR hi, lo: INTEGER)
This procedure reads the system clock. The clock is assumed to return a 32-bit value in units of 1/60th of a second, representing the time elapsed since some arbitrary point in the past (often, but not necessarily, the most recent power-on or reboot)—the high 16 bits are returned in the first argument and the low 16 bits in the second. Since most Apple IIs have no clock, Apple Pascal normally returns zero in both arguments, but if a clock card is available, software might be provided to patch the system to return meaningful numbers. (The above-mentioned p-Source includes an example of a SYSTEM.ATTACH driver that does this for a Mountain Computer clock card...alas, it only works with Apple Pascal 1.1. Another example, for the Apple IIGS built-in clock, can be downloaded from the Downloads section below.)

Compiler Options

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:

Debug, {$D+}
Turning this option on causes the compiler to emit a P-code 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-}.
Flip, {$F+}
If this option appears at the beginning of the source code, the compiler will issue P-code with the opposite byte order from that normally used by the host computer...on the Apple II, that means to issue big-endian P-code instead of little-endian P-code. Apple Pascal cannot run such code. The default state is {$F-}.
Tiny, {$T+}
If this option appears at the beginning of the source code, the compiler will omit some of its built-in identifiers from the symbol table. This makes more memory available for compiling, but it means the missing built-ins can't be used by the source code being compiled. In UCSD Pascal II.0, the omitted built-ins were READLN, PRED, SUCC, SQR, UNITREAD, INSERT, DELETE, COPY, POS, SEEK, GET, PUT, PAGE, STR, and GOTOXY; the list in Apple Pascal is probably similar. The default state is {$T-}.

Assembler Directives

The assembler recogizes two directives that aren't documented in the manual:

Absolute section, .ASECT
This has an effect similar to the 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.
Program section, .PSECT
This directive ends a .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.

Machine Types

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.)

The Truth About {$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:

Turning on {$U-} does this instead:

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.

SYSTEM.PASCAL's Global Variables

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.

Accessing SYSTEM.PASCAL's Globals Without {$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;

Where's My Variable?

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.

All About MISCINFO

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.

Device #3:, GRAPHIC:

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.

Memory, and Addresses of Useful Things

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:
Value Version Language Card? Sets? Floatint Point?
1 Full Yes Yes Yes
2 Runtime No Yes Yes
3 Runtime No No Yes
4 Runtime No Yes No
5 Runtime No No No
6 Runtime Yes Yes Yes
7 Runtime Yes No Yes
8 Runtime Yes Yes No
9 Runtime Yes No No
1.2 and 1.3—a bit mask:
Bit Meaning
0 0=full version, 1=runtime
1 1 if floating point support not included
2 1 if set support not included
6, 5 00=64K, 01=48K, 10=128K, 11=reserved
7 1 if console output directed to graphics screen
-16601 BF27 8 SLTTYPS: 8 bytes, one for each slot, giving the type of card in the slot:
Value Meaning
0 Empty slot
1 Unrecognized card
2 Disk II
3 Apple Communications Interface Card
4 Apple Serial Interface Card (the first version, not the Super Serial Card)
5 Parallel Card
6 Pascal-protocol firmware card
7 Block device with ProDOS interface (Apple Pascal 1.3 only)
255 ? Pascal-protocol firmware card, but only in slot 3 and only if the "40 columns only" bit is set in the directory of the boot disk (Apple Pascal 1.2 and 1.3 only)
-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.
Bit Meaning
0 1 if an 80-column card is available.
1 1 if 128K is available.
7, 6 00=Apple II or Apple II+, 10=Apple IIe, 11=Apple IIc. 01 was reserved (but never used) for other Apple II models. Only Apple Pascal 1.3 detects the Apple IIc.
-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.

Downloads

On 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.