DATE75, PDP-10, TULIP, and C

I recently saw a FaceBook question, "Remember Y2K? If you're old enough, you do." Sigh. Whippersnappers these days.

So I replied with:

DEC's "DATE-75" project dealt with a problem in their 15 bit field. The fundamental problems was that people didn't think hardware or operating systems would be around for more than 2^15th days. Wow, there's essentially nothing about it on the web, but I did find this:

... OTOH, the infamous "Date75" (the first of the Y2K-like bugs) hit all Digital Equipment Corporation users of the TOPS-10 operating system. There were only 15 bits used for the date, and they overflowed sometime early in 1975. We got another three bits by hijacking a different field of the file info field, but this did not apply to DECtapes, where there were no spare bits. Oh, wait a minute. The high-order bits of three other fields were not used, so they could be hijacked to form the three high-order bits of the date.

Every program that read and reported a file date had to be modified to support both of these quite different approaches. [And we used to have to walk barefoot through the snow to pick up our output].

I forget when that time started. Ah, looks like 1964. And a 12 bit field. And low-order bit. I don't know when computers started storing dates in file systems, but in 1964, 1975 looked to be a long way off. OTOH, a computer was expected to last 10 years or so, even then, million dollar systems were awfully expensive. Operating systems were developed for each architecture until Unix came along and disrupted that model. Actually, I do have code on the web that deals with DATE75, see http://wermenh.com/folklore/tulip.doc.html

I included an excerpt from that document, part of the assembler listing of the relevant code, with formatting mangled by FaceBook. After comments about "old school" and "we definitely don't code like that anymore," I offered to translate some into C and suggested it might not be more compact than PDP-10 assembler code. So I did, and spent a bit more time at it than I should, so instead of letting FaceBook mangle C code too, here is both the assembler and C code I to add. I might go back to include the first part of the directory listing subroutine later. I've added some comments along the way to describe things that people are not likely to understand these days. One thing first - this code is from an example of how to use the TULIP I/O package, non-PDP-10 instructions like DISIX (DIsplay SIXbit) and WDECI (Write DECimal Immediate) are "Unimplemented User Operations" that trigger a fault that is captured by user level code. They are the heart of TULIP and made I/O in assembler programming much, much easier. See this one page summary of the PDP-10 instruction set for help with that.

DIRSIZ==200                     ;SIZE OF DTA DIRECTORY BLOCK
DIRADR==^D100                   ;ADDRESS OF DIRECTORY BLOCK
MAXFIL==^D22                    ;MAX # OF FILES THAT WILL FIT ON A TAPE
TAPLEN==^D578                   ;# OF BLOCKS ON A DECTAPE
DIRBYT==0                       ;RELATIVE ADDR OF DIRECTORY BYTE MAP (5 BIT BYTES)
DIRFIL==^D83                    ;RELATIVE ADDR OF FIRST FILENAME
DIREXT==^D105                   ;RELATIVE ADDR OF EXTENSION/DATE WORD
DIRLBL==^D127                   ;RELATIVE ADDR OF LABEL WORD

        MOVEI   T4,1            ;MAKE INDEX INTO FILSIZ FOR BLOCKS USED
                                ;NO NEED TO SAVE P1 NOW, USE IT AS AOBJN WORD
