\datethis @*Introduction. This program determines the number of paths that run from the source vertex to sink vertices at each level of a given periodic directed acyclic graph. Although it was designed originally to process the output of {\mc POLYENUM}, it might also be useful for processing the output of other programs. The input file consists of 32-bit words, where the top 5 bits are an ``opcode,'' the next bit is a ``tag,'' and the remaining 26 bits are the ``address.'' Opcode 0 means: Choose a new vertex as the current vertex from which successive arcs will emanate. The address is the vertex number. Opcodes 1--30 mean: Create an arc of this multiplicity from the current vertex to another vertex specified by the tag and the address. If the tag is~1, the address gives the serial number of the destination vertex; all vertices are given a serial number, starting with~1. Address 0 is, however, legal; it denotes the sink vertex on the current level. (In the program {\mc POLYENUM}, the sink vertex on level~$m$ represents all polyominoes that have exactly $m$ cells.) If the tag is~0, the address has the form |(a<<8)+b|, where $a$ and $b$ are the ``birth'' and ``death'' levels of a new vertex; this new vertex is assigned the next serial number. (Levels will be explained momentarily.) For convenience in English, we call $a$ and $b$ the birthdate and deathdate. Each new vertex defined on level~$m$ must have birth and death dates satisfying $m @@; @@; @@; main(int argc, char *argv[]) { @; @; @; @; @; } @ The program checks frequently that everything in the input file is legitimate. If not, too bad; the run is terminated (although a debugger can help diagnose nonobvious problems). Extensive checks like this have helped the author to detect errors in the program as well as errors in the input. @= void panic(char *mess) { fprintf(stderr,"%s!\n",mess); exit(-1); } @ Several gigabytes of input might be needed, so the input file name will be extended by \.{.0}, \.{.1}, \dots, just as in {\mc POLYENUM}. @d nmax 30 /* at most this many levels */ @d bad(k,v) sscanf(argv[k],"%d",&v)!=1 @= if (argc!=6 || bad(1,n) || bad(2,verts) || bad(3,clones) || bad(4,arcs)) { fprintf(stderr,"Usage: %s n verts clones arcs infilename\n",argv[0]); exit(0); } if (n<=0 || n>nmax) { fprintf(stderr,"Sorry, n must be between 1 and %d; try again",nmax); panic(""); } if (verts= int n; /* the maximum level */ int verts; /* upper bound on number of proto-vertices */ int clones; /* upper bound on total number of vertices */ int arcs; /* upper bound on memory needed to store the proto-arcs */ @*Input. We might as well start small, with the basic routines that are needed to read instructions from the input file(s). As soon as $2^{30}$ bytes of data have been read from file \.{foo.0}, we'll turn to file \.{foo.1}, etc. @d filelength_threshold 0x10000000 /* in tetrabytes */ @= FILE* in_file; /* the input file */ unsigned int buf; /* place for binary input */ int words_in; /* the number of tetrabytes input from the current input file */ int file_extension; /* the number of GGbytes input */ char *base_name, filename[100]; @ @= void open_it() { sprintf(filename,"%.90s.%d",base_name,file_extension); in_file=fopen(filename,"rb"); if (!in_file) { fprintf(stderr,"I can't open file %s",filename); panic(" for input"); } words_in=0; } @ @= void close_it() { if (fclose(in_file)!=0) panic("I couldn't close the input file"); printf("[%d bytes read from file %s.]\n",4*words_in,filename); } @ @= int get_it() { if (words_in==filelength_threshold) { close_it(); file_extension++; open_it(); } words_in++; return fread(&buf,sizeof(unsigned int),1,in_file)==1; } @ @= open_it(); @* Arithmetic. The integer numbers involved here can get very large, but we assume that one octabyte (64 bits) is enough to hold them. My computer deals directly with 32-bit words, so I represent an octabyte as a pair of unsigned ints: @= typedef struct { unsigned int h,l; /* high-order and low-order halves */ } octa; /* two tetrabytes make one octabyte */ @ @= octa oplus(octa y,octa z) /* compute $(y+z)\bmod2^{64}$ */ { octa x; x.h=y.h+z.h, x.l=y.l+z.l; if (x.l= void print_octa(octa o) { register unsigned int hi=o.h, lo=o.l, r, t; register int j; char dig[20]; if (lo==0 && hi==0) printf("0"); else { for (j=0;hi;j++) { /* 64-bit division by 10 */ r=((hi%10)<<16)+(lo>>16); hi=hi/10; t=((r%10)<<16)+(lo&0xffff); lo=((r/10)<<16)+(t/10); dig[j]=t%10; } for (;lo;j++) { dig[j]=lo%10; lo=lo/10; } for (j--;j>=0;j--) printf("%c",dig[j]+'0'); } } @*Data structures. Basic information about a proto-vertex $v$ is kept in the vertex node |vtable[v]|. If this vertex has clones $v_a$, $v_{a+1}$, \dots,~$v_b$, the number of paths from the source to each clone is accumulated in $b-a+1$ consecutive entries of |ctable|. The sink vertices for each level are considered to be clones of |vtable[0]|. @s arc_struct int @= typedef struct vertex_struct { char birth; /* the birthdate $a$ */ char death; /* the deathdate $b$ */ char weight; /* the level difference from all predecessors */ char cleared; /* has the last clone been processed? */ octa *base; /* the counter for the first clone, $v_a$ */ struct vertex_struct *next; /* the vertex to process next */ struct arc_struct *arcs; /* the first arc from this vertex */ } vert; /* 20 bytes per vertex, plus 8 bytes per clone */ @# typedef struct arc_struct { unsigned int tip; /* multiplicity and destination */ struct arc_struct *next; /* the next arc from the same vertex */ } arc; /* 8 bytes per arc */ @ @= vert *vtable; /* master table for all proto-vertices */ vert *bad_vert; /* address at the end of |vtable| */ vert *vtable_ptr; /* the first unused space in |vtable| */ octa *ctable; /* counters for all vertex clones */ octa *bad_count; /* address at the end of |ctable| */ octa *ctable_ptr; /* the first unused space in |ctable| */ arc *atable; /* current pool of arc information */ arc *bad_arc; /* address at the end of |atable| */ arc *atable_ptr; /* the first unused space in |atable| */ @ @= verts++; /* make room for the proto-sink vertex */ vtable=(vert*)calloc(verts,sizeof(vert)); if (!vtable) panic("I can't allocate the vertex table"); bad_vert=vtable+verts; clones+=n; /* make room for the sink clones */ ctable=(octa*)calloc(clones,sizeof(octa)); if (!ctable) panic("I can't allocate enough space for counters"); bad_count=ctable+clones; atable=(arc*)calloc(arcs,sizeof(arc)); if (!atable) panic("I can't allocate that much space for arcs"); bad_arc=atable+arcs; vtable[0].base=ctable; vtable[0].birth=1; vtable[0].death=n; vtable_ptr=vtable+1; ctable_ptr=ctable+n; /* the |n| sink vertices occur at the start of |ctable| */ atable_ptr=atable; @ When a vertex is first mentioned, we place it in a |pending| list of all vertices having the same birthdate; the |next| field will then point to the previous vertex sharing that date. After level |m| has been processed, the list of all vertices born on that level is scanned to make sure that each one had outgoing arcs. Subsequently the |next| fields are used to link together all proto-vertices that are still alive. @ @= vert *pending[nmax+1]; /* lists of vertices to be defined on future levels */ vert *alive; /* list of all vertices still kicking */ @ When an |arc| is no longer needed, it is placed on the |avail| list, from whence it can be reused. Here is a subroutine that returns a fresh |arc|, using a familiar allocation ritual: @= arc *new_arc() { register arc*a; if (avail) a=avail, avail=a->next, a->next=NULL; else if (atable_ptr==bad_arc) panic("Arc memory overflow"); else a=atable_ptr++; return a; } @ @= arc *avail; /* top of the stack of recycled |arc| nodes */ @* The basic execution cycle. This program is structured like a simple form of machine simulator, reading instructions one by one. Luckily, the basic control loop is quite simple, because we aren't simulating a stored-program computer. (On the other hand, we do store arc information that is used repeatedly, so the complexity shows up elsewhere.) The program will |goto done| when level |n| is reached. @= while (1) { @; switch(opcode) { case new_pred_code: case new_level_code: if (u) @; if (opcode==new_level_code) @@; else @; break; default: @; } loc++; } done: @; @ @d address_mask ((1<<27)-1) @= if (!get_it()) panic("Premature end of input"); opcode=buf>>27; address=buf&address_mask; if (verbose) print_inst(0); @ @= int verbose=0; /* set nonzero when debugging */ int loc; /* the number of instructions processed */ unsigned int buf; /* the current instruction from |in_file| */ int opcode; /* its operation code */ int address; /* its address field, including the tag */ int mm; /* global copy of local variable |m| */ @ Sometimes we want to see an instruction in symbolic form. Each proto-vertex is identified by its decimal serial number, starting with~1 (not starting with~0 as in the binary file). Of course this serial number is not the same as the hexadecimal number of the corresponding pattern in {\mc POLYENUM}; we haven't been told that number. But the verbose outputs of {\mc DAGENUM} and {\mc POLYENUM} are otherwise quite similar. @= void print_inst(int err) { register int cur_verts=vtable_ptr-vtable, taddress=address^tag_bit; register FILE* f=err? stderr: stdout; switch (opcode) { case new_level_code: fprintf(f,"New level %d\n",address);@+break; case new_pred_code: fprintf(f,"%d:%d\n",address, address"); goto common_ending; default: fprintf(f,"-%d>",opcode); common_ending: if (address>8,address&0xff); else fprintf(f,"%d:%d\n",taddress, taddress= void ipanic(char *mess) { if (!verbose) fprintf(stderr,"Problem at instruction %d:\n",loc); print_inst(1); /* show context before dying */ panic(mess); } @*Creating vertices and arcs. As this program runs, |m| will be the current level, |u| will be the current (proto-)vertex from which arcs are being generated, and |v| will be the current (proto-)vertex to which a new arc is being directed. @= register int m=0; /* the cloning level */ register vert *u=NULL,*v=NULL; /* current vertices of interest */ @ @= if (address>=tag_bit) { v=vtable+(address^tag_bit); if (v>=vtable_ptr) ipanic("Vertex number out of range"); if (m+v->weight>v->death) ipanic("Vertex is already dead"); }@+else { if (vtable_ptr==bad_vert) ipanic("Vertex memory overflow"); v=vtable_ptr++; v->birth=address>>8, v->death=address&0xff; if (v->birth<=m) ipanic("Birthdate too early"); if (v->death>n) ipanic("Deathdate too late"); if (v->deathbirth) ipanic("Death before birth"); v->weight=v->birth-m; v->base=ctable_ptr; ctable_ptr+=v->death-v->birth+1; if (ctable_ptr>bad_count) ipanic("Counter memory overflow"); v->next=pending[v->birth], pending[v->birth]=v; } @; @ The value |u=NULL| represents the source vertex, which is legal and relevant only on level zero. The value |v=vtable| represents the sink vertex on the current level. Incidentally, multiple arcs are allowed, in case we need to deal with graphs that have multiplicities exceeding~30. @= if (u) { buf=(opcode<<27)+(v-vtable); s=new_arc(); s->tip=buf; s->next=u->arcs, u->arcs=s; } else { if (m) ipanic("No predecessor specified"); if (v==vtable) ipanic("There is no sink on level zero"); if ((*v->base).l) ipanic("Initial arc not to a new vertex"); (*v->base).l=opcode; } /* the code now falls through to the end of the master opcode switch */ @ @= register arc *r,*s; /* current arcs of interest */ @ @= { u=vtable+address; if (u>=vtable_ptr) ipanic("Predecessor vertex not yet defined"); if (u->birth!=m) ipanic("This vertex has the wrong birthdate"); } @ @= { @; if (m) { print_octa(ctable[m-1]);@+printf(" paths to sink[%d]\n",m); } if (address!=++m) ipanic("Level number is out of sync"); mm=m; printf("Entering level %d (%d)\n",m,atable_ptr-atable); if (m==n) goto done; u=NULL; } @* Accumulating path counts. Now we're ready for the main action, which is to add $c$ times the counter for $u_m$ to the counter for $v_{m+w}$ for all arcs that run from $u$ to $v$. The multiplicity $c$ is usually 1 and almost never very large. Therefore we don't actually multiply, we simply use addition to maintain a table of multiples of |u|'s counter, building that table on the fly as needed. @= octa mcount[31]; /* |mcount[c]=c*mcount[1]| */ int mcount_ptr; /* the number of valid entries of |mcount| */ int recycling; /* is |u| is about to die? */ @ The algorithm would be quite simple if we didn't take the trouble to recycle arcs that are no longer needed. But hey, that makes things interesting. @= { mcount[1]=*(u->base+m-u->birth); mcount_ptr=1; recycling=(u->death==m); if (!u->arcs) ipanic("Vertex has no successor arcs"); for (r=NULL,s=u->arcs;s;) @; if (recycling) r->next=avail, avail=r, u->cleared=1, u->arcs=NULL; } @ @= {@+register octa *o; register int k; k=s->tip>>27; /* the multiplicity */ while (k>mcount_ptr) mcount[mcount_ptr+1]=oplus(mcount[1],mcount[mcount_ptr]),mcount_ptr++; v=vtable+(s->tip&address_mask); o=v->base+m+v->weight-v->birth; *o=oplus(mcount[k],*o); if (!recycling && m+v->weight==v->death) { /* recycle this arc only */ if (!r) u->arcs=s->next, s->next=avail, avail=s, s=u->arcs; else r->next=s->next, s->next=avail, avail=s, s=r->next; }@+else r=s,s=s->next; } @ @= @; @; @; @ @= if (pending[m]) { for (u=pending[m]; u; v=u,u=u->next) if (!u->cleared && !u->arcs) ipanic("Missing arcs on previous level"); v->next=alive; } @ @= for (u=alive; u; u=u->next) @; @ @= if (pending[m]) alive=pending[m]; for (v=NULL,u=alive;u;u=u->next) if (u->cleared) { if (!v) alive=u->next; else v->next=u->next; }@+else if (!u->arcs) ipanic("An old vertex became a sink"); else v=u; @ @= for (u=pending[m];u;u=u->next) ctable[m-1]=oplus(*u->base,ctable[m-1]); for (u=alive;u;u=u->next) { s=u->arcs; if (!s || s->tip!=(1<<27) || s->next) ipanic("Vertex alive at level n should point to sink[n]"); ctable[m-1]=oplus(*(u->base+m-u->birth),ctable[m-1]); } print_octa(ctable[m-1]); printf(" paths to sink[%d]\n",m); @ @= printf("All done! (%d arc slots needed)\n",atable_ptr-atable); close_it(); @*Index.