DIRLOP: SKIPN   DIRFIL(P1)      ;DOES THIS FILE EXIST?
        JRST    DIRAOB          ;NO, TRY NEXT
        LDB     T1,[POINT 12,DIREXT(P1),35];GET LOW 12 BITS OF CREATION DATE
        MOVEI   T2,1            ;CHECK THE BYTE MAP FOR THE TOP 3 BITS
        TDNE    T2,DIRBYT(P1)
        IORI    T1,1B23         ;BRING UPTO 1985
        TDNE    T2,DIRBYT+MAXFIL(P1)
        IORI    T1,1B22         ;UPTO 2007
        TDNE    T2,DIRBYT+<2*MAXFIL>(P1)
        IORI    T1,1B21         ;UPTO 2051 (FOR THE PDP-10 IN THE SMITHSONIAN)
        DISIX   [[SIXBIT\%.% %  %#!\]
                 WSIX   6,DIRFIL(P1);FILE
                 WSIX   3,DIREXT(P1);AND EXTENSION
                 WDEC   3,FILSIZ(T4);THEN LENGTH
                 PUSHJ  P,DATTHN]   ;AND CREATION DATE
DIRAOB: MOVEI   T4,1(T4)        ;POINT TO NEXT FILE NUMBER
        AOBJN   P1,DIRLOP       ;AND LOOP FOR NEXT FILE
        POPJ    P,              ;OR RETURN WHEN DONE

MACRO-10 takes the source within the square brackets and replaces it with the address where it puts the relevant object code. So the DISIX instruction gets the address of memory that has the address of the SIXBIT string followed by instructions that usually print something. It's very much like printf, except that the arguments have the formatting information, and the arguments can be regular instructions or even subroutine calls. As you'll see below, I can't do some of this easily with printf(). Think of SIXBIT as ASCII without lower case. In those years, many terminals were upper case only, and TULIP had control characters to change the output from upper to lower case, so it was easy to write code that would display lower case strings on terminals that could. Also, note that six SIXBIT characters fit in the PDP-10's 36 bit word. They were pervasive in PDP-10 code, e.g. TOPS-10 file names were six characters long with a three character extension.

The PDP-10 instruction set includes a rich collection of medium strength instructions, AOBJN is one of them. It does "Add One to both halves of register and Jump if the result is Negative." I.e. it's often found at the bottom of loops. The high half of the register has the negative of a loop count, and the right half has either an index into an array, or an adress of the array element that was just processed.

;SUBROUTINE TO PRINT CURRENT DATE AND TIME AS
;       'ON <DATE> AT <TIME>'
;USES T1-T4

DATTIM: DISIX   [CPOPJ##,,[SIXBIT\&ON % AT %!\];PRINT DATE, TIME, THEN RETURN
                PUSHJ   P,DATPRT ;PRINT CURRENT DATE
                PUSHJ   P,TIMPRT];PRINT CURRENT TIME

This little routine (hmm, one instruction by some counting) isn't called from my sample. The address supplied to DISIX points to a 36 bit word split (by ',,') into high and low 18 bit values, the PDP-10 has many instructions that work with half words. The high half is the address of a "POPJ P," instruction, the standard subroutine return. The '##' merely told the assembler that the symbol might be defined in another source file. After DISIX does the output, it goes to that CPOPJ which saves us from wasting a word of memory to return from this subroutine.

;SUBROUTINE TO PRINT EITHER CURRENT DATE (ENTER AT DATPRT) OR DATE
;PASSED IN T1 (ENTER AT DATTHN). USES T1-T3

DATPRT: DATE    T1,             ;GET TODAY'S DATE
DATTHN: IDIVI   T1,^D<12*31>    ;T1_YEAR-64
        IDIVI   T2,^D<   31>    ;T2_MONTH-1, T3_DAY-1
        WDECI   2,1(T3)         ;DAY
        WNAME   MONTAB(T2)      ;.MONTH.
        WDECI   ^D64(T1)        ;AND YEAR
        POPJ    P,              ;AND RETURN


;SUBROUTINE TO PRINT TODAYS TIME. USES T1-T4, EXITS WITH LZEFLG OFF

TIMPRT: MSTIME  T1,             ;GET CURRENT TIME
        IDIVX   T1,^D<60*60*1000>;T1_HOURS
        IDIVX   T2,^D<   60*1000>;T2_MINUTES
        IDIVX   T3,^D<      1000>;T3_SECONDS, T4_THOUSANTHS
        TXO     F,LZEFLG        ;PRINT TIME WITH LEADING ZEROS
        DISIX   [[SIXBIT\%:%:%!\]
                 WDEC   2,T1    ;HOURS
                 WDEC   2,T2    ;MINUTES
                 WDEC   2,T3]   ;SECONDS
        TXZ     F,LZEFLG        ;TURN OFF AS PROMISED
        POPJ    P,              ;AND RETURN

DEFINE  MAKLST  (A)<
        IRP     A<SIXBIT/.'A'./>>
MONTAB: MAKLST  <JAN,FEB,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,NOV,DEC>

I hate, I hate, I hate the C preprocessor. When I first learned C, I expected that the preprocessor would grow into something broadly useful. Much to my everlasting disappointment, it never did. MAKLST is a MACRO that takes any number of arguments. IRP (Indefinte RePeat) takes each item in the list and, in this case, creates a word of data with a period on either side of the month, e.g.

    SIXBIT  /.JAN./
    SIXBIT  /.FEB./
    ...
    SIXBIT  /.DEC./

C code translation

There may be bugs. There are certainly "missing" things like forward references of subroutines. In some places I've done things in more of a C-style than we did in MACRO-10. Some are symbol names (MACRO-10 had a six character limit), some are just the way we write C code.

struct dir {             // Layout of the DECtape directory block
   int dirbyt[83];
   int dirfil[22];
   int dirext[22];
   int dirlbl;
} dirblk;

dirsub(struct dir *dirp, int *file_size) {
    int loop_count;      // This is the high half of P1, it's the negative of
                         // the loop count
    int dir_ind;         // The low half of P1 is the address of the directory
    			 // block, and the PDP-10 code increments in in the
			 // loop to access the elements in the block's arrays.
			 // We use a separate index here so we won't offend
			 // the C compiler with weird casts and increments.
    int file_num;        // Register T4, index to a file size table

    for (loop_count = -MAXFIL, dir_ind = 0, file_num = 1;
         loop_count;
         loop_count++, dir_ind++) {
        if (dir->dirfil[dir_ind]) {
            t1 = dirp->dirext[dir_ind] & 07777;   // Low 12 bits of creation date
            if (dirp->dirbyt[dir_ind] & 1)
                t1 |= 010000;                     // Bring upto 1985
            if (dirp->dirbyt[MAXFIL + dir_ind] & 1)
                t1 |= 020000;                     // Upto 2007
            if (dirp->dirbyt[2*MAXFIL + dir_ind] & 1)
                t1 |= 040000;                     // Upto 2051 (for the PDP-10 in the Smithsonian)
            printf("%6s.%3s. %3d  %s\n", dir->dirfil[dir_ind], \
	           dir->dirext[dir_ind], file_size[file_num], date_then(t1));
        }
    }
}


/*
 * Subroutine to print a date encoded as (((year - 1964) * 12 + (month - 1)) * 31) + day - 1.
 * We can't call printf recursively as a printf argument, so we have to settle for
 * returning a sprintf string that is generated while arguments are evaluated.
 */
char *date_print() {
    return date_then(DATE_SYSCALL());
}

char *date_then(int date) {
    int day, month, year;
    char buff[10];       // Overflows in year 2000!  Y2K bug!

    day = date % 31 + 1;
    date = date / 31;
    month = date % 12;
    year = date / 12 + 64;
    return sprintf(buff, "%2d%s%d", day, montab[month], year);
}

/* This horror is about the best I can do with cpp.  Of course, C programmers don't do this. */
#define x(month) "." #month ".",

#define MAKELIST \
    x(Jan) \
    x(Feb) \
    x(Mar) \
    x(Apr) \
    x(May) \
    x(Jun) \
    x(Jul) \
    x(Aug) \
    x(Sep) \
    x(Oct) \
    x(Nov) \
    x(Dec)

char *montab[] = { MAKELIST };


Contact Ric Werme or return to his home page.

Written 2021 Jan 13, last updated 2024 Mar 29.