diff --git a/arch/z8001/code.c b/arch/z8001/code.c new file mode 100644 index 000000000..b868ceacb --- /dev/null +++ b/arch/z8001/code.c @@ -0,0 +1,589 @@ +/* + * Copyright (c) 2026 Michal Pleban. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* + * pass1 code generation for the Zilog Z8001 / Coherent target. + */ + +#include "pass1.h" + +#include +#include +#include /* dup, dup2, close, fileno (output-capture peephole) */ + +#ifndef LANG_CXX +#define NODE P1ND +#undef NIL +#define NIL NULL +#define talloc p1alloc +#define tcopy p1tcopy +#define sap sss +#define n_type ptype +#undef n_ap +#define n_ap pss +#undef n_df +#define n_df pdf +#endif + +/* + * Print out assembler segment name. + * Coherent uses its own segment directives instead of .text/.data/.bss. + */ +void +setseg(int seg, char *name) +{ + switch (seg) { + case PROG: name = "\t.shri"; break; /* code segment */ + case STRNG: name = "\t.strn"; break; /* string literals */ + case DATA: + case LDATA: + case RDATA: name = "\t.prvd"; break; /* private (initialized) data */ + case UDATA: name = "\t.bssd"; break; /* BSS (uninitialized) data */ + default: + cerror("setseg: unknown segment %d", seg); + } + printf("%s\n", name); +} + +/* + * Emit alignment directive. + * Z8001 only has .even/.odd; everything wider than a byte needs .even. + */ +void +defalign(int al) +{ + if (al > ALCHAR) + printf("\t.even\n"); +} + +/* + * Define everything needed to print out some data (or text): + * segment switch, global visibility, and the symbol label. + */ +void +defloc(struct symtab *sp) +{ + TWORD t; + char *n; + int s; + + t = sp->stype; + s = ISFTN(t) ? PROG : + ISCON(cqual(t, sp->squal)) ? RDATA : DATA; + + if (s != lastloc) { + setseg(s, NULL); + lastloc = s; + } + + n = getexname(sp); + if (sp->sclass == EXTDEF) + printf("\t.globl\t%s\n", n); + + if (sp->slevel == 0) + printf("%s:\n", n); + else + printf(LABFMT ":\n", sp->soffset); +} + +/* + * Struct/union return, hidden-pointer ABI (invented here: the Coherent + * K&R compiler has no struct-by-value at all, so this only has to be + * self-consistent). The caller reserves a buffer in its frame and + * pushes its address LAST, so it sits at the first-argument slot + * (+ARGINIT); all declared arguments are one pointer higher (bfcode + * shifts them). On "return expr", clocal(FORCE) puts &expr in rr0; + * efcode copies *rr0 into the caller's buffer and returns the buffer + * address in rr0. + */ +int strtemp; /* pass1 temp holding the incoming hidden pointer */ + +void +efcode(void) +{ + extern NODE *cftnod; + NODE *p, *q; + TWORD t; + int i; + + if (cftnsp->stype != STRTY+FTN && cftnsp->stype != UNIONTY+FTN) + return; + t = DECREF(cftnsp->stype); + + if (cftnod != NIL) { + /* *(hidden pointer) = *(rr0): copy the returned value out. + * If the function never executed "return expr" (cftnod + * unset), rr0 is garbage - skip the copy. + * + * rr0 must first move into a temp: rr0 cannot be an + * indirect base on the Z8001, and a literal REG rr0 node + * as the STASG source would end up as "ldirb ...,(rr0)". */ + q = block(REG, NIL, NIL, INCREF(t), + cftnsp->sdf, cftnsp->sap); + slval(q, 0); + regno(q) = RR0; + p = tempnode(0, INCREF(t), cftnsp->sdf, cftnsp->sap); + i = regno(p); + ecomp(buildtree(ASSIGN, p, q)); + + q = tempnode(i, INCREF(t), cftnsp->sdf, cftnsp->sap); + q = buildtree(UMUL, q, NIL); + p = tempnode(strtemp, INCREF(t), cftnsp->sdf, cftnsp->sap); + p = buildtree(UMUL, p, NIL); + ecomp(buildtree(ASSIGN, p, q)); + } + + /* return the caller's buffer address in rr0 */ + p = block(REG, NIL, NIL, INCREF(t), cftnsp->sdf, cftnsp->sap); + slval(p, 0); + regno(p) = RR0; + q = tempnode(strtemp, INCREF(t), cftnsp->sdf, cftnsp->sap); + ecomp(buildtree(ASSIGN, p, q)); +} + +/* + * Beginning-of-function code. + * Z8001 ABI is pure stack-based (no argument registers), so apart from + * the struct-return hidden pointer there is nothing to do here unless + * we are optimising args into temps. + */ +void +bfcode(struct symtab **sp, int cnt) +{ + struct symtab *sp2; + NODE *n, *p; + int i; + + if (cftnsp->stype == STRTY+FTN || cftnsp->stype == UNIONTY+FTN) { + /* + * The hidden pointer occupies the first-argument slot, so + * every declared argument really sits one pointer (32 bits) + * higher than pass 1 assigned it. Fix the offsets, then + * save the hidden pointer in a temp for efcode(). + */ + for (i = 0; i < cnt; i++) + sp[i]->soffset += SZPOINT(CHAR); + n = tempnode(0, INCREF(CHAR), 0, 0); + p = block(OREG, NIL, NIL, INCREF(CHAR), 0, 0); + slval(p, ARGINIT/SZCHAR); + regno(p) = FPREG; + strtemp = regno(n); + ecomp(buildtree(ASSIGN, n, p)); + } + + /* + * A char argument is pushed as a word; on big-endian its value + * lives in the LOW byte of that slot, one byte past the slot + * start. Adjust the offset once here so every access - including + * ¶m - sees the true byte address (clocal used to add the + * correction per-access, which the stref/&-able rewrite can't). + */ + for (i = 0; i < cnt; i++) { + TWORD t = sp[i]->stype; + + if (t == CHAR || t == UCHAR || t == BOOL) + sp[i]->soffset += SZINT - SZCHAR; + } + + if (xtemps == 0) + return; + + for (i = 0; i < cnt; i++) { + if (sp[i]->stype == STRTY || sp[i]->stype == UNIONTY || + cisreg(sp[i]->stype) == 0) + continue; + sp2 = sp[i]; + n = tempnode(0, sp[i]->stype, sp[i]->sdf, sp[i]->sap); + n = buildtree(ASSIGN, n, nametree(sp2)); + sp[i]->soffset = regno(n->n_left); + sp[i]->sflags |= STNODE; + ecomp(n); + } +} + +/* + * Called just before compiler exits. + * + * NB: programs that print floating point must be linked with + * "ld -u _dtoa_" so the real _dtefg printf formatter (libc + * crt/dtefg.o) is pulled in instead of the "No floating point!" + * dummy (gen/sdtoa.o). That is a link-time policy - the native + * "cc -f" flag - and deliberately NOT emitted by the compiler: + * it would drag the formatter into every FP program whether or + * not it prints, and would tie generated code to one libc layout. + */ +/* + * Post-emit stack-cleanup peephole. + * + * A call's caller-cleanup "inc r15,$K" immediately followed by the next + * call's first argument push of exactly K bytes is a wasteful SP round-trip: + * the inc frees K bytes and the push re-allocates the same K, so the pair + * collapses to a single in-place store - drop the inc and turn that push + * into a store to (rr14). SP is unchanged, the value lands in the same slot, + * and any further pushes proceed normally from the unchanged SP: + * + * inc r15,$2 ; push (rr14),x ; push (rr14),y + * -> store (rr14),x ; push (rr14),y + * + * The store re-provides the argument from the (callee-saved, preserved) + * register, so this stays correct even if the first callee overwrote its own + * argument slot; only a *register* push source qualifies (a memory/immediate + * push has no legal mem<-mem / mem<-imm store form). A label between the two + * lines is a separate line, so the required adjacency naturally respects + * basic-block boundaries (a jump target between them blocks the collapse). + * + * All pass-2 assembly is printf'd straight to stdout with no instruction or + * line buffer to peephole over, so the whole compilation's stdout is captured + * to a temp file between bjobcode() and ejobcode() (which bracket every + * emission in the combined ccom) and streamed back through the line filter. + * z8001-only - no shared MIP changes. If capture cannot be set up the output + * is left direct (no peephole), never wrong. + */ +static int argcse_savefd = -1; +static FILE *argcse_cap; + +/* SRC (the text after "(rr14),") a plain word/long register? */ +static int +argcse_isreg(const char *p, int longp) +{ + if (longp) { + if (p[0] != 'r' || p[1] != 'r') + return 0; + p += 2; + } else { + if (p[0] != 'r') + return 0; + p += 1; + } + if (*p < '0' || *p > '9') + return 0; + while (*p >= '0' && *p <= '9') + p++; + return *p == '\0'; +} + +static void +argcse_filter(FILE *in, FILE *out) +{ + char prev[4096], cur[4096]; + int haveprev = 0, prevk = 0; + + while (fgets(cur, sizeof(cur), in) != NULL) { + char *e; + const char *src; + int longp, matched = 0; + + /* strip trailing CR/LF; a single \n is re-added on output */ + e = cur + strlen(cur); + while (e > cur && (e[-1] == '\n' || e[-1] == '\r')) + *--e = '\0'; + + if (haveprev) { + src = NULL; + longp = 0; + if (strncmp(cur, "\tpushl\t(rr14),", 14) == 0) { + src = cur + 14; + longp = 1; + } else if (strncmp(cur, "\tpush\t(rr14),", 13) == 0) { + src = cur + 13; + longp = 0; + } + if (src != NULL && prevk == (longp ? 4 : 2) && + argcse_isreg(src, longp)) { + fprintf(out, "\t%s\t(rr14),%s\n", + longp ? "ldl" : "ld", src); + haveprev = 0; /* both lines consumed */ + matched = 1; + } + } + if (matched) + continue; + + if (haveprev) + fprintf(out, "%s\n", prev); + + if (strncmp(cur, "\tinc\tr15,$", 10) == 0 || + strncmp(cur, "\tadd\tr15,$", 10) == 0) { + prevk = atoi(cur + 10); + strcpy(prev, cur); + haveprev = 1; + } else { + fprintf(out, "%s\n", cur); + haveprev = 0; + } + } + if (haveprev) + fprintf(out, "%s\n", prev); +} + +void +ejobcode(int flag) +{ + if (argcse_cap != NULL) { + fflush(stdout); + dup2(argcse_savefd, fileno(stdout)); /* restore real stdout */ + close(argcse_savefd); + argcse_savefd = -1; + rewind(argcse_cap); + argcse_filter(argcse_cap, stdout); /* peephole -> real out */ + fclose(argcse_cap); + argcse_cap = NULL; + fflush(stdout); + } +} + +/* + * Called at the very beginning of compilation. + * Set up assembler type names for Coherent's assembler syntax: + * .byte - 8-bit + * .word - 16-bit (int and short are both 16-bit on Z8001) + * .long - 32-bit + */ +void +bjobcode(void) +{ + extern char *asspace; + extern int clregs[]; + + astypnames[SHORT] = astypnames[USHORT] = "\t.word"; + astypnames[INT] = astypnames[UNSIGNED] = "\t.word"; + astypnames[LONG] = astypnames[ULONG] = "\t.long"; + asspace = "\t.blkb"; + + /* + * RR0 (color 0 of class B) is the long/ptr return register, so it must + * stay a valid color so the MIP call-result coalescing move (regs.c + * moveadd(rv, RETREG)) can be mapped by colfind. But the Z8000 cannot + * use RR0 as an indirect base "(rr0)", so we must never *allocate* it. + * Clear its bit from class B's selectable-color mask: the allocator + * then picks only rr2..rr10, while color2reg/regK still know RR0. + * (COLORMAP CLASSB is set to 5 accordingly.) + * + * clregs is indexed by class-1; class B is index 1 (CLASSB is a pass2 + * macro not visible in this pass1 file, so the literal 1 is used). + */ + clregs[1] &= ~1; /* class B: drop color 0 (== RR0) */ + + /* + * Redirect all pass-2 assembly output through a temp file so ejobcode() + * can run the stack-cleanup peephole over the emitted text. bjobcode + * emits no assembly itself, so capturing from here is complete. On any + * failure leave stdout untouched (output stays direct, no peephole). + */ + fflush(stdout); + argcse_savefd = dup(fileno(stdout)); + argcse_cap = tmpfile(); + if (argcse_savefd >= 0 && argcse_cap != NULL) { + dup2(fileno(argcse_cap), fileno(stdout)); + } else { + if (argcse_savefd >= 0) { + close(argcse_savefd); + argcse_savefd = -1; + } + if (argcse_cap != NULL) { + fclose(argcse_cap); + argcse_cap = NULL; + } + } +} + +/* + * Emit a string literal. + * Coherent's assembler has no .ascii directive, so emit the bytes one per + * line with .byte (decimal), matching the native Coherent compiler output, + * terminated by a NUL byte. The string lives in the .strn segment. + */ +void +instring(struct symtab *sp) +{ + char *s; + int val; + TWORD t; + + /* Switch to the string segment (.strn) */ + locctr(STRNG, sp); + + /* Emit the label for this string literal */ + if (sp->slevel == 0) + printf("%s:\n", getexname(sp)); + else + printf(LABFMT ":\n", sp->soffset); + + t = BTYPE(sp->stype); + s = sp->sname; + if (t == CHAR || t == UCHAR) { + while (*s) { + if (*s == '\\') + val = (int)esccon(&s); + else + val = *s++; + printf("\t.byte\t%d\n", val & 0377); + } + printf("\t.byte\t0\n"); + } else + cerror("instring: wide strings not supported"); +} + +/* + * Default integer argument promotion: char/short arguments occupy a + * full word slot and a K&R callee ("int fd;") reads the whole word, + * but a class-D char value only defines the LOW byte of its containing + * word, so a bare word push would carry a garbage high byte. Widen to + * int/unsigned so the push is a properly extended word, exactly like + * the native compiler. (The front end only promotes FLOAT to DOUBLE + * for unprototyped calls, in params.c oldarg(); the integer promotions + * land here.) + */ +static NODE * +argwiden(NODE *q) +{ + TWORD t = q->n_type; + + /* short/ushort are already full 16-bit words on this target */ + if (t == CHAR) + q = block(SCONV, q, NIL, INT, 0, 0); + else if (t == UCHAR) + q = block(SCONV, q, NIL, UNSIGNED, 0, 0); + return q; +} + +/* + * Prepare a function call: wrap each argument in a FUNARG node + * so pass2 knows to push them onto the stack. + */ +NODE * +funcode(NODE *p) +{ + NODE *r, *l; + + for (r = p->n_right; r->n_op == CM; r = r->n_left) { + if (r->n_right->n_op != STARG) { + l = argwiden(r->n_right); + r->n_right = block(FUNARG, l, NIL, + l->n_type, l->n_df, l->n_ap); + } + } + if (r->n_op != STARG) { + l = talloc(); + *l = *r; + r->n_op = FUNARG; + r->n_left = argwiden(l); + r->n_type = r->n_left->n_type; + } + return p; +} + +/* Fix up type of a bit-field. */ +void +fldty(struct symtab *p) +{ +} + +/* + * Sparse-switch dispatch via the Z8000 cpir block-search idiom. + * + * Native Coherent cc dispatches a scattered set of case values (typically + * ASCII character codes) not with a compare ladder but with a single cpir + * that linearly searches a compact .word table of the case values, then + * indexes a parallel .long table of target addresses and jumps through it: + * + * ld rC, $N + * ldl rrP, $LTAB + * cpir rX, (rrP), rC, eq ; rX = switch value + * jr ne, DEFAULT + * sub r(P+1), $LTAB ; r(P+1) = 2*(idx+1) + * add r(P+1), r(P+1) ; = 4*(idx+1) + * ldl rrP, LTAB+(2N-4)(r(P+1)) + * jp un, (rrP) + * LTAB: .word v0..v(N-1) ; .long t0..t(N-1) + * + * The search is one instruction regardless of N, so eight instructions + * dispatch any number of cases - a large win over the 2-per-case ladder once + * N grows. The idiom cannot be built from pass1 trees (there is no C-level + * "search", and cpir's fixed register roles need pass2 allocation), so we + * only MARK the switch here in comment interpasses carrying the case table; + * myreader() rewrites the marks into a GOTO(SWDISP(value)) node plus the data + * table, once pass2 can reserve the scratch pair + count register. + * + * Eligibility: a word-sized (int/unsigned) switch value - cpir compares + * 16-bit words, so a long switch cannot use it - and a SIZE gate. The + * dispatch is ~28 bytes plus 4 bytes per case (a .word value + a .word + * target offset); a compare ladder is ~6 bytes per case (cp $imm; jr) with + * no tables. So cpir is only smaller for the larger switches; use it just + * when 28 + 4N < 6N (+2 if there is a default branch), i.e. N >= ~14. Below + * that the ladder is smaller (and smaller than native's cpir). + */ +int +mygenswitch(int num, TWORD type, struct swents **p, int n) +{ + char buf[64]; + int i, deflab, ltab; + + if (type != INT && type != UNSIGNED) + return 0; + if (28 + 4 * n >= 6 * n + (p[0]->slab > 0 ? 2 : 0)) + return 0; /* ladder is at least as small */ + + /* default target: the real default label, or a fresh label placed + * just after the dispatch (falls through past the switch) */ + deflab = p[0]->slab > 0 ? p[0]->slab : getlab(); + ltab = getlab(); + + snprintf(buf, sizeof(buf), "\t/SWH %d %u %d %d %d\n", + num, (unsigned)type, n, deflab, ltab); + send_passt(IP_ASM, buf); + for (i = 1; i <= n; i++) { + snprintf(buf, sizeof(buf), "\t/SWC %d %d\n", + (int)p[i]->sval, p[i]->slab); + send_passt(IP_ASM, buf); + } + send_passt(IP_ASM, "\t/SWE\n"); + + if (p[0]->slab <= 0) + send_passt(IP_DEFLAB, deflab); + + return 1; +} + +NODE * +builtin_cfa(const struct bitable *bt, NODE *a) +{ + uerror("__builtin_cfa not supported"); + return bcon(0); +} + +NODE * +builtin_return_address(const struct bitable *bt, NODE *a) +{ + uerror("__builtin_return_address not supported"); + return bcon(0); +} + +NODE * +builtin_frame_address(const struct bitable *bt, NODE *a) +{ + uerror("__builtin_frame_address not supported"); + return bcon(0); +} diff --git a/arch/z8001/local.c b/arch/z8001/local.c new file mode 100644 index 000000000..b3583e249 --- /dev/null +++ b/arch/z8001/local.c @@ -0,0 +1,763 @@ +/* + * Copyright (c) 2026 Michal Pleban. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* + * pass1 local routines for the Zilog Z8001 / Coherent target. + */ + +#include "pass1.h" + +#ifndef LANG_CXX +#define NODE P1ND +#undef NIL +#define NIL NULL +#define fwalk p1fwalk +#define nfree p1nfree +#define talloc p1alloc +#define n_type ptype +#undef n_df +#define n_df pdf +#undef n_ap +#define n_ap pss +#define sap sss +#endif + +#include + +/* + * offword: rewrite a pointer-arithmetic index subtree to word type. + * + * The front end converts the index of pointer arithmetic to the 32-bit + * offset type before scaling, so pass 2 widens it into a pair (exts) + * and adds with addl. Under the Coherent segmented assumption (no + * object crosses a segment boundary - the license recorded on the + * TPOINT SP16/SPCON rules in table.c) only the low 16 bits of the + * index ever reach the pointer's offset word, so the widen is dead: + * strip it and compute the index in word registers; the TPOINT + * "add/sub UL,AR" rules then operate on the offset word alone, like + * native cc ("sla r9,$2; add r11,r9"). Scaling (LS/MUL by a constant) + * moves into the word domain with it - the low 16 product/shift bits + * are identical. Returns the rewritten subtree, or NULL to leave the + * tree (and thus the full-width addl/subl) alone. POINTERS ONLY: the + * caller checks the PLUS/MINUS node is pointer-typed, a plain long + * addition keeps its real 32-bit carry. + */ +static NODE * +offword(NODE *p) +{ + NODE *l; + + if (p->n_type != LONG && p->n_type != ULONG) + return NULL; + switch (p->n_op) { + case SCONV: + l = p->n_left; + if (l->n_op == FLD) + return NULL; /* keep bitfield extraction intact */ + switch (l->n_type) { + case INT: + case UNSIGNED: + case SHORT: + case USHORT: + /* the widen becomes a no-op: use the word value */ + nfree(p); + return l; + case CHAR: + case UCHAR: + /* widen to a word instead of a pair */ + p->n_type = l->n_type == CHAR ? INT : UNSIGNED; + return p; + } + return NULL; + case LS: + case MUL: + /* constant elsize scaling: scale the word instead of the + * pair (word mult/sla, exactly the native idiom) */ + if (p->n_right->n_op != ICON || p->n_right->n_sp != NULL) + return NULL; + if (glval(p->n_right) < 0 || glval(p->n_right) > 32767) + return NULL; + if ((l = offword(p->n_left)) == NULL) + return NULL; + p->n_left = l; + p->n_type = INT; + p->n_right->n_type = INT; + return p; + } + return NULL; +} + +/* + * conaddr: a compare operand that is, or will fold to, a constant. + * + * Besides a bare ICON, an ADDROF of a static-duration NAME (an array + * name in an expression, "&global") counts: optim folds it to a named + * ICON, but only after the compare node has been through clocal, so + * the const-to-right swap below must recognize the pre-fold form. + * The andable() gate mirrors the fold's own condition. + */ +static int +conaddr(NODE *q) +{ + return q->n_op == ICON || + (q->n_op == ADDROF && q->n_left->n_op == NAME && + andable(q->n_left)); +} + +/* + * clocal: perform local pass-1 transformations. + * + * The main job here is to rewrite NAME nodes for AUTO and PARAM variables + * into OREG nodes relative to the frame pointer (R13). + * + * Frame pointer (R13) model: + * - R13 points to the entry SP (just below the return address) + * - Locals: negative OREG offsets from R13 (AUTOINIT=0) + * - Params: positive OREG offsets from R13 (ARGINIT=32 bits = 4 bytes) + */ +NODE * +clocal(NODE *p) +{ + struct symtab *q; + NODE *l; + int o; + TWORD m; + + switch (o = p->n_op) { + + case NAME: + if ((q = p->n_sp) == NULL) + return p; + + switch (q->sclass) { + case AUTO: + case REGISTER: + case PARAM: + /* + * Every local and parameter - scalar or aggregate - + * becomes a frame structure reference *(r13 + off), + * the arch/i86 idiom. Pass 2's canon folds the plain + * accesses back into OREG(r13) (identical code), while + * &x cancels ADDROF(UMUL) in buildtree instead of + * hitting "unacceptable operand of &" - a direct OREG + * here broke &scalar-local whenever the variable was + * not an xtemps TEMP (always, for doubles). + * + * The big-endian char-parameter byte correction + * (value in the low byte of the pushed word slot) + * is applied once to soffset in bfcode(), not here. + */ + l = block(REG, NIL, NIL, PTR+STRTY, 0, 0); + slval(l, 0); + l->n_rval = FPREG; + p = stref(block(STREF, l, p, 0, 0, 0)); + break; + + case STATIC: + /* Static local: use slevel to distinguish from globals */ + if (q->slevel == 0) + break; + slval(p, 0); + break; + + case EXTERN: + case EXTDEF: + /* External: leave as NAME (will become a symbol reference) */ + break; + } + break; + + case PCONV: + /* + * Pointer conversion: if the source is already pointer-sized + * (32-bit pair), this is a no-op. + */ + l = p->n_left; + m = l->n_type; + if (ISPTR(m) || m == LONG || m == ULONG) { + /* Already 32-bit; just change the type label */ + l->n_type = p->n_type; + l->n_df = p->n_df; + l->n_ap = p->n_ap; + nfree(p); + return l; + } + break; + + case LS: + case RS: + /* + * Integer-promote a byte shift count. buildtree() promotes + * the value being shifted, but only *demotes* an oversized + * count (long -> int); a char/uchar count is left as-is. The + * dynamic shift instructions (sdl/sda/sdll/sdal) take the + * count in a word register, so the register-count table rules + * require a TWORD right operand - a byte count would match no + * rule ("Cannot generate code ... op >>"). SHORT/USHORT are + * already word-sized here, so only CHAR/UCHAR need widening. + * + * Built with block()/direct retype rather than makety() so the + * shared file compiles under both ccom and cxxcom (whose + * makety() prototypes differ). A constant count is retyped in + * place (no widen needed); a loaded byte gets a real SCONV. + */ + if (p->n_right->n_type == CHAR || p->n_right->n_type == UCHAR) { + if (p->n_right->n_op == ICON) + p->n_right->n_type = INT; + else + p->n_right = block(SCONV, p->n_right, NIL, + INT, 0, 0); + } + break; + + case SCONV: + /* + * Scalar conversion: try to eliminate no-ops. + */ + l = p->n_left; + m = l->n_type; + + /* + * These same-size folds RETYPE the child, which is only a + * bit-level no-op. Never do it to a FLD: its signedness + * controls how rmfldops extracts the bitfield (sra vs srl), + * so stref's SCONV wrapper must stay and the field keep its + * declared type. (Seen as: unsigned fields read back + * sign-extended.) + */ + if (l->n_op == FLD) + break; + + /* int/short <-> int/short in same class: no-op */ + if ((m == INT || m == UNSIGNED || m == SHORT || m == USHORT) && + (p->n_type == INT || p->n_type == UNSIGNED || + p->n_type == SHORT || p->n_type == USHORT)) { + l->n_type = p->n_type; + nfree(p); + return l; + } + + /* long/pointer <-> long in same class: no-op. A pointer + * source shows up as SCONV from an explicit (long)ptr cast + * (conversions TO pointers are PCONV); both are 32-bit + * pairs, so it is the same bit-level no-op as long<->long. + * (Seen as: "Cannot generate code op SCONV" on units.c + * "u_name += (long)sstart".) */ + if ((m == LONG || m == ULONG || ISPTR(m)) && + (p->n_type == LONG || p->n_type == ULONG)) { + l->n_type = p->n_type; + nfree(p); + return l; + } + break; + + case PLUS: + case MINUS: + /* + * Variable pointer index: strip the int->long widen from + * the index subtree so the arithmetic happens on the + * offset word (see offword above). Pointer-typed nodes + * only; the index is always the right operand (buildtree + * puts the pointer on the left), and pointer difference + * is not pointer-typed so it never gets here. + */ + if (!ISPTR(p->n_type) || ISPTR(p->n_right->n_type)) + break; + if ((l = offword(p->n_right)) != NULL) + p->n_right = l; + break; + + case EQ: + case NE: + case LT: + case LE: + case GT: + case GE: + case ULT: + case ULE: + case UGT: + case UGE: { + /* + * Narrow a char compare against a constant: the front end + * promotes both sides to int, hiding the byte operand + * behind an SCONV so pass 2 can never use testb/cpb/andb. + * For EQ/NE the promotion is value-preserving in both + * directions whenever the constant fits the char's value + * range. For the ORDERED compares the promoted word + * compare maps to the byte compare with the char's own + * extension: sign-extension preserves signed order (char + * keeps the signed operator), zero-extension puts both + * sides in 0..255 where the signed word compare equals + * the UNSIGNED byte compare (uchar flips the operator to + * the unsigned condition). A sign-extended char under an + * already-unsigned word compare has no byte equivalent + * and is left alone. + */ + NODE *sc, *cn; + + /* + * Constant to the right, for every compare width: the + * parser builds a>b and a<=b as b=a (cgram.y + * eve()), leaving the constant on the left where pass 2 + * has no imm-compare rule and detours it through a + * register ("ldk r1,$0; cp r1,r0"). Swapping a (side- + * effect-free) constant reverses the ordered operator; + * EQ/NE are symmetric. conaddr() also catches address + * constants still in their pre-fold ADDROF(NAME) form + * ("sp > fname" against an array). + */ + if (conaddr(p->n_left) && !conaddr(p->n_right)) { + cn = p->n_left; + p->n_left = p->n_right; + p->n_right = cn; + switch (p->n_op) { + case LT: p->n_op = GT; break; + case GT: p->n_op = LT; break; + case LE: p->n_op = GE; break; + case GE: p->n_op = LE; break; + case ULT: p->n_op = UGT; break; + case UGT: p->n_op = ULT; break; + case ULE: p->n_op = UGE; break; + case UGE: p->n_op = ULE; break; + } + } + + sc = p->n_left; + cn = p->n_right; + if (cn->n_op != ICON || cn->n_sp != NULL) + break; + + /* + * Truth test of a masked char: EQ/NE(AND(SCONV(char->int), + * ICON m), ICON k) with m and k both in 0..255. The mask + * clears every promoted high bit no matter how the char + * extended, so the equality is decided by the low byte + * alone: strip the SCONV and retype the AND to the char + * type. Pass 2 then emits andb, whose Z flag feeds the + * eq/ne branch directly (native: "andb rlN,$m; jr ne"). + */ + if ((p->n_op == EQ || p->n_op == NE) && sc->n_op == AND && + glval(cn) >= 0 && glval(cn) <= 255) { + NODE *a = sc->n_left, *msk = sc->n_right; + + if (a->n_op != SCONV || msk->n_op != ICON || + msk->n_sp != NULL || + glval(msk) < 0 || glval(msk) > 255) + break; + if (a->n_type != INT && a->n_type != UNSIGNED) + break; + if (a->n_left->n_op == FLD) + break; /* keep bitfield extraction intact */ + m = a->n_left->n_type; + if (m != CHAR && m != UCHAR) + break; + sc->n_left = a->n_left; + nfree(a); + sc->n_type = m; + msk->n_type = m; + cn->n_type = m; + break; + } + + if (sc->n_op != SCONV) + break; + if (sc->n_type != INT && sc->n_type != UNSIGNED) + break; + if (sc->n_left->n_op == FLD) + break; /* keep bitfield extraction intact */ + m = sc->n_left->n_type; + if (m == CHAR) { + if (glval(cn) < -128 || glval(cn) > 127) + break; + } else if (m == UCHAR) { + if (glval(cn) < 0 || glval(cn) > 255) + break; + } else + break; + if (p->n_op != EQ && p->n_op != NE) { + if (m == CHAR) { + /* sign-extended: only the SIGNED word + * compare maps to the signed byte compare */ + if (p->n_op != LT && p->n_op != LE && + p->n_op != GT && p->n_op != GE) + break; + } else { + /* zero-extended: both sides are 0..255, so + * signed and unsigned word compares agree + * and both map to the UNSIGNED byte compare */ + if (p->n_op == LT) + p->n_op = ULT; + else if (p->n_op == LE) + p->n_op = ULE; + else if (p->n_op == GT) + p->n_op = UGT; + else if (p->n_op == GE) + p->n_op = UGE; + } + } + p->n_left = sc->n_left; + nfree(sc); + cn->n_type = m; + break; + } + + case NOT: + /* + * !(a REL b) on integer/pointer operands folds to the + * negated relation - cgram negates every if/while + * condition with a NOT wrapper, and wrapping the relop + * in EQ(rel, 0) instead would push it into VALUE context + * (KEEPLOGOPVALUE) and pessimize every branch. Float + * compares keep their NOT: andorbr's label swap tracks + * their negation (ATTR_FP_SWAPPED). + * + * For a plain scalar, !e is exactly (e == 0): rewriting + * lets "f = !x" materialize through clr+tcc instead of + * the branch diamond; in branch context andorbr treats + * NOT(e) and EQ(e, 0) identically. ANDAND/OROR/NOT + * operands need andorbr's lazy evaluation: left alone. + * The clocal re-run applies the compare canonicalization + * above (char narrowing) to the new node. + */ + l = p->n_left; + if (l->n_op >= EQ && l->n_op <= UGT) { + static int negrel[] = { NE, EQ, GT, GE, LT, LE, + UGT, UGE, ULT, ULE }; + + if (KEEPLOGOPVALUE_T(l->n_left->n_type) && + KEEPLOGOPVALUE_T(l->n_right->n_type)) { + l->n_op = negrel[l->n_op - EQ]; + *p = *l; + nfree(l); + } + break; + } + if (l->n_op == ANDAND || l->n_op == OROR || l->n_op == NOT) + break; + if (KEEPLOGOPVALUE_T(l->n_type)) { + p->n_op = EQ; + p->n_right = bcon(0); + return clocal(p); + } + break; + + case FORCE: + /* + * FORCE ensures the return value is in the correct register. + * On Z8001: int/short in R1, long/ptr in RR0. A struct + * value is returned by ADDRESS in RR0 (hidden-pointer ABI); + * efcode() then copies it into the caller's buffer. + */ + if (p->n_type == STRTY || p->n_type == UNIONTY) { + p->n_right = buildtree(ADDROF, p->n_left, NIL); + p->n_op = ASSIGN; + p->n_type = p->n_right->n_type; + p->n_left = block(REG, NIL, NIL, p->n_right->n_type, + p->n_right->n_df, p->n_right->n_ap); + slval(p->n_left, 0); + regno(p->n_left) = RETREG(p->n_type); + break; + } + p->n_op = ASSIGN; + p->n_right = p->n_left; + p->n_left = block(REG, NIL, NIL, p->n_type, p->n_df, p->n_ap); + slval(p->n_left, 0); + regno(p->n_left) = RETREG(p->n_type); + break; + } + + return p; +} + +/* + * exname: return the external (mangled) name of a symbol. + * + * Coherent uses a TRAILING underscore convention: "printf" → "printf_". + * This is the opposite of the leading-underscore convention used on many + * other Unix systems. + */ +char * +exname(char *p) +{ +#define NCHNAM 256 + static char text[NCHNAM + 1]; + int i; + + if (p == NULL) + return ""; + + for (i = 0; *p && i < NCHNAM - 1; ++i) + text[i] = *p++; + text[i++] = '_'; /* Coherent uses a trailing underscore */ + text[i] = '\0'; + text[NCHNAM] = '\0'; /* truncate */ + + return text; +} + +/* + * cisreg: return 1 if the type can live in a register. + * + * On Z8001: char, short, int fit in a word register (SAREG). + * long, float, and pointers fit in a pair register (SBREG). + * double lives in a quad (SCREG) transiently, but with only 3 quads and + * every FP operation being a helper call, register-resident doubles buy + * nothing: keep them memory-resident (matches the native compiler). + */ +int +cisreg(TWORD t) +{ + switch (t) { + case CHAR: case UCHAR: + case SHORT: case USHORT: + case INT: case UNSIGNED: + case LONG: case ULONG: + case FLOAT: + return 1; + default: + return ISPTR(t) ? 1 : 0; + } +} + +/* + * ninval: emit an initializer for a data item. + * Returns 1 if handled here, 0 to let the generic code handle it. + * + * Floating-point values are done here: the generic inval() emits them + * as astypnames[INT] units assuming soft_toush() returns SZINT-sized + * chunks, but it returns uint32_t chunks (least significant first) - + * on a 16-bit-int target that prints 32-bit values as ".word" in the + * wrong positions. Emit big-endian 16-bit words explicitly. + */ +int +ninval(CONSZ off, int fsz, NODE *p) +{ +#ifndef LANG_CXX /* n_scon is a C-frontend node field */ + TWORD t = p->n_type; + uint32_t *ufp; + int nbits, i; + + if (t != FLOAT && t != DOUBLE && t != LDOUBLE) + return 0; /* generic inval() handles integers fine */ + + ufp = soft_toush(p->n_scon, t, &nbits); + for (i = nbits/32 - 1; i >= 0; i--) { + printf("\t.word\t%u\n", (unsigned)((ufp[i] >> 16) & 0xffff)); + printf("\t.word\t%u\n", (unsigned)(ufp[i] & 0xffff)); + } + return 1; +#else + return 0; +#endif +} + +/* + * myp2tree: convert FCON (floating-point constant) nodes to named static + * symbols in the data segment, so pass2 can handle them as memory references. + */ +void +myp2tree(NODE *p) +{ + struct symtab *sp; + + /* + * Struct-return calls need ATTR_P2STRUCT in pass 2 (the caller + * reserves the return buffer from it), which p2tree only attaches + * when p->pss is set. Member access directly on a call result + * ("f().y") retypes the STCALL via pprop and loses pss; restore + * it from the function designator node. (pss is a C-frontend + * P1ND field; the C++ frontend has its own node layout.) + */ +#ifndef LANG_CXX + if ((p->n_op == STCALL || p->n_op == USTCALL) && p->pss == NULL) + p->pss = p->n_left->pss; +#endif + + if (p->n_op != FCON) + return; + + sp = isinlining ? permalloc(sizeof(struct symtab)) + : tmpalloc(sizeof(struct symtab)); + /* + * Zero the whole struct: the "#define sap sss" compat macro above + * makes the real attribute-list field unnameable here, and leaving + * it as tmpalloc garbage sends locctr()'s attr_find() down a wild + * pointer (crashed layout-dependently on the first FCON in any + * function after one containing FP helper calls). + */ + memset(sp, 0, sizeof(struct symtab)); + sp->sclass = STATIC; + sp->slevel = 1; /* fake numeric label */ + sp->soffset = getlab(); + sp->stype = p->n_type; + sp->squal = (CON >> TSHIFT); + + locctr(DATA, sp); + defloc(sp); + inval(0, tsize(sp->stype, sp->sdf, sp->sap), p); + + p->n_op = NAME; + slval(p, 0); + p->n_sp = sp; +} + +/* + * spalloc: allocate stack space for a struct return value. + * The hidden pointer argument is passed in RR0 (first argument register). + */ +void +spalloc(NODE *t, NODE *p, OFFSZ off) +{ + NODE *sp; + + if (off & (ALSTACK - 1)) + off += ALSTACK - (off & (ALSTACK - 1)); + + p->n_left->n_type = INCREF(p->n_left->n_type); + sp = block(REG, NIL, NIL, INCREF(p->n_type), t->n_df, t->n_ap); + slval(sp, 0); + regno(sp) = RR0; + p = buildtree(ASSIGN, t, sp); + ecomp(p); +} + +/* + * Map a type to its canonical machine type. + * Z8001 has no 16-bit/64-bit register types distinct from int/long, and + * no long double, so collapse those onto the supported types. + */ +TWORD +ctype(TWORD type) +{ + switch (BTYPE(type)) { + case SHORT: + MODTYPE(type, INT); + break; + case USHORT: + MODTYPE(type, UNSIGNED); + break; + case LDOUBLE: + MODTYPE(type, DOUBLE); + break; + /* XXX no 64-bit integer support yet */ + case LONGLONG: + MODTYPE(type, LONG); + break; + case ULONGLONG: + MODTYPE(type, ULONG); + break; + } + return type; +} + +/* Every name can have its address taken. */ +int +andable(NODE *p) +{ + return 1; +} + +/* No target-specific pragmas. */ +int +mypragma(char *str) +{ + return 0; +} + +/* No fixup needed when a symbol is defined. */ +void +fixdef(struct symtab *sp) +{ +} + +/* No last-minute pass1 rewriting. */ +void +pass1_lastchance(struct interpass *ip) +{ +} + +/* Function declaration hook - nothing to emit. */ +void +calldec(NODE *p, NODE *q) +{ +} + +/* External declaration hook - nothing to emit. */ +void +extdec(struct symtab *q) +{ +} + +/* + * Emit an uninitialized (common) data definition. + * Coherent's assembler S_COMM handler requires ".comm ," with a + * COMMA: it does getid(name) then "if (getnb() != ',') qerr()". A tab makes + * getnb() return the size digit instead of ',', so as reports a 'q' error. + */ +void +defzero(struct symtab *sp) +{ + int off; + char *name; + + name = getexname(sp); + off = (int)tsize(sp->stype, sp->sdf, sp->sap); + SETOFF(off, SZCHAR); + off /= SZCHAR; + + if (sp->sclass == STATIC || sp->sclass == USTATIC) { + /* + * Statics must NOT use .comm: Coherent commons are + * linker-global, so a static would leak into the global + * namespace, and same-numbered L labels from two modules + * would be merged into one shared common. Reserve zeroed + * .bssd storage under the label instead, like the native + * compiler (".bssd; .even; L85: .blkb 4"). + */ + if (lastloc != UDATA) { + setseg(UDATA, NULL); + lastloc = UDATA; + } + printf("\t.even\n"); + if (sp->slevel == 0) + printf("%s:\n", name); + else + printf(LABFMT ":\n", sp->soffset); + printf("\t.blkb\t%d\n", off); + return; + } + + /* Ensure a data segment is active (see comment above). */ + if (lastloc != DATA) { + setseg(DATA, NULL); + lastloc = DATA; + } + + if (sp->slevel == 0) + printf("\t.comm\t%s,%d\n", name, off); + else + printf("\t.comm\t" LABFMT ",%d\n", sp->soffset, off); +} + diff --git a/arch/z8001/local2.c b/arch/z8001/local2.c new file mode 100644 index 000000000..8df92e379 --- /dev/null +++ b/arch/z8001/local2.c @@ -0,0 +1,3083 @@ +/* + * Copyright (c) 2026 Michal Pleban. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* + * pass2 local routines for the Zilog Z8001 / Coherent target. + * + * Frame layout (matches the reference Coherent compiler). + * + * Node offsets are computed relative to the ENTRY SP (the SP before the frame + * is allocated): args positive, autos negative. But r13 (the frame pointer) + * is set to the frame BOTTOM, and every frame slot is addressed in the stack + * segment via a per-function equate "L = SS|total": + * + * address form effective address + * arg: L+off(r13) -> SS:(total + off + r13), off = +4,+6,... + * auto: L+off(r13) -> SS:(total + off + r13), off = -2,-4,... + * with r13 = entry_SP - total, so EA = SS:(entry_SP + off) = the slot. The + * equate also supplies the SS segment, needed when the address is taken (lda). + * + * high address + * [caller args] entry_SP+4, +6, ... (ARGINIT=32 bits = 4 bytes) + * [return addr] entry_SP+0 .. +3 (4-byte segmented return addr) + * -------------- entry SP + * [locals] entry_SP-2, -4, ... (AUTOINIT=0) + * [callee saves] bottom .. bottom+2*nsave-1 + * -------------- r13 = r15 = current SP = frame bottom + * low address + * + * Prologue sequence: + * L = SS|total (total = fsize + 2*nsave; framelab) + * dec/sub r15, $total + * [ld (rr14), r13 (for nsave == 1)] + * [ldm (rr14), rX, $nsave (for nsave > 1, saves rX..r13)] + * ld r13, r15 (r13 = frame bottom) + * + * Note: neither the ld/ldm store nor load forms change r15; the frame is + * allocated solely by "dec/sub r15,$total" and reclaimed in the + * epilogue by "inc/add r15,$total". + */ + +#include "pass2.h" +#include + +int canaddr(NODE *); +void mygenregs(struct interpass *ip); + +/* + * Descriptor carried by a SWDISP node (in n_name) from swcpir() to the ZZ + * zzzcode: case count, default-target label, and the value/target table + * label. See mygenswitch() in code.c for the dispatch shape. + */ +struct swdesc { + int n; /* number of cases */ + int deflab; /* default (no-match) label */ + int ltab; /* .word value/offset table label */ +}; + +/* + * Register names indexed by PCC register number. + * r0-r15 are 16-bit word registers; rr0,rr2,...,rr10 are 32-bit pairs; + * rq0,rq4,rq8 are 64-bit quads (doubles); rl0-rl7 are the 8-bit byte + * registers (low halves of r0-r7, register class D for char values). + * Indices: 0-15 = r0-r15, 16=rr0 .. 21=rr10, 22=rq0 .. 24=rq8, + * 25=rl0 .. 32=rl7. + */ +char *rnames[] = { + "r0", "r1", "r2", "r3", "r4", "r5", + "r6", "r7", "r8", "r9", "r10", "r11", + "r12", "r13", "r14", "r15", + "rr0", "rr2", "rr4", "rr6", "rr8", "rr10", + "rq0", "rq4", "rq8", + "rl0", "rl1", "rl2", "rl3", "rl4", "rl5", "rl6", "rl7" +}; + +/* + * dword: the word register containing byte register b. Byte values are + * operated on with WORD instructions (extsb, and, neg, push, ld) whenever + * the byte-register field encoding would not do: word ops work on any + * register and only the low byte carries the value. + */ +static int +dword(int b) +{ + return b - RL0; +} + +/* + * The component pieces of a quad register: qword(q) is the index of its + * first (most significant) word register, qpair(q, lo) names its high + * (lo=0) or low (lo=1) pair. Big-endian: the high pair/word holds the + * IEEE sign+exponent. + */ +static int +qword(int q) +{ + return (q - RQ0) * 4; +} + +static char * +qpair(int q, int lo) +{ + return rnames[RR0 + (q - RQ0) * 2 + (lo ? 1 : 0)]; +} + +/* + * Labels already emitted in the current function, newest first. cbfuse() + * consults this to tell a backward branch (target already seen) from a + * forward one: djnz is only a win backward - forward it would relax in the + * assembler to the larger "dec;jp" form. Allocated from the per-function + * tmpalloc arena and reset at each prologue. + */ +struct seenlab { + struct seenlab *next; + int lab; +}; +static struct seenlab *seenlabs; + +static int +labseen(int lab) +{ + struct seenlab *s; + + for (s = seenlabs; s != NULL; s = s->next) + if (s->lab == lab) + return 1; + return 0; +} + +void +deflab(int label) +{ + struct seenlab *s; + + s = tmpalloc(sizeof(struct seenlab)); + s->lab = label; + s->next = seenlabs; + seenlabs = s; + + printf(LABFMT ":\n", label); +} + +/* + * Per-function frame-base label. Coherent addresses every frame slot in the + * stack segment SS via a symbol "L = SS|" and references slots + * as "L+off(r13)" (see echo.s: L10001=SS|526, "lda rr0,L10001-512(r13)"). + * r13 is set to the frame bottom (the SP after allocation), and the equate's + * SS|framesize value plus the (entry-SP-relative) node offset reconstructs the + * absolute frame offset while carrying the SS segment. Set in prologue(). + */ +static int framelab; + +/* + * Set by prologue() and consulted by eoftn(): this function needs no + * frame-pointer setup at all (no r13 save, no "ld r13,r15", no frame + * allocation), so both prologue and epilogue collapse to nothing but the + * body and a bare "ret un". See usesfp() and prologue() for the criteria. + */ +static int curframeless; + +/* + * Prologue's register-save plan, recomputed-invariant parts stashed here for + * eoftn() (which cannot re-run usesfp() - it only sees the epilog end of the + * list). curneedfp: r13 is set up as a frame pointer (has autos/spills or the + * body addresses a frame slot). cursavetop: highest register in the saved + * contiguous run (R13 when curneedfp, else the top used callee-saved r6..r12). + */ +static int curneedfp; +static int cursavetop; + +/* + * walkf callback: flag any reference to r13 (FPREG) used as a frame base. + * By emit time canon() has lowered stack args and autos to OREG(FPREG) and + * &local to ADDROF(OREG(FPREG)) or PLUS(REG FPREG, ICON); all of these carry + * FPREG in n_rval, so a single test on OREG/REG suffices. r13 is a dedicated + * frame pointer, never allocated, so a hit is always a genuine frame use. + */ +static void +fpuse(NODE *p, void *arg) +{ + if ((p->n_op == OREG || p->n_op == REG) && p->n_rval == FPREG) + *(int *)arg = 1; +} + +/* + * Does the function body reference the frame pointer r13 at all? The whole + * interpass list (IP_PROLOG .. IP_EPILOG) is still linked when prologue() runs + * during the emit walk, so we scan the body forward from the prolog node. A + * function that touches no frame slot needs no frame pointer; combined with a + * zero autosize (which also rules out spill slots, since any spill bumps + * p2maxautooff) this is the frameless criterion. + */ +static int +usesfp(struct interpass_prolog *ipp) +{ + struct interpass *ip; + int found = 0; + + for (ip = DLIST_NEXT(&ipp->ipp_ip, qelem); ip->type != IP_EPILOG; + ip = DLIST_NEXT(ip, qelem)) { + if (ip->type == IP_NODE) + walkf(ip->ip_node, fpuse, &found); + } + return found; +} + +/* + * Emit the function prologue. + * + * Callee-saved word registers are R6-R12; R13 is the frame pointer. We save a + * contiguous run of the callee-saved registers the body actually used. R13 is + * included in that run (and set to the frame bottom) ONLY when the function + * needs a frame pointer - it has autos/spills or addresses a frame slot / stack + * argument through r13 (see curneedfp). A function that uses neither r13 nor + * any callee-saved register emits no prologue at all. + */ +/* + * Determine the lowest-numbered callee-saved word register that must be + * saved. Callee-saved word registers are R6-R12; the pair registers + * RR6-RR10 alias those same word registers. R13 is always saved because + * the prologue overwrites it with the frame pointer. + */ +static int +firstsavereg(void) +{ + int i, firstsave = R13; + + for (i = R6; i <= R12; i++) { + if (TESTBIT(p2env.p_regs, i)) { + firstsave = i; + break; + } + } + /* A used pair RRn implies its two component word registers are used */ + for (i = RR6; i <= RR10; i++) { + if (TESTBIT(p2env.p_regs, i)) { + int base = (i - RR0) * 2; /* low word of the pair */ + if (base < firstsave) + firstsave = base; + } + } + /* A used quad clobbers its callee-saved words: rq4 = r6/r7, + * rq8 = r8-r11. (rq0 is all scratch.) */ + if (TESTBIT(p2env.p_regs, RQ4) && R6 < firstsave) + firstsave = R6; + if (TESTBIT(p2env.p_regs, RQ8) && R8 < firstsave) + firstsave = R8; + /* A used byte register rl6/rl7 lives inside callee-saved r6/r7 */ + if (TESTBIT(p2env.p_regs, RL6) && R6 < firstsave) + firstsave = R6; + if (TESTBIT(p2env.p_regs, RL7) && R7 < firstsave) + firstsave = R7; + return firstsave; +} + +/* + * Highest-numbered callee-saved word register (R6-R12) actually used, or -1 if + * none. Used when r13 is NOT a frame pointer (see prologue): the save run then + * ends at the topmost used callee-saved register instead of always at R13. + */ +static int +lastsavereg(void) +{ + int i, last = -1; + + for (i = R6; i <= R12; i++) + if (TESTBIT(p2env.p_regs, i)) + last = i; + /* pair RRn's high word is (n-RR0)*2+1: rr6->r7, rr8->r9, rr10->r11 */ + for (i = RR6; i <= RR10; i++) + if (TESTBIT(p2env.p_regs, i)) { + int hi = (i - RR0) * 2 + 1; + if (hi > last) + last = hi; + } + /* quads: rq4 = r4-r7 (high saved word r7), rq8 = r8-r11 (high r11) */ + if (TESTBIT(p2env.p_regs, RQ4) && R7 > last) + last = R7; + if (TESTBIT(p2env.p_regs, RQ8) && R11 > last) + last = R11; + if (TESTBIT(p2env.p_regs, RL6) && R6 > last) + last = R6; + if (TESTBIT(p2env.p_regs, RL7) && R7 > last) + last = R7; + return last; +} + +void +prologue(struct interpass_prolog *ipp) +{ + int firstsave, nsave, fsize, total; + + seenlabs = NULL; /* start a fresh emitted-label set (see cbfuse) */ + + firstsave = firstsavereg(); + fsize = (p2maxautooff + 1) & ~1; /* p2maxautooff is bytes; round to word */ + + /* + * r13 is set up as a frame pointer ONLY when the body needs one: either + * there are autos/spills (fsize>0) or the body addresses a frame slot / + * stack argument through r13 (usesfp()). When it isn't needed we neither + * save nor load r13 - the caller's r13 is preserved simply by our never + * touching it - and the register-save run stops at the topmost callee-saved + * register the body actually used instead of running through r13. + */ + curneedfp = (fsize > 0 || usesfp(ipp)); + if (curneedfp) { + cursavetop = R13; /* always save AND set up r13 */ + } else { + cursavetop = lastsavereg(); /* top used callee-saved, or -1 */ + if (cursavetop < 0) { + /* + * Frameless: no autos, no r13 use, no callee-saved + * register touched. Emit nothing; eoftn() emits only + * "ret un". Typical of leaf/no-arg helpers. + */ + curframeless = 1; + return; + } + } + curframeless = 0; + + nsave = cursavetop - firstsave + 1; + total = fsize + nsave * 2; + + /* + * Frame-base equate for stack-segment addressing (see framelab above), + * emitted only when r13 is a frame pointer. L = SS|total; slots are + * addressed "L+off(r13)" with r13 at the frame bottom, so EA = + * SS:(total + off + r13_bottom) recovers the absolute frame offset AND + * supplies the stack segment. + * + * SS is an external symbol pinned per-ABI by the startup: crts0.s sets + * "SS = 0x0000" for user programs (a single flat segment), while the + * kernel's md.s sets "SS = 0x3F3F" (system stack in segment 0x3F). Using + * the symbol makes frame/arg addressing correct in BOTH worlds; hardcoding + * a numeric "0|total" only works when SS==0 and silently reads segment 0 + * for anything running segmented (the kernel), where every callee read its + * stack arguments out of ROM. The symbolic segment is E_SEG; this relies + * on the assembler's E_SEG long-form emission being correct (it now is - + * the earlier outsof() bit-15 drop that motivated the "0|" workaround has + * been fixed in the Coherent "as"). + */ + if (curneedfp) + printf(LABFMT "=SS|%d\n", framelab, total); + + /* + * Allocate-and-save. With no autos (fsize==0, so total==nsave*2) a single + * stack instruction does the SP decrement AND the store: + * nsave==1 -> push (2 bytes vs dec + ld = 4) + * nsave==2, aligned pair -> pushl (2 bytes vs dec + ldm = 6) + * The pair case is gated to rr6/rr8/rr10 (firstsave even, <= R10); rr12 + * would include r13 and only arises on the frame-pointer path anyway. + * Otherwise allocate the frame with dec/sub, then ldm the run at the SP. + */ + if (fsize == 0 && nsave == 1) { + printf("\tpush\t(rr14),r%d\n", firstsave); + } else if (fsize == 0 && nsave == 2 && + (firstsave & 1) == 0 && firstsave <= R10) { + printf("\tpushl\t(rr14),rr%d\n", firstsave); + } else { + if (total > 0) + printf("\t%s\tr15,$%d\n", total <= 16 ? "dec" : "sub", + total); + if (nsave == 1) + printf("\tld\t(rr14),r%d\n", firstsave); + else + printf("\tldm\t(rr14),r%d,$%d\n", firstsave, nsave); + } + + /* r13 = current SP = frame bottom (only when it is the frame pointer). */ + if (curneedfp) + printf("\tld\tr13,r15\n"); +} + +/* + * Emit the function epilogue. + */ +void +eoftn(struct interpass_prolog *ipp) +{ + int firstsave, nsave, fsize, total; + + if (ipp->ipp_ip.ip_lbl == 0) + return; + + /* Frameless (see prologue()): nothing to tear down but the return. */ + if (curframeless) { + printf("\tret\tun\n"); + return; + } + + /* + * Recompute the plan. firstsave/fsize are deterministic from p2env, and + * cursavetop was stashed by prologue() (eoftn cannot re-run usesfp - it + * only sees the epilog end of the interpass list). + */ + firstsave = firstsavereg(); + fsize = (p2maxautooff + 1) & ~1; + nsave = cursavetop - firstsave + 1; + total = fsize + nsave * 2; + + /* + * Mirror of the prologue allocate-and-save combine: pop / popl restore the + * register(s) AND reclaim the frame in one instruction. + */ + if (fsize == 0 && nsave == 1) { + printf("\tpop\tr%d,(rr14)\n", firstsave); + printf("\tret\tun\n"); + return; + } + if (fsize == 0 && nsave == 2 && (firstsave & 1) == 0 && + firstsave <= R10) { + printf("\tpopl\trr%d,(rr14)\n", firstsave); + printf("\tret\tun\n"); + return; + } + + /* Restore the saved run (neither ld nor ldm changes r15) */ + if (nsave == 1) + printf("\tld\tr%d,(rr14)\n", firstsave); + else + printf("\tldm\tr%d,(rr14),$%d\n", firstsave, nsave); + + /* Reclaim the whole frame */ + if (total > 0) + printf("\t%s\tr15,$%d\n", total <= 16 ? "inc" : "add", total); + + printf("\tret\tun\n"); +} + +/* + * hopcode: emit the base opcode string for a simple ALU operation. + * The 'f' suffix is unused on Z8001 (no condition codes in opcode). + */ +void +hopcode(int f, int o) +{ + char *str; + + switch (o) { + case PLUS: str = "add"; break; + case MINUS: str = "sub"; break; + case AND: str = "and"; break; + case OR: str = "or"; break; + case ER: str = "xor"; break; + default: + comperr("hopcode: %d", o); + str = ""; + } + printf("%s", str); +} + +/* + * tlen: return the type size in bytes. + */ +int +tlen(NODE *p) +{ + switch (p->n_type) { + case CHAR: case UCHAR: + return SZCHAR / SZCHAR; + case SHORT: case USHORT: + return SZSHORT / SZCHAR; + case INT: case UNSIGNED: + return SZINT / SZCHAR; + case LONG: case ULONG: + return SZLONG / SZCHAR; + case FLOAT: + return SZFLOAT / SZCHAR; + case DOUBLE: case LDOUBLE: + return SZDOUBLE / SZCHAR; + case LONGLONG: case ULONGLONG: + return SZLONGLONG / SZCHAR; + default: + if (ISPTR(p->n_type)) + return SZPOINT(p->n_type) / SZCHAR; + comperr("tlen: unknown type %d", p->n_type); + return 0; + } +} + +/* + * bwput: print the WORD register containing a class-D byte-register + * operand, for the word instructions that implement char conversions + * and arithmetic (extsb/and/neg/push/ld work on any word register and + * the char value lives in its low byte). + */ +static void +bwput(NODE *p) +{ + if (p->n_op != REG || p->n_rval < RL0) + comperr("bwput: not a byte register (op %d)", p->n_op); + printf("%s", rnames[dword(p->n_rval)]); +} + +/* + * prword: print one 16-bit half of a 32-bit (pair) operand. + * + * The Z8001 has no 32-bit logical/negate/complement instructions, so long + * AND/OR/XOR/COM/NEG are synthesised from two word operations acting on the + * high and low halves of the pair. This prints whichever half is requested. + * + * Big-endian layout: the high (most significant) word is at the lower + * address / the even register Rn; the low word is at offset+2 / Rn+1. + * lo == 0 -> high word, lo == 1 -> low word. + */ +static void +prword(NODE *p, int lo) +{ + CONSZ val; + + switch (p->n_op) { + case REG: + printf("%s", rnames[(p->n_rval - RR0) * 2 + (lo ? 1 : 0)]); + break; + case OREG: + case NAME: + if (lo) + setlval(p, getlval(p) + 2); + adrput(stdout, p); + if (lo) + setlval(p, getlval(p) - 2); + break; + case ICON: + val = getlval(p); + printf("$" CONFMT, lo ? (val & 0xffffLL) : ((val >> 16) & 0xffffLL)); + break; + default: + comperr("prword: bad op %d", p->n_op); + } +} + +/* + * Cost, in bytes, of loading a single 16-bit constant word: clr and ldk + * are 2 bytes, a full "ld rN,#imm16" is 4. Used by the Z0 escape to decide + * whether splitting a long constant beats the 6-byte "ldl rrN,#imm32". + */ +static int +wordloadcost(CONSZ w) +{ + return (w == 0 || (w >= 1 && w <= 15)) ? 2 : 4; +} + +/* + * Emit the cheapest single-word load of constant w into half "lo" of the + * pair operand p: clr (zero), ldk (1..15), or ld (anything else). + */ +static void +prwordload(NODE *p, int lo, CONSZ w) +{ + if (w == 0) { + printf("\tclr\t"); + prword(p, lo); + } else if (w >= 1 && w <= 15) { + printf("\tldk\t"); + prword(p, lo); + printf(",$" CONFMT, w); + } else { + printf("\tld\t"); + prword(p, lo); + printf(",$" CONFMT, w); + } + printf("\n"); +} + +/* + * prmem: print a memory operand displaced by "plus" bytes. + * Used for the second halves of multi-word accesses; adrput picks the + * right encoding (X for frame, DA for names, IR/BA for pair bases). + */ +static void +prmem(NODE *p, CONSZ plus) +{ + setlval(p, getlval(p) + plus); + adrput(stdout, p); + setlval(p, getlval(p) - plus); +} + +/* + * quadmem: move a quad register to/from memory (a 64-bit double). + * + * Memory forms and their instructions: + * NAME (DA) or frame OREG r13 (X): single "ldm rN,mem,$4" - the form + * the native compiler uses (ldm's operand may be IR/DA/X, never BA). + * pair-base OREG: two ldl using IR/BA ("(rrN)" / "rrN(d)"). ldm is + * avoided here even at displacement 0: if the base pair lies inside + * the target quad, ldm would overwrite the base mid-transfer. For + * the two-ldl load the half containing the base is loaded LAST; its + * own ldl is safe because the destination write follows the address + * read. (The register allocator cannot exclude this aliasing: an + * OREG base has no regw node, so addedge_r never sees it.) + */ +static void +quadmem(NODE *mem, int q, int store) +{ + int base, hi; + + if (mem->n_op == OREG && mem->n_rval >= RR0) { + base = mem->n_rval; + /* which half's pair equals the base? -1 if none */ + hi = (RR0 + (q - RQ0) * 2 == base) ? 0 : + (RR0 + (q - RQ0) * 2 + 1 == base) ? 1 : -1; + if (store && hi >= 0) + comperr("quadmem: store base %s inside quad %s", + rnames[base], rnames[q]); + if (store) { + printf("\tldl\t"); + prmem(mem, 0); + printf(",%s\n\tldl\t", qpair(q, 0)); + prmem(mem, 4); + printf(",%s\n", qpair(q, 1)); + } else if (hi == 0) { + /* base is the high pair: load low half first */ + printf("\tldl\t%s,", qpair(q, 1)); + prmem(mem, 4); + printf("\n\tldl\t%s,", qpair(q, 0)); + prmem(mem, 0); + printf("\n"); + } else { + printf("\tldl\t%s,", qpair(q, 0)); + prmem(mem, 0); + printf("\n\tldl\t%s,", qpair(q, 1)); + prmem(mem, 4); + printf("\n"); + } + } else if (mem->n_op == NAME || (mem->n_op == OREG && + mem->n_rval == R13)) { + printf("\tldm\t"); + if (store) { + prmem(mem, 0); + printf(",r%d,$4\n", qword(q)); + } else { + printf("r%d,", qword(q)); + prmem(mem, 0); + printf(",$4\n"); + } + } else + comperr("quadmem: bad mem op %d", mem->n_op); +} + +/* + * zzzcode: handle special escape sequences in instruction templates. + * + * ZB stack cleanup after call: add/inc r15, $n_qual + * ZL long bitwise (AND/OR/ER): word op on high halves, then low halves + * ZC long complement: com on each half + * ZN long negate: complement both halves, then add 1 across the pair + * ZQ clear a pair (reg or mem): clr on each half + * ZF frame-address operand "off(reg)" for an lda + * ZM high (segment) word of result pair, for the post-lda segment mask + * ZG word register containing the class-D (byte) LEFT operand + * ZH word register containing the class-D (byte) result A1 + * ZK byte -> word conversion move: "ld A1,", omitted + * when A1 already is that word (NSL sharing) + * ZU uchar zero-extend of A1's low byte in place: "subb rhN,rhN" + * for r0-r7, "and A1,$0xff" for the high-byte-less r8-r15 + * ZI word -> byte conversion move: "ld ,AL", omitted + * when AL already is A1's word (NSL sharing) + * ZS struct assignment: ldirb block copy + * ZT struct argument: allocate stack slot + ldirb block copy + * ZD double assignment: quad reg <-> quad reg/memory + * ZE double load: memory/quad reg -> result quad A1 + * ZP double argument push: two pushl (low pair first) + * ZW high (sign+exponent) word of the left operand's quad/pair + * ZO the compare just emitted is a sign-Only flag setter (test/testl: + * S and Z valid, P/V left as parity/stale): the LT/GE branch that + * cbgen emits next must read S alone, i.e. jr mi/pl + * ZV condition-code name of this relop, non-negated, for the + * value-context "tcc cc,A1" (the truth value itself is wanted) + * ZY bit number of the single CLEAR bit in the right ICON (an + * SNPOW2 shape), for res: "x &= ~0x40" prints as "$6" + * ZX address constant + word index: "lda A1,sym(AR)" when the + * index register is not r0 and the address is named, else the + * "ldl A1,$sym ; add ,AR" fallback + */ + +/* set by ZO, consumed by the cbgen call that follows the compare */ +static int signonlycc; + +/* condition-code names, indexed by relop - EQ (used by cbgen and ZV) */ +static char *ccnames[] = { + "eq", /* EQ */ + "ne", /* NE */ + "le", /* LE */ + "lt", /* LT */ + "ge", /* GE */ + "gt", /* GT */ + "ule", /* ULE */ + "ult", /* ULT */ + "uge", /* UGE */ + "ugt", /* UGT */ +}; + +void +zzzcode(NODE *p, int c) +{ + NODE *l; + int n; + char *op; + + switch (c) { + case 'G': /* word register containing the byte left operand */ + bwput(getlr(p, 'L')); + break; + case 'H': /* word register containing the byte result A1 */ + bwput(getlr(p, '1')); + break; + + case 'K': /* byte -> word: copy the byte's containing word + * into the class-A result A1, whose low byte the + * following extsb/and then extends in place. With + * NSL sharing A1 often IS that word: emit nothing. */ + l = getlr(p, 'L'); + if (l->n_op != REG || l->n_rval < RL0) + comperr("ZK: left not a byte register"); + n = getlr(p, '1')->n_rval; + if (n != dword(l->n_rval)) + printf("\tld\t%s,%s\n", + rnames[n], rnames[dword(l->n_rval)]); + break; + + case 'U': /* zero-extend the low byte of the class-A result A1 + * in place (ZK put the uchar's word there): high-byte + * self-subtract when the word has an addressable high + * byte (r0-r7; the 2-byte native idiom), else the + * equivalent 4-byte word and. */ + n = getlr(p, '1')->n_rval; + if (n < 0 || n > 15) + comperr("ZU: result not a word register"); + if (n <= 7) + printf("\tsubb\trh%d,rh%d\n", n, n); + else + printf("\tand\t%s,$0xff\n", rnames[n]); + break; + + case 'A': /* block memory clear via overlapping ldirb. AL is the + * buffer's first byte (frame OREG); the node's n_lval is + * the byte count. Clear byte 0, then copy it forward: + * ldirb propagates the zero through the whole block. + * The node result pair is the dst (used and discarded - + * DECRA packs only 3 regs, so the result slot doubles as + * a scratch); A2 = src pair, A1 = byte count. */ + { + int dst = getlr(p, 'D')->n_rval; /* result pair, used as dst */ + int src = getlr(p, '2')->n_rval; /* B scratch pair (src) */ + int cnt = getlr(p, '1')->n_rval; /* A count register */ + + printf("\tlda\t%s,", rnames[dst]); /* dst = &buf[0] */ + expand(p, FOREFF, "AL"); + printf("\n\tand\t"); + prword(getlr(p, 'D'), 0); /* normalise segment word */ + printf(",$32512\n"); + printf("\tclrb\t(%s)\n", rnames[dst]); /* buf[0] = 0 */ + printf("\tldl\t%s,%s\n", rnames[src], rnames[dst]); /* src = &buf[0] */ + printf("\tinc\t"); + prword(getlr(p, 'D'), 1); /* dst = &buf[1] */ + printf(",$1\n"); + printf("\tld\t%s,$%d\n", rnames[cnt], p->n_rval - 1); + printf("\tldirb\t(%s),(%s),%s\n", + rnames[dst], rnames[src], rnames[cnt]); + } + break; + + case 'Z': /* sparse-switch cpir dispatch. Search the .word + * case-value table (label d->ltab) for the switch + * value AL; on no match branch to the default; else + * load the matched case's 16-bit TARGET OFFSET from the + * parallel .word offset table into the search pair's LOW + * word, leaving its high (segment) word - which cpir did + * not touch - pointing at this (the code) segment; the + * outer GOTO(SWDISP) then jumps through the pair. All + * case bodies share the dispatch's segment, so a 16-bit + * offset suffices (half the size of a .long address). + * A2 = search pair, A1 = count. Displacement 2N-2: + * after cpir the low word is 2*(idx+1) past the table + * base; the .word target table starts 2N bytes in, so + * target idx sits at base + 2N + 2*idx = + * (base + 2N-2) + 2*(idx+1). */ + { + struct swdesc *d = (struct swdesc *)p->n_name; + int cnt = getlr(p, '1')->n_rval; /* A1 = A count word */ + int pair = getlr(p, '2')->n_rval; /* A2 = B search pair */ + int lo = (pair - RR0) * 2 + 1; /* pair's low word */ + + printf("\tld\t%s,$%d\n", rnames[cnt], d->n); + printf("\tldl\t%s,$" LABFMT "\n", rnames[pair], d->ltab); + printf("\tcpir\t"); + expand(p, FOREFF, "AL"); + printf(",(%s),%s,eq\n", rnames[pair], rnames[cnt]); + printf("\tjr\tne," LABFMT "\n", d->deflab); + printf("\tsub\t%s,$" LABFMT "\n", rnames[lo], d->ltab); + printf("\tld\t%s," LABFMT "+%d(%s)\n", + rnames[lo], d->ltab, 2 * d->n - 2, rnames[lo]); + } + break; + + case 'O': /* the test/testl this template printed sets S and Z + * but leaves P/V alone, so the signed conditions + * lt/ge (S xor V) would read a stale V: make the + * following cbgen branch on the sign flag alone. */ + signonlycc = 1; + break; + + case 'V': /* value-context relational: the cc name of this + * OPLOG itself, non-negated (the compare is already + * printed; tcc sets bit 0 of A1 when cc holds). */ + if (p->n_op < EQ || p->n_op > UGT) + comperr("ZV: op %d not a relop", p->n_op); + printf("%s", ccnames[p->n_op - EQ]); + break; + + case 'J': /* bit number of the single-bit mask in the right + * ICON (an SPOW2 shape), for the bit/bitb test + * rules and set: "x & 0x40" prints as "$6". */ + { + CONSZ v = getlval(p->n_right); + + if (p->n_right->n_op != ICON) + comperr("ZJ: right not ICON"); + v &= (p->n_type == CHAR || p->n_type == UCHAR) ? + 0xff : 0xffff; + if (v == 0 || (v & (v - 1)) != 0) + comperr("ZJ: not a single-bit mask"); + for (n = 0; (v & 1) == 0; v >>= 1) + n++; + printf("$%d", n); + } + break; + + case 'Y': /* bit number of the single CLEAR bit in the right + * ICON (an SNPOW2 shape), for res: + * "x &= ~0x40" prints as "$6". */ + { + CONSZ v = getlval(p->n_right); + + if (p->n_right->n_op != ICON) + comperr("ZY: right not ICON"); + v = ~v & ((p->n_type == CHAR || p->n_type == UCHAR) ? + 0xff : 0xffff); + if (v == 0 || (v & (v - 1)) != 0) + comperr("ZY: not a single-clear-bit mask"); + for (n = 0; (v & 1) == 0; v >>= 1) + n++; + printf("$%d", n); + } + break; + + case 'X': /* address constant + word index: lda A1,sym(AR), + * unless the index sits in r0 (an X-mode index + * field of 0 decodes as DA) or the address is a + * nameless constant - those get the ldl+add + * fallback. A1 never overlaps AR (plain NBREG, + * no sharing), so the fallback's ldl cannot + * clobber the index before the add reads it. */ + { + NODE *r = getlr(p, 'R'); + + l = getlr(p, 'L'); + n = getlr(p, '1')->n_rval; + if (r->n_op != REG || l->n_op != ICON) + comperr("ZX: bad operands"); + if (r->n_rval != 0 && l->n_name[0] != '\0') { + printf("\tlda\t%s,%s", rnames[n], l->n_name); + if (getlval(l) != 0) + printf("+" CONFMT, getlval(l)); + printf("(%s)\n", rnames[r->n_rval]); + } else { + printf("\tldl\t%s,$", rnames[n]); + conput(stdout, l); + printf("\n\tadd\t%s,%s\n", + rnames[(n - RR0) * 2 + 1], rnames[r->n_rval]); + } + /* + * Normalise the segment (high) word. lda leaves the reserved + * high bit of the segment byte set; a raw &arr[i] pointer must + * have it clear so that pointer equality works (e.g. comparing + * &arr[i] against a differently-formed pointer to the same + * cell). Mask with $0x7F00 exactly as &local (ZF) and the + * lda-of-name rule (ZM) do. + */ + printf("\tand\t"); + prword(getlr(p, '1'), 0); + printf(",$32512\n"); + } + break; + + case 'I': /* word -> byte: copy the word into the byte result + * A1's containing word; A1 (its low byte) then holds + * the truncated char. The word's high byte is dead + * space - a live class-D value in rlN conflicts with + * any class-A value in rN via ROVERLAP. With NSL + * sharing the words often coincide: emit nothing. */ + l = getlr(p, 'L'); + if (l->n_op != REG || l->n_rval >= RL0) + comperr("ZI: left not a word register"); + n = getlr(p, '1')->n_rval; + if (dword(n) != l->n_rval) + printf("\tld\t%s,%s\n", + rnames[dword(n)], rnames[l->n_rval]); + break; + + case 'L': /* long bitwise: two word ops on the pair halves */ + switch (p->n_op) { + case AND: op = "and"; break; + case OR: op = "or"; break; + case ER: op = "xor"; break; + default: comperr("zzzcode ZL: bad op %d", p->n_op); op = ""; + } + printf("\t%s\t", op); + prword(p->n_left, 0); + printf(","); + prword(p->n_right, 0); + printf("\n\t%s\t", op); + prword(p->n_left, 1); + printf(","); + prword(p->n_right, 1); + printf("\n"); + break; + + case 'M': /* high (segment) word of result pair A1, for the + * post-lda segment-normalising "and rN,$0x7F00" */ + prword(getlr(p, '1'), 0); + break; + + case 'F': /* &local: segmented pointer to a frame object, exactly + * as native does it -- "lda A1,L+off(r13)" using the + * per-function frame-base equate L=SS|framesize, then + * "and A1hi,$32512" to normalise the segment word (see + * echo.s: "lda rr0,L10001-512(r13); and r0,$32512"). + * off is the PLUS/MINUS constant (entry-SP-relative), + * matching the OREG offsets in adrput. */ + { + NODE *res = getlr(p, '1'); + CONSZ off = getlval(p->n_right); + if (p->n_op == MINUS) + off = -off; + printf("\tlda\t%s," LABFMT, + rnames[res->n_rval], framelab); + if (off > 0) + printf("+"); + if (off != 0) + printf(CONFMT, off); + printf("(%s)\n", rnames[p->n_left->n_rval]); + printf("\tand\t"); + prword(res, 0); /* segment (high) word */ + printf(",$32512\n"); + } + break; + + case 'Q': /* clear a pair (reg or mem): clr both halves */ + printf("\tclr\t"); + prword(p->n_left, 0); + printf("\n\tclr\t"); + prword(p->n_left, 1); + printf("\n"); + break; + + case 'C': /* long complement: com both halves */ + printf("\tcom\t"); + prword(p->n_left, 0); + printf("\n\tcom\t"); + prword(p->n_left, 1); + printf("\n"); + break; + + case 'N': /* long negate: ~x then +1 across the full pair */ + printf("\tcom\t"); + prword(p->n_left, 0); + printf("\n\tcom\t"); + prword(p->n_left, 1); + printf("\n\taddl\t"); + adrput(stdout, p->n_left); + printf(",$1\n"); + break; + + case 'B': + /* Stack cleanup after call: n_qual bytes were pushed as args */ + n = p->n_qual; + if (n == 0) + break; + if (n <= 16) + printf("\tinc\tr15,$%d\n", n); + else + printf("\tadd\tr15,$%d\n", n); + break; + + case 'S': /* struct assignment: ldirb (dest),(src),count */ + { + struct attr *ap = attr_find(p->n_ap, ATTR_P2STRUCT); + int sz = ap->iarg(0); + + l = p->n_left; + + /* Resources are ordered by class: A1 = word (count), + * A2 = pair (dest address). AR = source pointer (pair). + * The lda-formed dest address needs its segment word + * normalised before use as an ldir base. + * + * lda has no IR mode, so a pair-based dest OREG cannot go + * through "lda A2,(rrN)": at displacement 0 just copy the + * pair (segment already clean); otherwise use the BA form + * rrN(disp), which lda does accept. */ + if (l->n_op == OREG && l->n_rval >= RR0) { + if (getlval(l) == 0) { + expand(p, FOREFF, "\tldl\tA2,"); + printf("%s\n", rnames[l->n_rval]); + } else { + expand(p, FOREFF, "\tlda\tA2,"); + printf("%s(" CONFMT ")\n", + rnames[l->n_rval], getlval(l)); + printf("\tand\t"); + prword(getlr(p, '2'), 0); + printf(",$32512\n"); + } + } else { + expand(p, FOREFF, "\tlda\tA2,AL\n"); + printf("\tand\t"); + prword(getlr(p, '2'), 0); + printf(",$32512\n"); + } + printf("\tld\t"); + expand(p, FOREFF, "A1"); + printf(",$%d\n", sz); + expand(p, FOREFF, "\tldirb\t(A2),(AR),A1\n"); + } + break; + + case 'R': /* struct-return call setup: push the address of this + * call's own frame buffer (assigned by myreader(), + * carried in ATTR_P2_STBUF) as the hidden argument. + * rr0 is free here: it is caller-saved and anything + * live across the call already excludes it. */ + { + struct attr *ap = attr_find(p->n_ap, ATTR_P2_STBUF); + + if (ap == NULL) + comperr("ZR: struct call without buffer attribute"); + printf("\tlda\trr0," LABFMT "-%d(r13)\n", framelab, ap->iarg(0)); + printf("\tand\tr0,$32512\n"); + printf("\tpushl\t(rr14),rr0\n"); + } + break; + + case 'D': /* double assignment: left <- right, one side a quad */ + { + NODE *r = p->n_right; + + l = p->n_left; + if (l->n_op == REG && r->n_op == REG) { + printf("\tldl\t%s,%s\n", + qpair(l->n_rval, 0), qpair(r->n_rval, 0)); + printf("\tldl\t%s,%s\n", + qpair(l->n_rval, 1), qpair(r->n_rval, 1)); + } else if (l->n_op == REG) + quadmem(r, l->n_rval, 0); + else if (r->n_op == REG) + quadmem(l, r->n_rval, 1); + else + comperr("ZD: no quad register operand"); + } + break; + + case 'E': /* double load: AL (leaf or quad reg) -> quad A1 */ + { + int q = getlr(p, '1')->n_rval; + + l = getlr(p, 'L'); + if (l->n_op == REG) { + printf("\tldl\t%s,%s\n", + qpair(q, 0), qpair(l->n_rval, 0)); + printf("\tldl\t%s,%s\n", + qpair(q, 1), qpair(l->n_rval, 1)); + } else + quadmem(l, q, 0); + } + break; + + case 'P': /* push a double argument: low pair first, so the + * high (sign+exponent) pair lands at the lower + * address (native: "pushl rr2; pushl rr0"). + * Memory sources push directly; pushl accepts + * DA/X sources (proven: "pushl (rr14),L+4(r13)" + * in factor.s) but not BA, so pair-based OREGs + * are excluded by the rule shapes. */ + l = getlr(p, 'L'); + if (l->n_op == REG) { + printf("\tpushl\t(rr14),%s\n", qpair(l->n_rval, 1)); + printf("\tpushl\t(rr14),%s\n", qpair(l->n_rval, 0)); + } else { + printf("\tpushl\t(rr14),"); + prmem(l, 4); + printf("\n\tpushl\t(rr14),"); + prmem(l, 0); + printf("\n"); + } + break; + + case 'W': /* high (sign+exponent) word of the left operand's + * register, for the sign-flip UMINUS "xor ,$-32768" + * (native negates doubles this way: modf.s + * "xor r0,$-32768") */ + l = getlr(p, 'L'); + if (l->n_op != REG) + comperr("ZW: not a register"); + if (l->n_rval >= RQ0) + printf("r%d", qword(l->n_rval)); + else + prword(l, 0); + break; + + case 'T': /* struct argument passed by value. + * + * Reserve the argument slot at the top of stack with + * "sub r15,$slot" (PCC's own post-call cleanup reclaims + * exactly this, so it is NOT double-counted), then copy + * the struct into it with ldirb. + * + * The ldirb dest needs a valid segmented pointer to the + * slot. We do NOT copy rr14: the SP's segment word is + * not a plain data-segment value usable as an indirect + * base. Instead build A2 = (segment of the source AL) : + * r15 -- AL is a valid pointer to the struct (for a + * stack/local struct it carries the stack segment) and + * r15 is the reserved slot's offset. A1 = byte count. */ + { + struct attr *ap = attr_find(p->n_ap, ATTR_P2STRUCT); + int bytes = ap->iarg(0); /* exact struct size */ + int slot = (bytes + 1) & ~1; /* word-rounded slot */ + + printf("\tsub\tr15,$%d\n", slot); + /* A2.hi = AL.hi (segment); A2.lo = r15 (slot offset) */ + printf("\tld\t"); + prword(getlr(p, '2'), 0); + printf(","); + prword(getlr(p, 'L'), 0); + printf("\n\tld\t"); + prword(getlr(p, '2'), 1); + printf(",r15\n"); + printf("\tld\t"); + expand(p, FOREFF, "A1"); + printf(",$%d\n", bytes); + expand(p, FOREFF, "\tldirb\t(A2),(AL),A1\n"); + } + break; + + case '0': /* materialize a numeric long/ptr constant (SLCON) into + * a pair register. Split into per-word clr/ldk/ld when + * that is no larger than the 6-byte "ldl rrN,#imm32", + * else emit the plain ldl. The destination is the ASSIGN + * left (RDEST) or the OPLTYPE result (RESC1); the constant + * is the other operand. */ + { + NODE *dst, *con; + CONSZ v, hi, lo; + + if (p->n_op == ASSIGN) { + dst = getlr(p, 'L'); + con = getlr(p, 'R'); + } else { + dst = getlr(p, '1'); + con = getlr(p, 'L'); + } + v = getlval(con); + hi = (v >> 16) & 0xffffLL; + lo = v & 0xffffLL; + if (v == 0) { + /* whole pair zero: 2-byte subl, cf. the ASSIGN + * SBREG<-SZERO rule (OPLTYPE has no zero rule). */ + printf("\tsubl\t%s,%s\n", + rnames[dst->n_rval], rnames[dst->n_rval]); + } else if (wordloadcost(hi) + wordloadcost(lo) >= 6) { + /* splitting is no smaller than one ldl #imm32: emit the + * plain ldl, printing the immediate exactly as the + * generic rule's "AR"/"AL" would (adrput). */ + printf("\tldl\t%s,", rnames[dst->n_rval]); + adrput(stdout, con); + printf("\n"); + } else { + prwordload(dst, 0, hi); /* high word */ + prwordload(dst, 1, lo); /* low word */ + } + } + break; + + default: + comperr("zzzcode: unknown code '%c'", c); + } +} + +int +rewfld(NODE *p) +{ + return 1; +} + +int +canaddr(NODE *p) +{ + int o = p->n_op; + + if (o == NAME || o == REG || o == ICON || o == OREG || + (o == UMUL && shumul(p->n_left, STARNM|SOREG))) + return 1; + return 0; +} + +int +flshape(NODE *p) +{ + int o = p->n_op; + + if (o == OREG || o == REG || o == NAME) + return SRDIR; + if (o == UMUL && shumul(p->n_left, SOREG)) + return SROREG; + return SRREG; +} + +int +shtemp(NODE *p) +{ + return 0; +} + +void +adrcon(CONSZ val) +{ + printf("$" CONFMT, val); +} + +void +conput(FILE *fp, NODE *p) +{ + CONSZ val = getlval(p); + + switch (p->n_op) { + case ICON: + if (p->n_name[0] != '\0') { + fprintf(fp, "%s", p->n_name); + if (val != 0) + fprintf(fp, "+" CONFMT, val); + } else { + fprintf(fp, CONFMT, val); + } + return; + default: + comperr("conput: bad op %d", p->n_op); + } +} + +/*ARGSUSED*/ +void +insput(NODE *p) +{ + comperr("insput"); +} + +/* + * upput: print the low (offset) word of a 32-bit value. + * + * For big-endian Z8001 pairs: + * rr0 = r0(high/segment) : r1(low/offset) → upput gives r1 + * rr2 = r2(high) : r3(low) → upput gives r3 + * etc. + * + * For memory: the second word (at offset+2) is the low word. + * For ICON: the low 16 bits. + */ +void +upput(NODE *p, int size) +{ + CONSZ val; + + switch (p->n_op) { + case NAME: + case OREG: + /* Low word is at the higher address (offset+2) */ + setlval(p, getlval(p) + 2); + adrput(stdout, p); + setlval(p, getlval(p) - 2); + break; + case REG: + /* Low word register of a pair: base+1 */ + /* rr0(16): base=(16-16)*2=0, low=1=r1 */ + printf("%s", rnames[(p->n_rval - RR0) * 2 + 1]); + break; + case ICON: + val = getlval(p); + if (p->n_name[0] != '\0') { + /* Symbol address - not split */ + printf("%s", p->n_name); + if (val) + printf("+" CONFMT, val); + } else { + printf("$" CONFMT, val & 0xffffLL); + } + break; + default: + comperr("upput: bad op %d size %d", p->n_op, size); + } +} + +/* + * adrput: print an address operand. + * + * REG: print register name (r0..r12, rr0..rr10) + * OREG: print offset(reg) — r13 used for frame-relative, pairs for pointer-based + * NAME: print symbol name (with optional +offset) + * ICON: print $value (or symbol name for labels) + */ +void +adrput(FILE *io, NODE *p) +{ + CONSZ val; + + if (p->n_op == FLD) + p = p->n_left; + + switch (p->n_op) { + case NAME: + if (p->n_name[0] != '\0') { + fputs(p->n_name, io); + val = getlval(p); + if (val != 0) + fprintf(io, "+" CONFMT, val); + } else { + fprintf(io, CONFMT, getlval(p)); + } + return; + + case OREG: + val = getlval(p); + if (p->n_rval == R13) { + /* + * Frame slot: address in the stack segment via the + * per-function equate, "L+off(r13)". r13 is a WORD + * register, so this is Indexed (X) addressing: the + * address (L+off) is indexed by the word r13. (off + * is the entry-SP-relative node offset; CONFMT prints + * the sign for negatives, positives need an explicit + * '+'.) + */ + fprintf(io, LABFMT, framelab); + if (val > 0) + fputc('+', io); + if (val != 0) + fprintf(io, CONFMT, val); + fprintf(io, "(%s)", rnames[R13]); + } else if (val != 0) { + /* + * Pair-register base + displacement: BASED (BA) + * addressing, written "rrN(disp)" -- the base pair + * comes FIRST, the displacement in parens (cf. native + * "lda rr4, rr10(4)"). Writing "disp(rrN)" would be + * parsed as Indexed mode, using the pair as a 16-bit + * word index (its segment half) -> wrong address. + */ + fprintf(io, "%s(", rnames[p->n_rval]); + fprintf(io, CONFMT, val); + fputc(')', io); + } else { + /* Pair-register base, zero displacement: indirect. */ + fprintf(io, "(%s)", rnames[p->n_rval]); + } + return; + + case ICON: + /* + * Addressable value of a constant: immediate mode, "$" prefix. + * Works for both numeric constants ($5) and symbol addresses + * ($L259, $printf_). Call targets bypass this by using the + * "CL" template code (conput), which prints the bare name. + */ + fputc('$', io); + conput(io, p); + return; + + case REG: + fprintf(io, "%s", rnames[p->n_rval]); + return; + + default: + comperr("adrput: bad op %d", p->n_op); + return; + } +} + +/* + * cbfuse: fuse a decrement-and-test loop branch into a single djnz. + * + * After fusecmp()/rmwrename() a "while (--n)" countdown reads, at emit + * time, as + * CBRANCH(NE(ASSIGN(REG n, PLUS(REG n, -1)), ICON 0), lab) + * which the normal path emits as "dec n,$1 ; jr ne,lab". The Z8000 djnz + * does exactly that in one word - decrement a word register and jump while + * the result is nonzero - so emit "djnz n,lab" instead, saving the word. + * + * Returns 0 (keep the default dec;jr) unless every djnz precondition holds: + * - branch sense NE: djnz jumps while the counter is nonzero; + * - counter in a WORD register: djnz is a 16-bit decrement, so byte + * counters (dbjnz, not handled here), pointer/long pairs and memory + * counters are excluded; + * - step exactly -1, on that same register; + * - target BACKWARD (already emitted this function). djnz cannot branch + * forward, and letting the assembler relax a forward one to "dec;jp" + * would be larger than the "dec;jr" it replaces. + * + * Called from emit()'s CBRANCH path (via TARGET_CBRANCH_FUSE) before the + * compare/branch are generated; returns 1 once it has emitted the branch. + */ +int +cbfuse(NODE *p) +{ + NODE *rel, *asg, *mod, *dst; + + rel = p->n_left; + if (rel->n_op != NE) + return 0; + /* + * Only the elided-compare shape, where the compare itself produced + * no instruction and the RMW child's flags ARE the branch result + * (the same su==0 discriminator emit() uses to pick the child rule). + * Otherwise a separate compare instruction is in play and djnz would + * drop it. + */ + if (rel->n_su != 0) + return 0; + asg = rel->n_left; + if (asg->n_op != ASSIGN) + return 0; + dst = asg->n_left; /* the counter (template AL) */ + mod = asg->n_right; /* the modifying expression */ + if (dst->n_op != REG) + return 0; + if (dst->n_type != INT && dst->n_type != UNSIGNED && + dst->n_type != SHORT && dst->n_type != USHORT) + return 0; + /* + * step must be a decrement by exactly 1 on the same register: + * MINUS(r,1) or PLUS(r,-1) - both select the "dec r,$1" rule. + */ + if (mod->n_left->n_op != REG || regno(mod->n_left) != regno(dst)) + return 0; + if (mod->n_right->n_op != ICON || mod->n_right->n_name[0] != '\0') + return 0; + if (!((mod->n_op == MINUS && getlval(mod->n_right) == 1) || + (mod->n_op == PLUS && getlval(mod->n_right) == -1))) + return 0; + /* djnz is backward-only; a forward target is never a win */ + if (!labseen((int)getlval(p->n_right))) + return 0; + + printf("\tdjnz\t%s," LABFMT "\n", rnames[regno(dst)], + (int)getlval(p->n_right)); + return 1; +} + +/* + * cbgen: emit a conditional branch. + */ +void +cbgen(int o, int lab) +{ + char *cc; + + if (o < EQ || o > UGT) + comperr("cbgen: bad op %d", o); + cc = ccnames[o - EQ]; + if (signonlycc) { + /* the compare was a test/testl (ZO): S is valid but V is + * not, so LT/GE must branch on the sign flag alone */ + signonlycc = 0; + if (o == LT) + cc = "mi"; + else if (o == GE) + cc = "pl"; + else + comperr("cbgen: sign-only cc for op %d", o); + } + printf("\tjr\t%s," LABFMT "\n", cc, lab); +} + +/* + * rmove: emit a register-to-register move. + */ +void +rmove(int s, int d, TWORD t) +{ + if (s == d) + return; + if (s >= RL0 || d >= RL0) { + /* byte (class D) move; both sides are byte registers */ + if (s < RL0 || d < RL0) + comperr("rmove: mixed byte/word %d -> %d", s, d); + printf("\tldb\t%s,%s\n", rnames[d], rnames[s]); + } else if (szty(t) == 4) { + /* 64-bit quad move: two pair moves (quads never overlap) */ + printf("\tldl\t%s,%s\n", qpair(d, 0), qpair(s, 0)); + printf("\tldl\t%s,%s\n", qpair(d, 1), qpair(s, 1)); + } else if (szty(t) == 2) { + /* 32-bit pair move */ + printf("\tldl\t%s,%s\n", rnames[d], rnames[s]); + } else { + /* 16-bit word move */ + printf("\tld\t%s,%s\n", rnames[d], rnames[s]); + } +} + +/* + * COLORMAP: can we add one more register of class c to the allocation set r[]? + * r[i] is non-zero if register i is already in use. + */ +/* + * COLORMAP: can a node of class c be colored, given r[], the count of + * already-colored interfering neighbours in each register class? + * r is indexed by class (r[CLASSA], r[CLASSB]) - NOT by register number. + * + * Class A holds 13 word registers (r0-r12). Class B holds 6 register + * pairs (each overlapping two words; rr0 is reserved, so 5 selectable). + * Class C holds 3 quads (rq0,rq4,rq8), each overlapping 4 words / 2 + * pairs. Class D holds the 8 byte registers rl0-rl7, each overlapping + * one word (and through it one pair / one quad). Worst-case + * (conservative) blocking counts. + */ +int +COLORMAP(int c, int *r) +{ + int num; + + switch (c) { + case CLASSA: + /* a pair blocks 2 words, a quad blocks 4, a byte 1 */ + num = r[CLASSA] + 2 * r[CLASSB] + 4 * r[CLASSC] + r[CLASSD]; + return num < 13; + case CLASSB: + /* 5 allocatable pairs (rr2..rr10; rr0 reserved); a word, + * pair or byte neighbour blocks 1 pair, a quad blocks 2 */ + num = r[CLASSB] + r[CLASSA] + 2 * r[CLASSC] + r[CLASSD]; + return num < 5; + case CLASSC: + /* any neighbour blocks at most 1 of the 3 quads */ + num = r[CLASSA] + r[CLASSB] + r[CLASSC] + r[CLASSD]; + return num < 3; + case CLASSD: + /* a word neighbour blocks (at most) its own byte register, + * a pair blocks 2 bytes, a quad blocks 4 */ + num = r[CLASSD] + r[CLASSA] + 2 * r[CLASSB] + 4 * r[CLASSC]; + return num < 8; + } + return 0; +} + +/* + * pickcolor: the PICKCOLOR hook consulted by AssignColors' fallback + * (regs.c colfind) when no move-related recommendation exists. + * + * Caller-saved registers come first in their usual lowest-first order, + * so allocations that fit in scratch registers are unchanged. The + * callee-saved registers are preferred TOP-DOWN (r12 before r6, rr10 + * before rr8 before rr6, rl7 before rl6): the prologue saves one + * CONTIGUOUS block from the lowest used callee register through r13 + * (ldm), so its cost in stack words and cycles is set by that lowest + * register alone. Native cc shows the same bias (common prologue is + * "ldm r8,$6", not "ldm r6,$8"). + * + * The tables list COLOR indices (positions in the mkext-generated rmap + * order), not register numbers: class A colors 0-12 are r0-r12, class + * B colors 0-5 are rr0,rr2,rr4,rr6,rr8,rr10 (rr0 is cleared from + * clregs and never appears in the mask), class C colors 0-2 are + * rq0,rq4,rq8, class D colors 0-7 are rl0-rl7. + */ +int +pickcolor(int class, int mask) +{ + static const signed char aorder[] = + { 0, 1, 2, 3, 4, 5, 12, 11, 10, 9, 8, 7, 6, -1 }; + static const signed char border[] = /* rr2 rr4 rr10 rr8 rr6 rr0 */ + { 1, 2, 5, 4, 3, 0, -1 }; + static const signed char corder[] = /* rq0 rq8 rq4(r6,r7) */ + { 0, 2, 1, -1 }; + static const signed char dorder[] = /* rl0-rl5, rl7 before rl6 */ + { 0, 1, 2, 3, 4, 5, 7, 6, -1 }; + const signed char *o; + int i; + + switch (class) { + case CLASSA: o = aorder; break; + case CLASSB: o = border; break; + case CLASSC: o = corder; break; + default: o = dorder; break; + } + for (i = 0; o[i] >= 0; i++) + if (mask & (1 << o[i])) + return o[i]; + comperr("pickcolor: empty mask class %d", class); + return 0; +} + +/* + * Return the register class suitable for a value of type t. + * Consistent with PCLASS in macdefs.h: char uses class D (byte + * registers), 64-bit values (double) class C (quads), 32-bit values + * (long/ptr/float) class B (pairs), everything else class A (words). + */ +int +gclass(TWORD t) +{ + if (t == CHAR || t == UCHAR) + return CLASSD; + return szty(t) == 4 ? CLASSC : szty(t) == 2 ? CLASSB : CLASSA; +} + +/* + * Return the number of bytes an argument of this type occupies on the stack. + */ +static int +argsiz(NODE *p) +{ + TWORD t = p->n_type; + + if (t == LONG || t == ULONG || t == FLOAT || ISPTR(t)) + return 4; + if (t == DOUBLE || t == LDOUBLE || t == LONGLONG || t == ULONGLONG) + return 8; + if (t == STRTY || t == UNIONTY) + /* word-rounded to keep the stack aligned (matches ZT) */ + return (attr_find(p->n_ap, ATTR_P2STRUCT)->iarg(0) + 1) & ~1; + return 2; /* char/short/int promoted to word */ +} + +/* + * Compute the total size of arguments to a call, stored in n_qual so the + * 'ZB' zzzcode escape can emit the post-call stack adjustment. + */ +void +lastcall(NODE *p) +{ + NODE *op = p; + int size = 0; + + p->n_qual = 0; + if (p->n_op != CALL && p->n_op != FORTCALL && p->n_op != STCALL && + p->n_op != UCALL && p->n_op != USTCALL) + return; + /* the ZR escape pushes the hidden struct-return pointer */ + if (p->n_op == STCALL || p->n_op == USTCALL) + size = 4; + if (p->n_right != NIL) { + for (p = p->n_right; p->n_op == CM; p = p->n_left) + size += argsiz(p->n_right); + size += argsiz(p); + } + op->n_qual = size; +} + +/* + * Special shape matching. See SNBA/SFRAME in macdefs.h: the based address + * mode rrN(disp) exists only for the ld-family, so non-load rules use these + * shapes to accept just the OREG flavours every instruction can encode. + * Frame OREGs (base r13) print as L+off(r13) = X mode; pair-base OREGs + * with zero displacement print as (rrN) = IR mode. + */ +int +special(NODE *p, int shape) +{ + switch (shape) { + case SNBA: + if (p->n_op == UMUL) { + /* + * A dereference whose address is a bare pair + * register/temp will fold to a ZERO-displacement + * OREG (IR mode, "(rrN)") at emit time: offstar + * puts the address in a pair and gencode's canon + * turns UMUL(REG) into the OREG. IR is legal in + * every SNBA consumer, so accept it as SROREG. + * PLUS/MINUS(base,con) addresses must NOT be + * accepted: they fold to a displaced pair base = + * BA mode, which these instructions cannot encode. + */ + NODE *q = p->n_left; + if (q->n_op == TEMP && szty(q->n_type) == 2) + return SROREG; + if (q->n_op == REG && szty(q->n_type) == 2 && + q->n_rval != FPREG) + return SROREG; + return SRNOPE; + } + if (p->n_op != OREG) + return SRNOPE; + if (p->n_rval == FPREG) + return SRDIR; + if (p->n_rval >= RR0 && getlval(p) == 0) + return SRDIR; + return SRNOPE; + case SFRAME: + if (p->n_op == OREG && p->n_rval == FPREG) + return SRDIR; + return SRNOPE; + case SR13: + if (p->n_op == REG && p->n_rval == FPREG) + return SRDIR; + return SRNOPE; + case SLDK: + if (p->n_op == ICON && p->n_name[0] == '\0' && + getlval(p) >= 0 && getlval(p) <= 15) + return SRDIR; + return SRNOPE; + case SP16: + if (p->n_op == ICON && p->n_name[0] == '\0' && + getlval(p) >= 1 && getlval(p) <= 16) + return SRDIR; + return SRNOPE; + case SPCON: + if (p->n_op == ICON && p->n_name[0] == '\0' && + getlval(p) >= -32768 && getlval(p) <= 65535) + return SRDIR; + return SRNOPE; + case SLCON: + /* any numeric (non-symbolic) constant: the Z0 escape decides + * per-value whether the per-word split beats a plain ldl. */ + if (p->n_op == ICON && p->n_name[0] == '\0') + return SRDIR; + return SRNOPE; + case SPOW2: { + /* + * A nameless ICON that is a single-bit mask within its + * type's width, in either sign representation (bit 15 of + * a word may arrive as 32768 or -32768). The ZJ escape + * prints it as the bit number for bit/bitb. + */ + CONSZ v = getlval(p), vv; + int w; + + if (p->n_op != ICON || p->n_name[0] != '\0') + return SRNOPE; + w = (p->n_type == CHAR || p->n_type == UCHAR) ? 8 : 16; + vv = v & ((((CONSZ)1) << w) - 1); + if (vv == 0 || (vv & (vv - 1)) != 0) + return SRNOPE; + if (v != vv && v != vv - (((CONSZ)1) << w)) + return SRNOPE; + return SRDIR; + } + case SNPOW2: { + /* + * The complement of SPOW2: a nameless ICON with every bit + * of its type's width set except one ("x &= ~mask"; the + * res operand). Same two sign representations: ~8 on a + * word arrives as -9 or 65527. ZY prints the clear + * bit's number. + */ + CONSZ v = getlval(p), vv, m; + int w; + + if (p->n_op != ICON || p->n_name[0] != '\0') + return SRNOPE; + w = (p->n_type == CHAR || p->n_type == UCHAR) ? 8 : 16; + m = (((CONSZ)1) << w) - 1; + vv = ~v & m; /* the single bit that is CLEAR in v */ + if (vv == 0 || (vv & (vv - 1)) != 0) + return SRNOPE; + if (v != (v & m) && v != (v & m) - (m + 1)) + return SRNOPE; + return SRDIR; + } + } + return SRNOPE; +} + +/* + * Bitfield expansion - not used (no FIELDOPS). + */ +int +fldexpand(NODE *p, int cookie, char **cp) +{ + return 0; +} + +/* + * Target-dependent command-line option handling. + */ +void +mflags(char *str) +{ +} + +/* + * Target-dependent handling of inline-asm operands. + */ +int +myxasm(struct interpass *ip, NODE *p) +{ + return 0; +} + +/* + * Fuse an assignment with the compare of the value it just stored: + * + * ASSIGN(TEMP t, expr) ; CBRANCH(relop(TEMP t, 0)) + * + * adjacent in the interpass list becomes + * + * CBRANCH(relop(ASSIGN(TEMP t, expr), 0)) + * + * An ASSIGN yields its stored value, so this is exactly the tree pass 1 + * builds for "if ((t = expr))" - a shape the whole pipeline already + * handles. When expr is a read-modify-write of t, the compare-vs-zero + * elision in geninsn then branches on the operation's own flags + * ("dec r5,$1 ; jr ne" instead of dec + test + jr, gated per-op by + * CCOKFORCOMP); otherwise pass 2 generates exactly the unfused code. + * Integer and pointer temps only: float compares are rewritten to + * fcomp helper calls and must keep their canonical shape. + * + * Runs from myoptim, i.e. AFTER deljumps/SSA/DCE and just before + * register allocation: SSA splits exactly this kind of tree back into + * the two-statement form (pass1's own "if ((t = expr))" trees + * included), so fusing any earlier is undone at -O1. + * + * The basic blocks built during optimize() are REUSED by the register + * allocator's liveness analysis (Build/LivenessAnalysis walk each + * block from bb->last back to bb->first in the circular interpass + * list) - removing an interpass that a bb->first points at would make + * that walk miss its terminator and loop forever (ar.c update(): the + * fused ASSIGN was the first statement of the for-body block). Patch + * any block boundary that names the removed element. + */ +static void +fusecmp(struct interpass *ipole) +{ + struct interpass *ip, *inext; + struct basicblock *bb; + NODE *p, *q; + + for (ip = DLIST_NEXT(ipole, qelem); ip != ipole; ip = inext) { + inext = DLIST_NEXT(ip, qelem); + if (ip->type != IP_NODE || inext == ipole || + inext->type != IP_NODE) + continue; + p = ip->ip_node; + if (p->n_op != ASSIGN || p->n_left->n_op != TEMP) + continue; + if (!KEEPLOGOPVALUE_T(p->n_left->n_type)) + continue; + q = inext->ip_node; + if (q->n_op != CBRANCH) + continue; + q = q->n_left; + if (q->n_op < EQ || q->n_op > UGT) + continue; + if (q->n_left->n_op != TEMP || + regno(q->n_left) != regno(p->n_left)) + continue; + if (q->n_right->n_op != ICON || getlval(q->n_right) != 0 || + q->n_right->n_name[0] != '\0') + continue; + nfree(q->n_left); + q->n_left = p; + if (xtemps || xssa) { /* blocks exist iff optimize built + * them - same condition it uses */ + DLIST_FOREACH(bb, &p2env.bblocks, bbelem) { + if (bb->first == ip) + bb->first = inext; + if (bb->last == ip) + bb->last = DLIST_PREV(ip, qelem); + } + } + DLIST_REMOVE(ip, qelem); + } +} + +/* + * Count definitions and uses of TEMP number num in a tree. A TEMP + * that is the direct target of an ASSIGN is a definition; every + * other occurrence is a use. + */ +static void +tmpcount(NODE *p, int num, int *defs, int *uses) +{ + if (p->n_op == ASSIGN && p->n_left->n_op == TEMP) { + if (regno(p->n_left) == num) + (*defs)++; + tmpcount(p->n_right, num, defs, uses); + return; + } + switch (optype(p->n_op)) { + case BITYPE: + tmpcount(p->n_right, num, defs, uses); + /* FALLTHROUGH */ + case UTYPE: + tmpcount(p->n_left, num, defs, uses); + return; + default: + if (p->n_op == TEMP && regno(p) == num) + (*uses)++; + } +} + +/* + * Recover the -O0 read-modify-write shape at -O1. + * + * SSA renames the two halves of pass 1's fused countdown tree, so + * after removephi a "while (--n)" loop reads + * + * ASSIGN(TEMP s, TEMP x) entry copy + * L: CBRANCH(rel(ASSIGN(TEMP d, op(TEMP s, e)), 0)) + * ... body uses TEMP d ... + * ASSIGN(TEMP s, TEMP d) back-edge copy + * GOTO L + * + * and findmops' treecmp(d, s) can never match, so the compare-vs-zero + * elision that works at -O0 ("dec r5,$1 ; jr ne") is lost; the + * register allocator does coalesce d and s into one register, but by + * then geninsn has already emitted the separate test. + * + * When the whole-function interpass list proves this exact structure + * - d defined only by the RMW, s used only by the RMW, and s defined + * by exactly two plain top-level temp-to-temp copies, one before the + * RMW (from neither d nor s) and one after it reading d - renaming s + * to d is a semantics-preserving coalesce: the entry copy then loads + * d, the back-edge copy becomes a self-copy and is deleted, and the + * RMW reads and writes one temp, so the elision fires again. The + * two-copy requirement is what makes the rename sound: it pins the + * removephi phi-lowering shape where every path back to the RMW + * passes one of the (consistently renamed) copies, and rejects + * aliasing shapes like "while (n = m - 1)" where m stays live. + * Gated on xssa: only SSA produces the split-temp shape, and the + * soundness argument leans on removephi's every-edge-gets-a-copy + * invariant. Runs after fusecmp, which builds the fused tree for + * the pairs SSA split completely apart. + * + * The same basic-block boundary rule as fusecmp applies to the + * deleted copy: the register allocator reuses optimize()'s blocks. + */ +static void +rmwrename(struct interpass *ipole) +{ + struct interpass *ip, *ip2, *entrycp, *backcp; + struct basicblock *bb; + NODE *p, *q, *rmw, *r; + int d, s, seen, bad, ddefs, duses, sdefs, suses, du, dd; + + if (!xssa) + return; + for (ip = DLIST_NEXT(ipole, qelem); ip != ipole; + ip = DLIST_NEXT(ip, qelem)) { + if (ip->type != IP_NODE) + continue; + p = ip->ip_node; + if (p->n_op != CBRANCH) + continue; + q = p->n_left; + if (q->n_op < EQ || q->n_op > UGT) + continue; + rmw = q->n_left; + if (rmw->n_op != ASSIGN || rmw->n_left->n_op != TEMP) + continue; + r = rmw->n_right; + if (r->n_op != PLUS && r->n_op != MINUS && r->n_op != AND && + r->n_op != OR && r->n_op != ER) + continue; + if (r->n_left->n_op != TEMP) + continue; + d = regno(rmw->n_left); + s = regno(r->n_left); + if (d == s || rmw->n_left->n_type != r->n_left->n_type) + continue; + /* the modifier expression may mention neither temp */ + dd = du = 0; + tmpcount(r->n_right, d, &dd, &du); + tmpcount(r->n_right, s, &dd, &du); + if (dd || du) + continue; + + ddefs = duses = sdefs = suses = 0; + entrycp = backcp = NULL; + seen = bad = 0; + for (ip2 = DLIST_NEXT(ipole, qelem); ip2 != ipole; + ip2 = DLIST_NEXT(ip2, qelem)) { + if (ip2->type != IP_NODE) + continue; + q = ip2->ip_node; + if (ip2 == ip) { + /* the candidate: 1 d-def + 1 s-use by + * shape; anything more counts below and + * fails the ddefs/suses/sdefs checks */ + seen = 1; + } else if (q->n_op == ASSIGN && + q->n_left->n_op == TEMP && + regno(q->n_left) == s && + q->n_right->n_op == TEMP) { + if (seen == 0 && entrycp == NULL && + regno(q->n_right) != d && + regno(q->n_right) != s) { + /* entry copy: 1 s-def, no other + * d/s references */ + entrycp = ip2; + sdefs++; + continue; + } + if (seen == 1 && backcp == NULL && + regno(q->n_right) == d) { + /* back-edge copy: 1 s-def, 1 d-use */ + backcp = ip2; + sdefs++; + duses++; + continue; + } + bad = 1; /* copy in the wrong place */ + break; + } + tmpcount(q, d, &ddefs, &duses); + tmpcount(q, s, &sdefs, &suses); + } + if (bad || ddefs != 1 || suses != 1 || sdefs != 2 || + entrycp == NULL || backcp == NULL) + continue; + + /* rename s to d; the back-edge copy becomes dead */ + entrycp->ip_node->n_left->n_rval = d; + r->n_left->n_rval = d; + DLIST_FOREACH(bb, &p2env.bblocks, bbelem) { + if (bb->first == backcp) + bb->first = DLIST_NEXT(backcp, qelem); + if (bb->last == backcp) + bb->last = DLIST_PREV(backcp, qelem); + } + tfree(backcp->ip_node); + DLIST_REMOVE(backcp, qelem); + } +} + +/* + * Recover the read-modify-write shape for a NON-loop store-and-test. + * + * The loop case above needs removephi's two edge-copies for soundness. + * A straight-line "if ((x -= y))" (or &=, |=, ^=, +=) leaves a simpler + * residue: SSA numbers the stored and read halves apart but inserts NO + * copies, because there is no control-flow merge feeding the read - + * + * ASSIGN(TEMP s, expr) single definition of s + * ... (no other reference to s) ... + * CBRANCH(rel(ASSIGN(TEMP d, op(TEMP s, e)), 0)) + * ... body/afterwards uses TEMP d ... + * + * so findmops' treecmp(d, s) still fails and the compare is emitted as a + * separate test. Renaming s to d is sound here for a different reason + * than the loop copies: in SSA a temp with a SINGLE definition (no phi, + * hence sdefs==1 after removephi) has that definition dominating every + * use, so the one s-def dominates the RMW; and because s is used ONLY by + * the RMW (suses==1) nothing else observes it. After the rename the s-def + * writes d and the RMW reads/writes d in place, exactly the -O0 shape. + * Nothing is deleted, so no basic-block boundary needs patching. + * + * Runs after rmwrename(); a site rmwrename() already collapsed now has + * d==s and is skipped here by the d==s guard. Gated on xssa: only SSA + * splits the two halves apart, and the soundness rests on the SSA + * single-def-dominates-use property. + */ +static void +rmwrenamesd(struct interpass *ipole) +{ + struct interpass *ip, *ip2, *sdef; + NODE *p, *q, *rmw, *r; + int d, s, seen, bad, ddefs, duses, sdefs, suses, du, dd; + + if (!xssa) + return; + for (ip = DLIST_NEXT(ipole, qelem); ip != ipole; + ip = DLIST_NEXT(ip, qelem)) { + if (ip->type != IP_NODE) + continue; + p = ip->ip_node; + if (p->n_op != CBRANCH) + continue; + q = p->n_left; + /* + * EQ/NE only: those read the Z flag alone, which every one of + * the ops below sets to (result == 0). An ordered relop would + * need S/V/C, and after the in-place op those differ from a + * "cp result,$0" - only harmlessly (signed overflow is UB) for + * signed, but genuinely wrong for unsigned wrap - so leave the + * separate compare in place for anything but EQ/NE. + */ + if (q->n_op != EQ && q->n_op != NE) + continue; + rmw = q->n_left; + if (rmw->n_op != ASSIGN || rmw->n_left->n_op != TEMP) + continue; + r = rmw->n_right; + if (r->n_op != PLUS && r->n_op != MINUS && r->n_op != AND && + r->n_op != OR && r->n_op != ER) + continue; + if (r->n_left->n_op != TEMP) + continue; + d = regno(rmw->n_left); + s = regno(r->n_left); + if (d == s || rmw->n_left->n_type != r->n_left->n_type) + continue; + /* the modifier expression may mention neither temp */ + dd = du = 0; + tmpcount(r->n_right, d, &dd, &du); + tmpcount(r->n_right, s, &dd, &du); + if (dd || du) + continue; + + ddefs = duses = sdefs = suses = 0; + sdef = NULL; + seen = bad = 0; + for (ip2 = DLIST_NEXT(ipole, qelem); ip2 != ipole; + ip2 = DLIST_NEXT(ip2, qelem)) { + if (ip2->type != IP_NODE) + continue; + q = ip2->ip_node; + if (ip2 == ip) { + /* candidate: 1 d-def + 1 s-use by shape, + * counted through the fall-through below */ + seen = 1; + } else if (q->n_op == ASSIGN && + q->n_left->n_op == TEMP && regno(q->n_left) == s) { + /* the definition of s: must be the only one, + * lie before the RMW, and source neither temp */ + if (seen != 0 || sdef != NULL) { + bad = 1; + break; + } + dd = du = 0; + tmpcount(q->n_right, d, &dd, &du); + tmpcount(q->n_right, s, &dd, &du); + if (dd || du) { + bad = 1; + break; + } + sdef = ip2; + sdefs++; + continue; + } + tmpcount(q, d, &ddefs, &duses); + tmpcount(q, s, &sdefs, &suses); + } + if (bad || ddefs != 1 || suses != 1 || sdefs != 1 || + sdef == NULL) + continue; + + /* rename s to d: the s-def now writes d, the RMW is in place */ + sdef->ip_node->n_left->n_rval = d; + r->n_left->n_rval = d; + } +} + +/* + * Recover the -O0 read-modify-write shape for a loop INDUCTION variable + * whose value is LIVE across the body - the mirror of rmwrename(). + * + * A "for (t = 0; t < N; t++) use(t)" loop, after SSA + removephi, reads + * + * ASSIGN(TEMP s, init) entry def of s + * L: ... body USES TEMP s (the compare, printf args, ...) ... + * ASSIGN(TEMP d, op(TEMP s, e)) the update, a bare statement + * ASSIGN(TEMP s, TEMP d) back-edge copy (reads d) + * GOTO L + * + * Here s is read all over the body, so rmwrename()'s "s used only by the + * RMW" (suses==1) never holds and the split shape survives; the register + * allocator then fails to make the update in place and emits the classic + * "ldl rr2,rr10 ; addl rr2,$1 ; ldl rr10,rr2". + * + * But d is single-use: defined only by the update and read only by the + * immediately-following back-edge copy. Renaming d to s - the OPPOSITE + * direction from rmwrename() - makes the update write s in place and turns + * the copy into the dead "s = s", which is deleted. Soundness: d has + * exactly one def (the update) and one use (the copy), so removing it + * observes nothing else; and because the copy IMMEDIATELY follows the + * update, no reader of the old s value lies between where s is now + * overwritten and where the copy originally overwrote it. Only a rename + * and a delete, both safe on the allocator's reused blocks (see fusecmp). + * + * Gated on xssa; runs after rmwrename()/rmwrenamesd(), whose (compare- + * wrapped) sites do not match the bare-statement shape required here. + */ +static void +rmwrenameloop(struct interpass *ipole) +{ + struct interpass *ip, *ip2, *backcp; + struct basicblock *bb; + NODE *p, *q, *r; + int d, s, ddefs, duses, dd, du; + + if (!xssa) + return; + for (ip = DLIST_NEXT(ipole, qelem); ip != ipole; + ip = DLIST_NEXT(ip, qelem)) { + if (ip->type != IP_NODE) + continue; + p = ip->ip_node; + /* the update: a bare top-level "d = op(s, e)" statement */ + if (p->n_op != ASSIGN || p->n_left->n_op != TEMP) + continue; + r = p->n_right; + if (r->n_op != PLUS && r->n_op != MINUS && r->n_op != AND && + r->n_op != OR && r->n_op != ER) + continue; + if (r->n_left->n_op != TEMP) + continue; + d = regno(p->n_left); + s = regno(r->n_left); + if (d == s || p->n_left->n_type != r->n_left->n_type) + continue; + /* the modifier expression may mention neither temp */ + dd = du = 0; + tmpcount(r->n_right, d, &dd, &du); + tmpcount(r->n_right, s, &dd, &du); + if (dd || du) + continue; + /* the LITERAL next element must be the back-edge copy "s = d": + * an intervening label would let control reach the copy without + * the update, and any statement between could read the old s. */ + backcp = DLIST_NEXT(ip, qelem); + if (backcp == ipole || backcp->type != IP_NODE) + continue; + q = backcp->ip_node; + if (q->n_op != ASSIGN || q->n_left->n_op != TEMP || + regno(q->n_left) != s || q->n_right->n_op != TEMP || + regno(q->n_right) != d) + continue; + /* d must be defined only by the update and used only by the + * back-edge copy: then eliminating it observes nothing else */ + ddefs = duses = 0; + for (ip2 = DLIST_NEXT(ipole, qelem); ip2 != ipole; + ip2 = DLIST_NEXT(ip2, qelem)) { + if (ip2->type != IP_NODE) + continue; + tmpcount(ip2->ip_node, d, &ddefs, &duses); + } + if (ddefs != 1 || duses != 1) + continue; + + /* rename d to s: the update writes s in place, the back-edge + * copy becomes "s = s" and is removed */ + p->n_left->n_rval = s; + DLIST_FOREACH(bb, &p2env.bblocks, bbelem) { + if (bb->first == backcp) + bb->first = DLIST_NEXT(backcp, qelem); + if (bb->last == backcp) + bb->last = DLIST_PREV(backcp, qelem); + } + tfree(backcp->ip_node); + DLIST_REMOVE(backcp, qelem); + } +} + +/* + * NOTE (attempted, reverted): a "rmwcopyfuse" that handled the LIVE-source + * non-loop RMW-test (the majority of the corpus's residual redundant tests, + * e.g. "if ((x = a op b))" with a used later) by INSERTING an explicit + * copy "d = s" at myoptim and rewriting the op in place - + * ASSIGN(d, s) ; CBRANCH(rel(ASSIGN(d, op(d, e)), 0)) + * - was semantically correct but destabilised the -xssa register allocator: + * inserting a fresh IP node here crashes insnwalk()'s interference build + * (rmwrename/rmwrenamesd only rename or delete nodes, which is safe; adding + * one is not). A sound version needs the copy materialised where liveness + * is rebuilt (or a post-regalloc peephole), not a mid-stream insert. Left + * for future work; the dead-source case is handled by rmwrenamesd() above. + */ + +void +myoptim(struct interpass *ip) +{ + fusecmp(ip); + rmwrename(ip); + rmwrenamesd(ip); + rmwrenameloop(ip); +} + +/* + * Software floating point. + * + * The Z8001 in the Commodore 900 has no FPU; the native Coherent + * compiler lowers all floating-point operations to calls into the libc + * runtime (libc/crt/{dadd,dcmp,dmul,ddiv,dtoi,itod,dtof,ftod}.s). We + * follow the exact same convention (ground truth: the compiler-emitted + * call sites in factor.s/modf.s and the helper sources themselves): + * + * value/arg convention: a double travels in rq0 (r0=sign+exponent), + * and is pushed "pushl rr2; pushl rr0" (8 bytes, caller pops); + * dradd/drsub/drmul/drdiv(da, db): both by value, result in rq0 + * (the dl* variants taking &db are just a size optimization); + * drcmp(da, db): three-way result in r1 (1/0/-1), compared against 0; + * diflt/duflt/dlflt/dvflt(i): int/unsigned/long/ulong -> double, rq0; + * ifix/ufix(d) -> r1, lfix/vfix(d) -> rr0: double -> integer; + * dfpack: float rr0 -> double rq0, fdpack: double rq0 -> float rr0 + * (register-based, no stack args; handled as table SCONV rules); + * negation is inline: "xor ,$-32768" (table UMINUS rules). + * + * Float arithmetic is computed in double (K&R style - the runtime has + * no float helpers), converting operands in and the result back out. + * + * The rewrite runs from myreader(), before canon/sucomp, so the created + * CALL nodes get the full standard treatment (argument FUNARG pushes, + * n_qual cleanup via lastcall, pre-evaluation into temps when several + * calls appear in one tree). + */ + +#define ISFPT(t) ((t) == FLOAT || (t) == DOUBLE || (t) == LDOUBLE) + +/* rewrite a unary node into a one-argument helper call */ +static void +mkcall(NODE *p, char *name) +{ + p->n_op = CALL; + p->n_right = mkunode(FUNARG, p->n_left, 0, p->n_left->n_type); + p->n_left = mklnode(ICON, 0, 0, FTN|p->n_type); + p->n_left->n_name = name; +} + +/* rewrite a binary node into a two-argument helper call; the CM chain + * is evaluated rightmost-first (gencode), so the left operand is pushed + * last and lands in the first-argument slot */ +static void +mkcall2(NODE *p, char *name) +{ + p->n_op = CALL; + p->n_right = mkunode(FUNARG, p->n_right, 0, p->n_right->n_type); + p->n_left = mkunode(FUNARG, p->n_left, 0, p->n_left->n_type); + p->n_right = mkbinode(CM, p->n_left, p->n_right, INT); + p->n_left = mklnode(ICON, 0, 0, FTN|p->n_type); + p->n_left->n_name = name; +} + +/* wrap p's current contents in a new child node and make p an SCONV + * to type t */ +static void +wrapsconv(NODE *p, TWORD t) +{ + NODE *q = talloc(); + + *q = *p; + p->n_op = SCONV; + p->n_left = q; + p->n_type = t; +} + +static NODE * +mksconv(NODE *p, TWORD t) +{ + return mkunode(SCONV, p, 0, t); +} + +static void +fixfloatops(NODE *p, void *arg) +{ + NODE *l, *r; + TWORD t = p->n_type, lt; + char *fn; + + switch (p->n_op) { + case PLUS: + case MINUS: + case MUL: + case DIV: + if (!ISFPT(t)) + return; + fn = p->n_op == PLUS ? "dradd" : p->n_op == MINUS ? "drsub" : + p->n_op == MUL ? "drmul" : "drdiv"; + if (t == FLOAT) { + /* compute in double, round the result back */ + p->n_left = mksconv(p->n_left, DOUBLE); + p->n_right = mksconv(p->n_right, DOUBLE); + p->n_type = DOUBLE; + mkcall2(p, fn); + wrapsconv(p, FLOAT); + } else + mkcall2(p, fn); + break; + + case EQ: + case NE: + case LE: + case LT: + case GE: + case GT: + lt = p->n_left->n_type; + if (!ISFPT(lt)) + return; + l = p->n_left; + r = p->n_right; + if (lt == FLOAT) { + l = mksconv(l, DOUBLE); + r = mksconv(r, DOUBLE); + } + /* r1 = drcmp(a, b) = 1/0/-1; keep the relational op and + * compare that against 0 (native: "calr drcmp; test r1; + * jr cc" - our SZERO rule emits cp, same flags) */ + l = mkunode(FUNARG, l, 0, DOUBLE); + r = mkunode(FUNARG, r, 0, DOUBLE); + r = mkbinode(CM, l, r, INT); + l = mklnode(ICON, 0, 0, FTN|INT); + l->n_name = "drcmp"; + p->n_left = mkbinode(CALL, l, r, INT); + p->n_right = mklnode(ICON, 0, 0, INT); + break; + + case SCONV: + l = p->n_left; + lt = l->n_type; + if (ISFPT(t) && ISFPT(lt)) + return; /* dfpack/fdpack table rules */ + if (ISFPT(t)) { + /* integer -> fp; helpers take int/unsigned/long/ + * ulong, so widen narrow types first */ + switch (lt) { + case CHAR: case SHORT: case BOOL: + p->n_left = mksconv(l, INT); + lt = INT; + break; + case UCHAR: case USHORT: + p->n_left = mksconv(l, UNSIGNED); + lt = UNSIGNED; + break; + } + if (lt == INT) + fn = "diflt"; + else if (lt == UNSIGNED) + fn = "duflt"; + else if (lt == LONG) + fn = "dlflt"; + else + fn = "dvflt"; /* ulong, pointers */ + if (t == FLOAT) { + p->n_type = DOUBLE; + mkcall(p, fn); + wrapsconv(p, FLOAT); + } else + mkcall(p, fn); + } else if (ISFPT(lt)) { + /* fp -> integer; helpers take a double and return + * int in r1 / long in rr0, narrow types truncate + * from the int result afterwards */ + TWORD rt; + + if (lt == FLOAT) + p->n_left = mksconv(p->n_left, DOUBLE); + switch (t) { + case CHAR: case SHORT: case BOOL: case INT: + rt = INT; fn = "ifix"; break; + case UCHAR: case USHORT: case UNSIGNED: + rt = UNSIGNED; fn = "ufix"; break; + case LONG: + rt = LONG; fn = "lfix"; break; + default: + rt = ULONG; fn = "vfix"; break; + } + if (rt != t) { + p->n_type = rt; + mkcall(p, fn); + wrapsconv(p, t); + } else + mkcall(p, fn); + } + break; + } +} + +/* + * Struct-return support: every STCALL/USTCALL gets its OWN buffer in + * the caller's frame for the returned value (its address is pushed as + * the hidden argument by the ZR escape). The offset is attached to the + * call node as ATTR_P2_STBUF. Buffers must be distinct per call, not + * shared: pass 2 pre-evaluates call-containing arguments into pointer + * temps before pushing any argument, so f(g(), h()) has both results + * live at once. Reserved below the pass-1 autos; bumping p2autooff + * here (myreader runs before code generation) keeps later spill temps + * below them. + */ +static void +findstcall(NODE *p, void *arg) +{ + int *offp = arg; + struct attr *ap; + int sz; + + if (p->n_op != STCALL && p->n_op != USTCALL) + return; + if ((ap = attr_find(p->n_ap, ATTR_P2STRUCT)) == NULL) + comperr("findstcall: struct call without size attribute"); + sz = (ap->iarg(0) + 1) & ~1; + *offp += sz; + ap = attr_new(ATTR_P2_STBUF, 1); + ap->iarg(0) = *offp; + p->n_ap = attr_add(p->n_ap, ap); +} + +/* + * Rewrite the /SW... comment interpasses that mygenswitch() left around a + * sparse switch into the cpir dispatch (see mygenswitch() in code.c). + * + * Runs from myreader() - before optimize() builds the CFG and before + * register allocation - which is essential: + * + * - the switch value TEMP still exists here and gains a real use (the + * SWDISP child), so it is not dead-eliminated; + * - the case + default labels are registered in epp->ip_labels. The + * dispatch ends in GOTO(SWDISP(value)) - a computed goto - so cfg_build + * adds an edge from the dispatch to every label in ip_labels; that both + * makes the case bodies reachable (else DCE removes them) and is what + * inuse()/deljumps use to keep those labels alive; + * - the SWDISP node gets its NEEDS scratch pair + count from the allocator. + * + * The value/target tables are emitted as one opaque IP_ASM blob (the table + * label is defined inside the text) so no collectable IP_DEFLAB is created + * for a label that is only ever referenced textually. + */ +static void +swcpir(struct interpass *ipole) +{ + struct interpass *ip, *inext, *prev, *m, *end, *dip, *bip, *r, *rn; + struct swdesc *d; + NODE *val, *sw, *go; + int num, n, deflab, ltab, i, k, on, last; + unsigned type; + int *vals, *labs, *ol, *nl, *tg; + char *blob, *q; + + for (ip = DLIST_NEXT(ipole, qelem); ip != ipole; ip = inext) { + inext = DLIST_NEXT(ip, qelem); + if (ip->type != IP_ASM || strncmp(ip->ip_asm, "\t/SWH ", 6)) + continue; + + sscanf(ip->ip_asm, "%*s %d %u %d %d %d", + &num, &type, &n, &deflab, <ab); + vals = tmpalloc(n * sizeof(int)); + labs = tmpalloc(n * sizeof(int)); + prev = DLIST_PREV(ip, qelem); + + /* collect the /SWC case lines up to /SWE */ + i = 0; + for (m = DLIST_NEXT(ip, qelem); m != ipole; + m = DLIST_NEXT(m, qelem)) { + if (m->type == IP_ASM && + strncmp(m->ip_asm, "\t/SWC ", 6) == 0) { + if (i < n) + sscanf(m->ip_asm, "%*s %d %d", + &vals[i], &labs[i]); + i++; + continue; + } + break; + } + end = m; /* the /SWE interpass */ + inext = DLIST_NEXT(end, qelem); + + /* drop the markers (SWH .. SWE inclusive) */ + for (r = ip; ; r = rn) { + rn = DLIST_NEXT(r, qelem); + last = (r == end); + DLIST_REMOVE(r, qelem); + if (last) + break; + } + + /* GOTO(SWDISP(TEMP value)); SWDISP result is the target pair */ + d = tmpalloc(sizeof(struct swdesc)); + d->n = n; + d->deflab = deflab; + d->ltab = ltab; + val = mklnode(TEMP, 0, num, (TWORD)type); + sw = mkunode(SWDISP, val, 0, INCREF(VOID)); + sw->n_name = (char *)d; + go = mkunode(GOTO, sw, 0, INT); + /* This dispatch's own target list (case labels + default), + * null-terminated, on the GOTO node: cfg_build edges only to + * these, keeping the computed-goto CFG precise per switch. */ + tg = tmpalloc((n + 2) * sizeof(int)); + for (i = 0; i < n; i++) + tg[i] = labs[i]; + tg[n] = deflab; + tg[n + 1] = 0; + go->n_name = (char *)tg; + + dip = tmpalloc(sizeof(struct interpass)); + dip->type = IP_NODE; + dip->lineno = 0; + dip->ip_node = go; + DLIST_INSERT_AFTER(prev, dip, qelem); + + /* value/target tables as one opaque blob (label defined in it). + * Targets are 16-bit .word OFFSETS (all case bodies share this + * code segment), not .long addresses - half the size. */ + blob = tmpalloc(n * 32 + 32); + q = blob; + q += sprintf(q, LABFMT ":\n", ltab); + for (k = 0; k < n; k++) + q += sprintf(q, "\t.word\t%d\n", vals[k]); + for (k = 0; k < n; k++) + q += sprintf(q, "\t.word\t" LABFMT "\n", labs[k]); + + bip = tmpalloc(sizeof(struct interpass)); + bip->type = IP_ASM; + bip->lineno = 0; + bip->ip_asm = blob; + DLIST_INSERT_AFTER(dip, bip, qelem); + + /* Also add them to the function's ip_labels. cfg_build now + * takes this dispatch's edges from the GOTO's own list (above), + * so this is purely to mark the labels used-outside so deljumps' + * inuse() never deletes a case body reached only by the jp. */ + ol = p2env.epp->ip_labels; + on = 0; + if (ol) + while (ol[on]) + on++; + nl = tmpalloc((on + n + 2) * sizeof(int)); + for (k = 0; k < on; k++) + nl[k] = ol[k]; + for (i = 0; i < n; i++) + nl[on + i] = labs[i]; + nl[on + n] = deflab; + nl[on + n + 1] = 0; + p2env.epp->ip_labels = nl; + } +} + +/* + * A single-byte zero store to a frame slot: + * ASSIGN(UMUL(MINUS(REG r13, ICON off)), ICON 0) [char] + * (pass1 endinit->clearbf->insbf emits one per byte of the ANSI zero-fill + * tail of a partially-initialized auto aggregate). Return 1 and the base + * register + byte offset, else 0. + */ +static int +bytezero(NODE *p, int *base, CONSZ *off) +{ + NODE *l, *m; + + if (p->n_op != ASSIGN || + (p->n_type != CHAR && p->n_type != UCHAR)) + return 0; + if (p->n_right->n_op != ICON || getlval(p->n_right) != 0 || + p->n_right->n_name[0] != '\0') + return 0; + l = p->n_left; + if (l->n_op != UMUL) + return 0; + m = l->n_left; + if (m->n_op != MINUS || m->n_left->n_op != REG || + regno(m->n_left) != R13 || + m->n_right->n_op != ICON || m->n_right->n_name[0] != '\0') + return 0; + *base = regno(m->n_left); + *off = getlval(m->n_right); + return 1; +} + +/* + * Compact the run of consecutive single-byte zero stores that pass1 emits for + * an auto aggregate's zero-fill tail (one clrb each; login 123, tar 101). + * The run is buf[0..len-1] at the SAME base register with offsets hi, hi-1, + * ... (decreasing = increasing address; the first node is buf[0], the lowest + * address / highest offset). + * + * - len >= BCLR_THRESH: fold the whole run into one overlapping-ldirb block + * clear (BCLR): ~7 insns regardless of len (clear buf[0], propagate it + * forward). Reuse the first node's address lvalue as the BCLR operand. + * - 2 <= len < BCLR_THRESH: merge each word-aligned byte pair into one word + * clear (r13 is even, so an even offset is a word-aligned frame address); + * the even-offset store is retyped to a word and its odd neighbour dropped. + * + * Runs from myreader(), before canon/regalloc. BCLR reserves its scratch + * pairs + count via NEEDS; the word clears need no scratch. + * + * Threshold: word-collapse costs ~ceil(len/2) instructions, BCLR a flat ~7, + * so BCLR only wins once len exceeds ~14. + */ +#define BCLR_THRESH 15 + +static void +clrfill(struct interpass *ipole) +{ + struct interpass *ip, *nx, *past, *d, *dn; + NODE *p, *l; + int base, nbase, len; + CONSZ off, noff; + + ip = DLIST_NEXT(ipole, qelem); + while (ip != ipole) { + if (ip->type != IP_NODE || !bytezero(ip->ip_node, &base, &off)) { + ip = DLIST_NEXT(ip, qelem); + continue; + } + /* measure the run: consecutive same-base bytes, off = off-1... */ + len = 1; + for (nx = DLIST_NEXT(ip, qelem); + nx != ipole && nx->type == IP_NODE && + bytezero(nx->ip_node, &nbase, &noff) && + nbase == base && noff == off - 1; + nx = DLIST_NEXT(nx, qelem)) { + len++; + off = noff; + } + past = nx; /* first interpass past the run */ + + if (len >= BCLR_THRESH) { + /* fold into one overlapping-ldirb clear. ip is buf[0]; + * reuse its address lvalue (UMUL) as the BCLR operand. */ + p = ip->ip_node; + l = p->n_left; + nfree(p->n_right); /* the ICON 0 */ + nfree(p); /* the ASSIGN shell */ + /* INCREF type -> the node result slot is a B pair, used + * as the dst scratch by ZA. The byte count rides in + * n_rval (n_lval would alias n_left, the child). */ + p = mkunode(BCLR, l, len, INCREF(CHAR)); + ip->ip_node = p; + for (d = DLIST_NEXT(ip, qelem); d != past; d = dn) { + dn = DLIST_NEXT(d, qelem); + tfree(d->ip_node); + DLIST_REMOVE(d, qelem); + } + } else { + /* word-collapse aligned byte pairs within the run */ + for (d = ip; d != past; ) { + bytezero(d->ip_node, &nbase, &noff); + dn = DLIST_NEXT(d, qelem); + if ((noff & 1) == 0 && dn != past) { + p = d->ip_node; /* even off: word clear */ + p->n_type = INT; + p->n_left->n_type = INT; + p->n_left->n_left->n_type = INCREF(INT); + p->n_right->n_type = INT; + tfree(dn->ip_node); + DLIST_REMOVE(dn, qelem); + d = DLIST_NEXT(d, qelem); + } else + d = dn; /* odd/last: lone clrb */ + } + } + ip = past; + } +} + +/* + * PROTOTYPE: common-argument CSE across consecutive calls. + * + * Two adjacent statements that call different functions with the SAME + * fixed-address argument + * + * time(&clock); + * tp = localtime(&clock); + * + * each rematerialise "&clock" (lda + segment-mask "and") before their + * push. There is no cross-statement CSE in the pipeline, so the address + * is computed once per call. When the argument is the address of a frame + * object, a global, an address-taken auto, a symbolic address constant + * (a global array / function address), or a wide numeric constant, its + * value is a compile-time fixed quantity that no intervening call can + * perturb - so + * evaluating it once into a temp and reusing that temp is unconditionally + * safe, regardless of what the calls themselves do. Register allocation + * then keeps the shared address in one (callee-saved, since it is live + * across the first call) register: + * + * lda rr8,L0-4(r13) ; and r8,$32512 ; &clock, ONCE + * pushl (rr14),rr8 ; calr time_ ; inc r15,$4 + * pushl (rr14),rr8 ; calr localtime_ ; inc r15,$4 + * + * This removes the redundant recompute (the "why not cache it" half of + * the observation). The inter-call pop+repush (inc r15,$4 then pushl of + * the same value) is left in place: collapsing it would rely on the + * freed stack slot surviving across the call, which Coherent's + * same-stack signal delivery does not guarantee. + * + * The shared address need not be the whole argument list: a call may take + * several arguments and only one of them be the shared fixed address + * + * fprintf(fp, "%s", &buf[0]); + * fputs(&buf[0], fp); + * + * so each call's arguments are scanned, and when one call passes several + * hoistable addresses the one reused by the longest following run of calls + * is chosen. Runs may overlap (A and B share X; B and C share Y): the scan + * advances one call at a time, and an address already rewritten to a temp + * is no longer a hoist candidate, so each distinct address is hoisted once. + * + * Runs from myreader(), before ADDROF/TEMP lowering, canon and optimize, + * so the fresh temp and the inserted assignment get the full standard + * treatment (this is the only point at which inserting interpass nodes is + * safe - doing it at myoptim, after the SSA allocator's CFG is built, + * crashes insnwalk(); see the rmwcopyfuse note above). Gated on xtemps: + * without it deltemp spills every temp to the stack, turning the shared + * address back into per-use reloads with no benefit. + * + * The run length at which the transform fires is a per-kind threshold, + * because hoisting the address into a register live across a call forces a + * callee-saved register (a prologue/epilogue save/restore, ~4-6 bytes when + * it grows the ldm range, upgrades ld->ldm, or - worst case - spills and + * pushes the frame past 16 so dec/inc become sub/add). The hoist clears + * that cost easily once (uses-1) * recompute_size is large: + * - ADDROF (&auto/&global): recompute is "lda + and" = 8 bytes, so two + * uses already win comfortably -> ARGCSE_MIN_ADDR = 2. + * - an ICON (symbolic address constant "ldl rrN,$sym" = 4 bytes, or a + * wide numeric constant "ldl rrN,$imm32" = 6 bytes): the 4-byte case at + * two uses is a coin-flip (win when a callee-saved reg is free, small + * loss when it forces a save/spill) that cannot be called before + * register allocation; the 6-byte numeric case is at least as profitable. + * Measured over the 106-program corpus, ICON at 2 nets about -120 bytes + * beyond ICON at 3 (~14 programs shrink, ~4-8 bytes each) at the cost of 7 + * programs growing by +2..+14. We take the larger net reduction and accept + * those small regressions (project decision); set ARGCSE_MIN_ICON = 3 to + * trade -120 bytes of wins for guaranteed no-regression instead. + */ +#define ARGCSE_MIN_ADDR 2 +#define ARGCSE_MIN_ICON 2 + +/* + * The call node carried by an expression-statement: the bare call of a + * void/discarded call, or the right-hand side of "lhs = f(...)". Only + * plain CALL (binary, has an argument list); UCALL has no args, and the + * struct-return / Fortran variants carry hidden operands best left alone. + */ +static NODE * +argcse_call(NODE *p) +{ + if (p->n_op == ASSIGN) + p = p->n_right; + return p->n_op == CALL ? p : NULL; +} + +/* Structural equality of two argument trees (same op, type, and leaf + * value); enough to prove two "&x" designators name the same object. */ +static int +argcse_same(NODE *a, NODE *b) +{ + if (a->n_op != b->n_op || a->n_type != b->n_type) + return 0; + switch (optype(a->n_op)) { + case BITYPE: + if (!argcse_same(a->n_right, b->n_right)) + return 0; + /* FALLTHROUGH */ + case UTYPE: + return argcse_same(a->n_left, b->n_left); + default: + if (a->n_rval != b->n_rval || getlval(a) != getlval(b)) + return 0; + return strcmp(a->n_name ? a->n_name : "", + b->n_name ? b->n_name : "") == 0; + } +} + +/* + * An argument-list node (a FUNARG) whose value is a call-invariant quantity + * that is expensive enough to rematerialise to be worth caching in a + * register across the calls that share it. Every accepted form is fixed at + * compile/link time, so hoisting it past any call is unconditionally safe: + * - ADDROF(NAME|TEMP|OREG(FPREG)): the address of a global/static, an + * address-taken auto (temp before lowering, frame OREG after) - "lda + + * segment-mask", 8 bytes; + * - a nameful ICON: a symbolic address constant (a global array or + * function address, which pass 1 hands us as ICON not ADDROF) - a long + * "ldl rrN,$sym", 4 bytes; + * - a nameless ICON of long/ulong/pointer type: a wide numeric constant, + * which is pushed through a register ("ldl rrN,$imm32; pushl"), 6 bytes. + * Returns that node, else NULL. Deliberately rejected: word/byte numeric + * constants (pushed as a direct immediate, nothing to cache), struct-by-value + * STARG, and any value load - a register/temp value is already its own cached + * form, and a memory load is not call-invariant (the callee may write it). + */ +static NODE * +argcse_addr(NODE *arg) +{ + NODE *a, *l; + TWORD t; + + if (arg->n_op != FUNARG) + return NULL; + a = arg->n_left; + if (a->n_op == ICON) { + if (a->n_name != NULL && a->n_name[0] != '\0') + return a; /* &global_array / func address */ + t = a->n_type; /* wide numeric constant only */ + if (t == LONG || t == ULONG || ISPTR(t)) + return a; + return NULL; /* word/byte const: push $N */ + } + if (a->n_op != ADDROF) + return NULL; + l = a->n_left; + if (l->n_op == NAME) /* &global / &static */ + return a; + if (l->n_op == TEMP) /* &auto (pre-lowering) */ + return a; + if (l->n_op == OREG && l->n_rval == FPREG) /* &auto (lowered) */ + return a; + return NULL; +} + +/* + * The argument list of a call is the left-leaning CM chain hanging off + * n_right: each CM's n_right is one argument and the terminal (non-CM) + * node is the first argument, all FUNARG-wrapped (see lastcall()). The + * three helpers below walk that list without materialising it. + */ + +/* The k-th hoistable (fixed-address) argument of call, or NULL. */ +static NODE * +argcse_nth(NODE *call, int k) +{ + NODE **pp, *a; + int i = 0; + + if (call->n_right == NIL) + return NULL; + for (pp = &call->n_right; (*pp)->n_op == CM; pp = &(*pp)->n_left) + if ((a = argcse_addr((*pp)->n_right)) != NULL && i++ == k) + return a; + if ((a = argcse_addr(*pp)) != NULL && i == k) + return a; + return NULL; +} + +/* True if some argument of call is structurally the fixed address x. */ +static int +argcse_has(NODE *call, NODE *x) +{ + NODE *p; + + if (call->n_right == NIL) + return 0; + for (p = call->n_right; p->n_op == CM; p = p->n_left) + if (p->n_right->n_op == FUNARG && + argcse_same(p->n_right->n_left, x)) + return 1; + return p->n_op == FUNARG && argcse_same(p->n_left, x); +} + +/* + * Replace every argument of call that equals x with a fresh TEMP(num). + * When keep is set, the first replaced subtree is detached and returned + * (to become the single hoisted "tnew = x" evaluation) and any further + * matches are freed; otherwise every match is freed and NULL returned. + */ +static NODE * +argcse_subst(NODE *call, NODE *x, int num, TWORD at, int keep) +{ + NODE **pp, *slot, *child, *first = NULL; + + for (pp = &call->n_right; ; pp = &(*pp)->n_left) { + int cm = (*pp)->n_op == CM; + slot = cm ? (*pp)->n_right : *pp; + if (slot->n_op == FUNARG && argcse_same(slot->n_left, x)) { + child = slot->n_left; + slot->n_left = mklnode(TEMP, 0, num, at); + if (keep && first == NULL) + first = child; + else + tfree(child); + } + if (!cm) + break; + } + return first; +} + +static void +argcse(struct interpass *ipole) +{ + struct interpass *ip, *inext, *run, *newip; + NODE *c1, *ck, *cand, *best, *arg1; + TWORD at; + int num, i, k, n, bestn, need; + + if (!xtemps) + return; + for (ip = DLIST_NEXT(ipole, qelem); ip != ipole; ip = inext) { + inext = DLIST_NEXT(ip, qelem); + if (ip->type != IP_NODE) + continue; + c1 = argcse_call(ip->ip_node); + if (c1 == NULL) + continue; + + /* + * Among c1's hoistable arguments pick the one shared by the + * longest run of immediately following calls (a call may pass + * several fixed addresses, each shared by a different reach; + * the longest run yields the most reuse for one temp). Each + * candidate must clear its own kind's threshold (ICON address + * constants need a longer run than ADDROF, being cheaper to + * recompute) before it is eligible. + */ + best = NULL; + bestn = 0; + for (k = 0; (cand = argcse_nth(c1, k)) != NULL; k++) { + need = cand->n_op == ICON ? ARGCSE_MIN_ICON + : ARGCSE_MIN_ADDR; + n = 1; + for (run = inext; run != ipole && + run->type == IP_NODE; run = DLIST_NEXT(run, qelem)) { + ck = argcse_call(run->ip_node); + if (ck == NULL || !argcse_has(ck, cand)) + break; + n++; + } + if (n >= need && n > bestn) { + bestn = n; + best = cand; + } + } + if (best == NULL) + continue; + + /* hoist: "tnew = &x" once, immediately before c1, reusing + * c1's own occurrence of the address as the evaluation */ + at = best->n_type; + num = p2env.epp->ip_tmpnum++; + arg1 = argcse_subst(c1, best, num, at, 1); + newip = ipnode(mkbinode(ASSIGN, + mklnode(TEMP, 0, num, at), arg1, at)); + DLIST_INSERT_BEFORE(ip, newip, qelem); + + /* rewrite the shared argument to read tnew in the rest of + * the run (arg1 keeps best's structure, so the compares hold) */ + run = inext; + for (i = 1; i < bestn; i++) { + ck = argcse_call(run->ip_node); + argcse_subst(ck, best, num, at, 0); + run = DLIST_NEXT(run, qelem); + } + } +} + +void +myreader(struct interpass *ip) +{ + struct interpass *ip2; + int off = p2autooff; + + swcpir(ip); + clrfill(ip); + argcse(ip); + DLIST_FOREACH(ip2, ip, qelem) { + if (ip2->type != IP_NODE) + continue; + walkf(ip2->ip_node, fixfloatops, 0); + walkf(ip2->ip_node, findstcall, &off); + } + if (off > p2autooff) { + p2autooff = off; + if (p2autooff > p2maxautooff) + p2maxautooff = p2autooff; + } +} + +void +mycanon(NODE *p) +{ +} + +void +mygenregs(struct interpass *ip) +{ +} diff --git a/arch/z8001/macdefs.h b/arch/z8001/macdefs.h new file mode 100644 index 000000000..e54cbc959 --- /dev/null +++ b/arch/z8001/macdefs.h @@ -0,0 +1,490 @@ +/* + * Copyright (c) 2026 Michal Pleban. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* + * Machine-dependent defines for both passes. + * Target: Zilog Z8001 (segmented) running Coherent on Commodore 900. + * + * Key facts: + * - 16-bit int, 32-bit long, 32-bit pointer (segmented: 7-bit segment + 16-bit offset) + * - Big-endian + * - Pure stack-based calling convention (no argument registers) + * - r13 = frame pointer, r14:r15 (rr14) = stack pointer + * - Segmented call pushes 4-byte return address (CS + PC) + */ + +/* + * Convert (multi-)character constant to integer. + * Big-endian: first character in high byte. + */ +#define makecc(val,i) lastcon = i ? (lastcon << 8) | val : val + +#define ARGINIT 32 /* bits above frame base to first argument (4-byte return address) */ +#define AUTOINIT 0 /* bits below frame base to first automatic (no initial reservation) */ + +/* + * Storage space requirements (in bits). + */ +#define SZCHAR 8 +#define SZBOOL 8 +#define SZINT 16 +#define SZFLOAT 32 +#define SZDOUBLE 64 +#define SZLDOUBLE 64 +#define SZLONG 32 +#define SZSHORT 16 +#define SZLONGLONG 64 +#define SZPOINT(t) 32 /* segmented pointer: 16-bit segment word + 16-bit offset word */ + +/* + * Alignment constraints (in bits). + * + * The Coherent ABI is word-aligned THROUGHOUT, longs and pointers + * included (PDP-11 heritage, K&R compiler). Proof: struct stat has + * seven 16-bit members before st_size (long), placing it at offset 14. + * Anything above 16 here also breaks our own calling convention: + * FUNARG pushes arguments contiguously while pass 1 would lay out the + * callee's incoming arg offsets with alignment padding (seen as the + * mixed(int, long, int) failure: caller put the long at +6, callee + * read it at +8). Register pairs need no memory alignment. + */ +#define ALCHAR 8 +#define ALBOOL 8 +#define ALINT 16 +#define ALFLOAT 16 +#define ALDOUBLE 16 +#define ALLDOUBLE 16 +#define ALLONG 16 +#define ALLONGLONG 16 +#define ALSHORT 16 +#define ALPOINT 16 +#define ALSTRUCT 16 /* struct: strictest member, minimum word */ +#define ALSTACK 16 /* stack pointer kept word-aligned */ + +/* + * Min/max values. + */ +#define MIN_CHAR -128 +#define MAX_CHAR 127 +#define MAX_UCHAR 255 +#define MIN_SHORT -32768 +#define MAX_SHORT 32767 +#define MAX_USHORT 65535 +#define MIN_INT (-0x7fff-1) +#define MAX_INT 0x7fff +#define MAX_UNSIGNED 0xffff +#define MIN_LONG (-0x7fffffffL-1) +#define MAX_LONG 0x7fffffffL +#define MAX_ULONG 0xffffffffUL +#define MIN_LONGLONG (-0x7fffffffffffffffLL-1) +#define MAX_LONGLONG 0x7fffffffffffffffLL +#define MAX_ULONGLONG 0xffffffffffffffffULL + +/* char is signed by default (ldb + extsb pattern in reference output) */ +#undef CHAR_UNSIGNED +#define BOOL_TYPE CHAR + +/* + * Use large-enough types for constants and offsets. + */ +typedef long long CONSZ; +typedef unsigned long long U_CONSZ; +typedef long long OFFSZ; + +#define CONFMT "%lld" /* format for printing constants (decimal) */ +#define LABFMT "L%d" /* format for printing internal labels */ + +#define STACK_DOWN /* stack grows toward lower addresses */ + +#undef FIELDOPS /* no hardware bit-field instructions; use shift+mask */ +#define TARGET_ENDIAN TARGET_BE /* big-endian */ + +#define FINDMOPS /* Z8001 ALU ops can target memory directly */ +#define MYALIGN /* backend provides defalign() */ + +/* Coherent's assembler has no .file directive; suppress it. */ +#define MYDOTFILE +#define printdotfile(x) + +/* Coherent's assembler has no .ascii; emit strings as .byte lists. */ +#define MYINSTRING + +/* Definitions mostly used in pass2 */ + +#define BYTEOFF(x) ((x) & 01) /* nonzero if offset x is byte-aligned */ +#define wdal(k) (BYTEOFF(k) == 0) + +#define STOARG(p) +#define STOFARG(p) +#define STOSTARG(p) + +/* + * szty(t): number of 16-bit register units occupied by type t. + * char/short/int -> 1 + * long/float/ptr -> 2 (register pair) + * double/longlong -> 4 (register quad) + * + * A float value in registers is the raw 32-bit IEEE bit pattern in a + * pair; all float ARITHMETIC is done in double via the runtime helpers + * (K&R style, matching the native compiler, which has no float helpers + * at all - see fixfloatops() in local2.c). + */ +/* STRTY/UNIONTY count as 2: a struct value is only ever "in registers" + * as the pointer holding its address (hidden-pointer return ABI), so + * PCLASS/RETREG must see it as a pair. */ +#define szty(t) ((t) == DOUBLE || (t) == LDOUBLE || \ + (t) == LONGLONG || (t) == ULONGLONG ? 4 : \ + (t) == LONG || (t) == ULONG || \ + (t) == FLOAT || ISPTR(t) || \ + (t) == STRTY || (t) == UNIONTY ? 2 : 1) + +/* + * Register definitions. + * + * Physical registers r0-r15 occupy PCC indices 0-15. + * Register pairs rr0, rr2, rr4, rr6, rr8, rr10 occupy indices 16-21. + * r13 (frame pointer) and r14/r15 (stack pointer) are reserved. + * + * Register classes: + * A (SAREG) - 16-bit word registers: r0-r12 + * B (SBREG) - 32-bit register pairs: rr0,rr2,rr4,rr6,rr8,rr10 + * C (SCREG) - 64-bit register quads: rq0,rq4,rq8 (doubles; native + * soft-FP convention keeps a double value in rq0) + * D (SDREG) - 8-bit byte registers rl0-rl7 (the low bytes of r0-r7) + * + * Class D exists because Z8001 byte instructions (ldb/cpb/...) can only + * name the halves of r0-r7; r8-r12 have no byte-addressable halves. + * Making char values their own class (the arch/i86 model) lets the + * allocator enforce that structurally, through ROVERLAP/aliasmap, + * instead of via per-rule NOLEFT/NORIGHT/NEVER word-register constraints + * - NEVER in particular is clobber semantics (addalledges), which gave + * r8-r12 interference edges to EVERY value live near a char access and + * starved cross-call values out of rr8/rr10. Only the LOW halves exist + * as registers here (no rh0-rh7): a char held in a word register lives + * in its low byte everywhere in this ABI (big-endian argument slots, + * memory loads, extsb), so high-byte registers would break that + * convention at every conversion or store. + */ +#define R0 0 /* scratch, low half of rr0, return word (int) */ +#define R1 1 /* scratch, high half of rr0, return value (int) */ +#define R2 2 /* scratch, low half of rr2 */ +#define R3 3 /* scratch, high half of rr2 */ +#define R4 4 /* scratch, low half of rr4 */ +#define R5 5 /* scratch, high half of rr4 */ +#define R6 6 /* callee-saved, low half of rr6 */ +#define R7 7 /* callee-saved, high half of rr6 */ +#define R8 8 /* callee-saved, low half of rr8 */ +#define R9 9 /* callee-saved, high half of rr8 */ +#define R10 10 /* callee-saved, low half of rr10 */ +#define R11 11 /* callee-saved, high half of rr10 */ +#define R12 12 /* callee-saved (no pair: rr12 includes r13=FP) */ +#define R13 13 /* frame pointer - reserved */ +#define R14 14 /* stack pointer high - reserved */ +#define R15 15 /* stack pointer low - reserved */ + +#define RR0 16 /* r0:r1 - scratch pair, return value (long/ptr) */ +#define RR2 17 /* r2:r3 - scratch pair */ +#define RR4 18 /* r4:r5 - scratch pair */ +#define RR6 19 /* r6:r7 - callee-saved pair */ +#define RR8 20 /* r8:r9 - callee-saved pair */ +#define RR10 21 /* r10:r11 - callee-saved pair */ + +#define RQ0 22 /* r0-r3 - scratch quad, return value (double) */ +#define RQ4 23 /* r4-r7 - scratch quad (r6/r7 saved when used) */ +#define RQ8 24 /* r8-r11 - callee-saved quad */ + +#define RL0 25 /* low byte of r0 - scratch */ +#define RL1 26 /* low byte of r1 - scratch, return value (char) */ +#define RL2 27 /* low byte of r2 - scratch */ +#define RL3 28 /* low byte of r3 - scratch */ +#define RL4 29 /* low byte of r4 - scratch */ +#define RL5 30 /* low byte of r5 - scratch */ +#define RL6 31 /* low byte of r6 - callee-saved (via word r6) */ +#define RL7 32 /* low byte of r7 - callee-saved (via word r7) */ + +#define MAXREGS 33 + +/* + * RSTATUS: register class membership and caller/callee-saved flags. + * r13, r14, r15 are reserved (0) and never allocated. + */ +#define RSTATUS \ + SAREG|TEMPREG, /* r0 */ \ + SAREG|TEMPREG, /* r1 */ \ + SAREG|TEMPREG, /* r2 */ \ + SAREG|TEMPREG, /* r3 */ \ + SAREG|TEMPREG, /* r4 */ \ + SAREG|TEMPREG, /* r5 */ \ + SAREG|PERMREG, /* r6 */ \ + SAREG|PERMREG, /* r7 */ \ + SAREG|PERMREG, /* r8 */ \ + SAREG|PERMREG, /* r9 */ \ + SAREG|PERMREG, /* r10 */ \ + SAREG|PERMREG, /* r11 */ \ + SAREG|PERMREG, /* r12 */ \ + 0, /* r13 - frame pointer */ \ + 0, /* r14 - SP high */ \ + 0, /* r15 - SP low */ \ + SBREG, /* rr0 - valid color (RETREG) but cleared from clregs + in bjobcode so never allocated (RR0 can't be a base) */ \ + SBREG, /* rr2 */ \ + SBREG, /* rr4 */ \ + SBREG, /* rr6 - preserved via its words r6/r7 */ \ + SBREG, /* rr8 - preserved via its words r8/r9 */ \ + SBREG, /* rr10 - preserved via its words r10/r11 */ \ + SCREG, /* rq0 - caller-saved, double RETREG */ \ + SCREG, /* rq4 - r6/r7 half saved by prologue when used */ \ + SCREG, /* rq8 - preserved via its words r8-r11 */ \ + SDREG, /* rl0 - caller-saved via its word r0 */ \ + SDREG, /* rl1 - caller-saved, char RETREG */ \ + SDREG, /* rl2 - caller-saved via its word r2 */ \ + SDREG, /* rl3 - caller-saved via its word r3 */ \ + SDREG, /* rl4 - caller-saved via its word r4 */ \ + SDREG, /* rl5 - caller-saved via its word r5 */ \ + SDREG, /* rl6 - preserved via its word r6 */ \ + SDREG, /* rl7 - preserved via its word r7 */ +/* + * The byte registers carry no TEMPREG either: a char live across a call + * conflicts with the precolored r0-r5 edges added there, and aliasmap + * turns those into rl0-rl5 exclusions in class D automatically. + * + * Only the word registers r0-r5 carry TEMPREG. tempregs[] is consumed in + * exactly one place (regs.c call handling: addalledges of every listed + * register at each call site), and marking the caller-saved pairs/quads + * there as well is both redundant and costly: + * - redundant because AssignColors excludes overlapped colors through + * aliasmap(): a pair/quad temp live across a call already conflicts + * with precolored r0-r5, which maps to rr0/rr2/rr4 in class B and + * rq0/rq4 in class C; + * - costly because listing rq4 (r4-r7) makes every call exclude the + * callee-saved words r6/r7 (and pair rr6) from values that live + * across it, so cross-call words lost r6/r7 and cross-call pairs + * lost rr6 for no reason. Found compiling wc.c: every register + * local spilled to the frame. + */ + +/* + * ROVERLAP: which other registers each register aliases. + * A pair RRn aliases the two word registers Rn and R(n+1); a quad RQn + * aliases four words and two pairs; a byte register RLn aliases its + * containing word Rn and, transitively, that word's pair and quad. + */ +#define ROVERLAP \ + { RL0, RR0, RQ0, -1 }, /* r0 */ \ + { RL1, RR0, RQ0, -1 }, /* r1 */ \ + { RL2, RR2, RQ0, -1 }, /* r2 */ \ + { RL3, RR2, RQ0, -1 }, /* r3 */ \ + { RL4, RR4, RQ4, -1 }, /* r4 */ \ + { RL5, RR4, RQ4, -1 }, /* r5 */ \ + { RL6, RR6, RQ4, -1 }, /* r6 */ \ + { RL7, RR6, RQ4, -1 }, /* r7 */ \ + { RR8, RQ8, -1 }, /* r8 */ \ + { RR8, RQ8, -1 }, /* r9 */ \ + { RR10, RQ8, -1 }, /* r10 */ \ + { RR10, RQ8, -1 }, /* r11 */ \ + { -1 }, /* r12 - no pair */ \ + { -1 }, /* r13 - reserved */ \ + { -1 }, /* r14 - reserved */ \ + { -1 }, /* r15 - reserved */ \ + { R0, R1, RL0, RL1, RQ0, -1 }, /* rr0 */ \ + { R2, R3, RL2, RL3, RQ0, -1 }, /* rr2 */ \ + { R4, R5, RL4, RL5, RQ4, -1 }, /* rr4 */ \ + { R6, R7, RL6, RL7, RQ4, -1 }, /* rr6 */ \ + { R8, R9, RQ8, -1 }, /* rr8 */ \ + { R10, R11, RQ8, -1 }, /* rr10 */ \ + { R0, R1, R2, R3, RL0, RL1, RL2, RL3, RR0, RR2, -1 }, /* rq0 */ \ + { R4, R5, R6, R7, RL4, RL5, RL6, RL7, RR4, RR6, -1 }, /* rq4 */ \ + { R8, R9, R10, R11, RR8, RR10, -1 }, /* rq8 */ \ + { R0, RR0, RQ0, -1 }, /* rl0 */ \ + { R1, RR0, RQ0, -1 }, /* rl1 */ \ + { R2, RR2, RQ0, -1 }, /* rl2 */ \ + { R3, RR2, RQ0, -1 }, /* rl3 */ \ + { R4, RR4, RQ4, -1 }, /* rl4 */ \ + { R5, RR4, RQ4, -1 }, /* rl5 */ \ + { R6, RR6, RQ4, -1 }, /* rl6 */ \ + { R7, RR6, RQ4, -1 }, /* rl7 */ + +/* Return the register class for a node's type */ +#define PCLASS(p) ((p)->n_type == CHAR || (p)->n_type == UCHAR ? SDREG : \ + szty((p)->n_type) == 4 ? SCREG : \ + szty((p)->n_type) == 2 ? SBREG : SAREG) + +#define NUMCLASS 4 /* classes: A (word), B (pair), C (quad), D (byte) */ + +int COLORMAP(int c, int *r); +#define GCLASS(x) ((x) < 16 ? CLASSA : (x) < 22 ? CLASSB : \ + (x) < 25 ? CLASSC : CLASSD) + +/* AssignColors fallback choice (regs.c colfind): caller-saved registers + * lowest-first as before, callee-saved TOP-DOWN so the prologue's + * contiguous ldm save range stays minimal (pickcolor in local2.c). */ +int pickcolor(int class, int mask); +#define PICKCOLOR(c, m) pickcolor(c, m) + +/* A permreg shadow that loses its own register must SPILL (the ldm + * range already covers a saved register at zero extra instructions), + * never sit in another callee-save register behind entry/exit moves + * (regs.c AssignColors). */ +#define SPILLSHADOW +/* 6-bit register fields: MAXREGS is 33, three regs packed in n_reg */ +#define DECRA(x,y) (((x) >> ((y)*6)) & 63) /* decode register from n_reg */ +#define ENCRD(x) (x) /* encode dest register */ +#define ENCRA1(x) ((x) << 6) +#define ENCRA2(x) ((x) << 12) +#define ENCRA(x,y) ((x) << (6+(y)*6)) /* encode source register */ + +/* + * Return register for each type (matches the native Coherent compiler: + * factor.s reads int results from r1, long results from rr0, and double + * results from rq0; the FP runtime's ifix returns int in r1 too): + * char -> rl1 (the low byte of r1; the class-D view of the + * native word convention - a caller wanting an int + * re-extends, one reading a char takes rl1 as is) + * int/short -> r1 + * long/ptr/float -> rr0 (r0:r1) + * double -> rq0 (r0-r3) + */ +#define RETREG(x) ((x) == CHAR || (x) == UCHAR ? RL1 : \ + szty(x) == 4 ? RQ0 : szty(x) == 2 ? RR0 : R1) + +#define FPREG R13 /* frame pointer */ +#define STKREG R15 /* stack pointer (low word of rr14) */ + +/* + * Special shapes. The Z8001 based address mode rrN(disp) (BA) is only + * legal in the ld/ldl/ldb/lda family; every other instruction taking a + * memory operand accepts only IR "(rrN)", DA "name" and X "L+off(r13)" + * modes (Coherent as machine.c: S_RSRC/S_DEC/S_CLR/S_CP/S_PUSH all lack + * BAOK). So plain SOREG must not appear in non-load rules; these shapes + * take its place there. + */ +/* + * Target pass2 attribute: each STCALL/USTCALL carries the offset of its + * OWN struct-return buffer in the caller's frame (assigned by myreader() + * in local2.c, consumed by the ZR escape). One shared buffer is not + * enough: pass 2 pre-evaluates call-containing arguments into pointer + * temps before pushing any argument, so with two struct-returning calls + * in one argument list both results are live at the same time. + */ +#define ATTR_P2_TARGET ATTR_P2_STBUF + +#define SNBA (MAXSPECIAL+1) /* OREG encodable in a non-load insn: + frame (X mode) or pair base with zero + displacement (IR mode) */ +#define SFRAME (MAXSPECIAL+2) /* frame (r13-based) OREG only: for rules + that access both pair halves at off and + off+2 (ZL/ZQ), where a pair-base OREG + would need BA for the second half */ +#define SR13 (MAXSPECIAL+3) /* REG node that is exactly r13: the frame + pointer as a bare word register, produced + only by the reader lowering &frameobj to + PLUS/MINUS(REG r13, ICON off) */ +#define SLDK (MAXSPECIAL+4) /* nameless ICON in 0..15: loadable with the + 2-byte ldk instead of the 4-byte ld $imm */ +#define SP16 (MAXSPECIAL+5) /* nameless ICON in 1..16: the inc/dec + immediate range */ +#define SPCON (MAXSPECIAL+6) /* nameless ICON representable as a word + immediate (-32768..65535): pointer offset + arithmetic on the pair's low word */ +#define SPOW2 (MAXSPECIAL+7) /* nameless ICON that is a single-bit mask + in its type's width (word or char): the + bit-test rules print it as a bit number */ +#define SNPOW2 (MAXSPECIAL+8) /* nameless ICON with every bit of its + type's width set EXCEPT one (~mask of a + single bit, either sign representation): + the res rule prints the clear bit's + number via ZY */ +#define SLCON (MAXSPECIAL+9) /* nameless (numeric) ICON loaded into a pair + register: the Z0 escape splits it into + per-word clr/ldk/ld when that is no larger + than the 6-byte "ldl #imm32", else emits the + plain ldl. Symbolic constants (n_name set) + fail this shape and take the generic ldl. */ + +/* + * Compare-vs-zero elision gate (reader.c geninsn): the word logical + * instructions AND/OR/XOR leave P/V unaffected (and the byte forms set + * it to parity), so after an elided compare only the Z and S flags are + * trustworthy - an ordered branch (jr lt/ge = S xor V) would read a + * stale V. Allow the elision for EQ/NE always (they read Z alone) and + * for PLUS/MINUS children unconditionally (add/sub/inc/dec set V + * arithmetically, which is exactly the signed compare against zero). + * The bit-test rules (BIT sets ONLY Z) also rely on this gate: FORCC + * can reach an AND node only from an EQ/NE compare. + */ +#define CCOKFORCOMP(o, ch) \ + ((o) == EQ || (o) == NE || (ch) == PLUS || (ch) == MINUS) + +/* + * Fuse a decrement-and-test loop branch into a single djnz (see cbfuse in + * local2.c). Consulted by emit() before an RESCC compare/branch is lowered. + */ +#define TARGET_CBRANCH_FUSE(p) cbfuse(p) + +/* + * sizeof and pointer-difference results are plain 16-bit integers, NOT + * pointer-sized: the Coherent libc is K&R and unprototyped, and its + * functions declare sizes and counts as int/unsigned words - + * qsort(base, nel, width, compar) "unsigned nel, width", malloc(size) + * "unsigned int size", fread "int size, nitems". The native compiler + * pushes them as words (ls.s qsort call: "push (rr14),$38"). With the + * pcc default (INTPTR = LONG, since pointers are 32-bit), every + * malloc(n*sizeof(x)) would push 4 bytes where libc reads 2. + */ +#define SIZET UNSIGNED +#define PTRDIFFT INT + +/* + * Never fold x+0/x-0 to a bare frame-pointer REG: a segmented address + * needs the lda+mask pair materialization (ZF), and r13 alone is a + * word with no segment. Hit by &arr[N] where the array end lands at + * frame offset exactly 0 (tee.c "fpp >= &fp[NUFILE]" emitted the + * illegal "cpl rr8,r13"). + */ +#define OPTIM_KEEPZERO(p) (p->n_left->n_op == REG && \ + p->n_left->n_rval == FPREG) + +/* + * Keep value-context relationals (f = (a < b), return x == y) as bare + * OPLOG nodes instead of pass1's branch diamond: pass2 materializes + * the truth value with "cp ; clr A1 ; tcc cc,A1". TCC sets only bit 0 + * of its register destination when cc holds (all other bits and all + * flags untouched), and CLR sets no flags either - so the order is + * compare first, clr second, tcc last, which also makes it safe for + * the result register to share with a compare operand. Integer and + * pointer operands only: float and longlong compares have no + * value-context OPLOG rules and keep the diamond. + */ +#define KEEPLOGOPVALUE_T(t) (ISPTR(t) || ((t) >= CHAR && (t) <= ULONG)) +#define KEEPLOGOPVALUE(p) (KEEPLOGOPVALUE_T(p->n_left->n_type) && \ + KEEPLOGOPVALUE_T(p->n_right->n_type)) + +/* Soft-float: big-endian IEEE binary32 and binary64 */ +#define USE_IEEEFP_32 +#define FLT_PREFIX IEEEFP_32 +#define USE_IEEEFP_64 +#define DBL_PREFIX IEEEFP_64 +#define LDBL_PREFIX IEEEFP_64 +#define DEFAULT_FPI_DEFS { &fpi_binary32, &fpi_binary64, &fpi_binary64 } diff --git a/arch/z8001/order.c b/arch/z8001/order.c new file mode 100644 index 000000000..7559f073f --- /dev/null +++ b/arch/z8001/order.c @@ -0,0 +1,201 @@ +/* + * Copyright (c) 2026 Michal Pleban. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* + * pass2 instruction ordering for the Zilog Z8001 / Coherent target. + * + * On the Z8001, all pointer dereferences use a 32-bit segmented pair + * register (SBREG class) as the base address. Frame-relative access + * (autos, params) uses r13 (a plain word register) with the implicit DS. + * + * An OREG can be formed from: + * - A pair register (rr0..rr10): pointer-based indirect access + * - r13 (FPREG): frame-relative access (already an OREG from clocal) + * - PLUS/MINUS of a pair register and a small integer constant + */ + +#include "pass2.h" +#include + +int canaddr(NODE *); + +/* + * notoff: can we form an OREG with this offset? + * Z8001 indirect addressing supports a 16-bit signed displacement, + * so all offsets that fit in 16 bits are fine. + */ +int +notoff(TWORD t, int r, CONSZ off, char *cp) +{ + /* All offsets fit in 16 bits for our purposes */ + return off > 32767 || off < -32768; +} + +/* + * offstar: prepare the argument of a UMUL (pointer dereference) so that + * it can be converted into an OREG by myormake(). + * + * For Z8001, valid OREG bases are: + * - A pair register (SBREG): evaluate to INBREG + * - r13 (word reg): already handled as OREG by clocal + * - PLUS(pair_reg, ICON): evaluate left to INBREG + */ +void +offstar(NODE *p, int shape) +{ + if (x2debug) + printf("offstar(%p)\n", p); + + if (isreg(p)) + return; + + if (p->n_op == PLUS || p->n_op == MINUS) { + if (p->n_right->n_op == ICON) { + /* base + constant offset: evaluate base into a pair reg */ + if (!isreg(p->n_left)) + (void)geninsn(p->n_left, INBREG); + return; + } + } + + /* General case: evaluate p into a pair register */ + (void)geninsn(p, INBREG); +} + +/* + * myormake: convert an already-evaluated UMUL child into an OREG. + * + * Called after offstar() has arranged the child. We look for the + * patterns that offstar() prepared. + */ +void +myormake(NODE *p) +{ + NODE *q = p->n_left; + + if (x2debug) { + printf("myormake(%p)\n", p); + fwalk(p, e2print, 0); + } + + if (q->n_op == OREG) + return; + + if ((q->n_op == PLUS || q->n_op == MINUS) && + q->n_right->n_op == ICON && + q->n_left->n_op == REG) { + /* PLUS/MINUS(reg, icon) → OREG with offset */ + CONSZ off = getlval(q->n_right); + if (q->n_op == MINUS) + off = -off; + p->n_op = OREG; + setlval(p, off); + p->n_rval = regno(q->n_left); + tfree(q); + return; + } + + if (q->n_op == REG) { + /* bare register → OREG with zero offset */ + p->n_op = OREG; + setlval(p, 0); + p->n_rval = regno(q); + tfree(q); + return; + } +} + +/* + * shumul: can a UMUL node be represented as a memory shape? + * + * Returns SROREG (will call offstar/myormake) or SRNOPE. + */ +int +shumul(NODE *p, int shape) +{ + if (x2debug) + printf("shumul(%p)\n", p); + + if (p->n_op == NAME && (shape & STARNM)) + return SRDIR; + + if (shape & SOREG) + return SROREG; + + return SRNOPE; +} + +/* + * setbin, setasg, setuni, setorder: instruction ordering hooks. + * For Z8001 we rely entirely on the table and don't need special handling. + */ +int +setbin(NODE *p) +{ + return 0; +} + +int +setasg(NODE *p, int cookie) +{ + return 0; +} + +int +setuni(NODE *p, int cookie) +{ + return 0; +} + +int +setorder(NODE *p) +{ + return 0; +} + +/* + * livecall: return registers that are live at a call instruction. + * + * On Z8001 with pure stack calling convention, no argument registers + * are live at the call site; the callee reads everything from the stack. + */ +int * +livecall(NODE *p) +{ + static int r[] = { -1 }; + return r; +} + +/* + * acceptable: is this instruction acceptable for our target? + * + * Z8001 uses a simple flat instruction set with no variants that need + * filtering, so all table entries are acceptable. + */ +int +acceptable(struct optab *op) +{ + return 1; +} diff --git a/arch/z8001/table.c b/arch/z8001/table.c new file mode 100644 index 000000000..1429dfb0b --- /dev/null +++ b/arch/z8001/table.c @@ -0,0 +1,2160 @@ +/* + * Copyright (c) 2026 Michal Pleban. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* + * Instruction selection table for the Zilog Z8001 (segmented) targeting + * Coherent OS on the Commodore 900. + * + * Register model: + * SAREG (class A) - 16-bit word registers r0..r12 + * SBREG (class B) - 32-bit register pairs rr0,rr2,rr4,rr6,rr8,rr10 + * SCREG (class C) - 64-bit register quads rq0,rq4,rq8 (doubles) + * SDREG (class D) - 8-bit byte registers rl0..rl7 (char values) + * + * Key calling convention facts: + * - Pure stack, right-to-left push, caller cleans + * - int result in r1; long/ptr/float result in rr0; double in rq0 + * - Stack pointer is the 32-bit pair rr14 (r14:r15) + * - Frame pointer is r13 + * + * Assembler syntax (Coherent as): + * Dest-first: ld r1, r0 (r1 <- r0) + * Indirect: (rr0) (MUST be a 32-bit pair register) + * Disp+ind: 4(rr0) + * Immediate: $N + * Push word: push (rr14), r1 + * Push long: pushl (rr14), rr0 + * Call: calr sym_ (pc-relative) / call (rr0) (indirect) + * Return: ret un + */ + +#include "pass2.h" + +/* Type groupings */ +#define TWORD (TINT|TUNSIGNED) +#define TLNG (TLONG|TULONG) +#define TFLT TFLOAT +#define TDBL (TDOUBLE|TLDOUBLE) + +/* Convenience NEEDS shorthands (new-style) */ +#define NAREG NEEDS(NREG(A, 1)) +#define NBREG NEEDS(NREG(B, 1)) +#define NDREG NEEDS(NREG(D, 1)) +#define XSL(c) NEEDS(NREG(c, 1), NSL(c)) +#define XSR(c) NEEDS(NREG(c, 1), NSR(c)) + +/* + * Char values live in register class D (the byte registers rl0-rl7, + * i.e. the low halves of r0-r7), so the "byte instructions can only + * name r0-r7's halves" hardware restriction is enforced by register + * class membership - byte rules need no NOLEFT/NORIGHT/NEVER + * constraints (whose addalledges clobber semantics used to poison + * r8-r12 for everything live near a char access). + * + * Conversions between class D and the word/pair classes exploit the + * fact that WORD instructions (ld/extsb/and/neg/push) work on any + * register and a char value sits in its containing word's low byte: + * they move the containing word (ZG/ZH print it, ZK/ZI move it with + * NSL sharing usually reducing the move to nothing) and extend with + * word ops. No fixed registers, no constraints. + */ + +struct optab table[] = { + +/* First entry must be an empty entry */ +{ -1, FOREFF, SANY, TANY, SANY, TANY, 0, 0, "", }, + +/* + * PCONVs - pointer conversions. + * On Z8001 pointers are 32-bit pairs (SBREG), same as long. + * PCONV between two pair types is a no-op. + */ +{ PCONV, INBREG, + SBREG, TPOINT|TLONG|TULONG, + SANY, TPOINT|TLONG|TULONG, + 0, RLEFT, + "", }, + +/* ptr/long from memory into a pair register */ +{ PCONV, INBREG, + SOREG|SNAME, TPOINT|TLONG|TULONG, + SANY, TPOINT, + XSL(B), RESC1, + " ldl A1,AL\n", }, + +/* word reg to pointer pair: value into the low (offset) word, clear + * the high (segment) word. (ZM names the high word explicitly; a bare + * "clr A1" would print the pair name, which Coherent as encodes as the + * high word by symbol-value luck - and the old "ld A1,AL; clr U1" form + * had it backwards: value into the segment, offset cleared.) */ +{ PCONV, INBREG, + SAREG, TWORD, + SANY, TPOINT, + XSL(B), RESC1, + " ld U1,AL\n clr ZM\n", }, + +/* + * SCONVs - scalar type conversions. + */ + +/* (u)int <-> (u)int: already the right size in a word reg */ +{ SCONV, INAREG, + SAREG, TWORD|TSHORT|TUSHORT, + SANY, TWORD|TSHORT|TUSHORT, + 0, RLEFT, + "", }, + +/* pointer (pair) <-> pointer: no-op */ +{ SCONV, INBREG, + SBREG, TPOINT, + SANY, TPOINT, + 0, RLEFT, + "", }, + +/* long (pair) <-> long (pair): no-op */ +{ SCONV, INBREG, + SBREG, TLONG|TULONG, + SANY, TLONG|TULONG, + 0, RLEFT, + "", }, + +/* int -> (u)long: sign-extend word into pair (the SOURCE type picks + * sign- vs zero-extension; the result signedness is bit-identical) */ +{ SCONV, INBREG, + SAREG, TINT, + SANY, TLNG, + XSL(B), RESC1, + " ld U1,AL\n exts A1\n", }, + +/* unsigned int -> (u)long: zero-extend word into pair */ +{ SCONV, INBREG, + SAREG, TUNSIGNED, + SANY, TLNG, + XSL(B), RESC1, + " ld U1,AL\n clr ZM\n", }, + +/* int/short from memory -> (u)long */ +{ SCONV, INBREG, + SOREG|SNAME, TINT|TSHORT, + SANY, TLNG, + NBREG, RESC1, + " ld U1,AL\n exts A1\n", }, + +/* unsigned/ushort from memory -> (u)long */ +{ SCONV, INBREG, + SOREG|SNAME, TUNSIGNED|TUSHORT, + SANY, TLNG, + NBREG, RESC1, + " ld U1,AL\n clr ZM\n", }, + +/* long/ptr pair -> int: take low word of pair */ +{ SCONV, INAREG, + SBREG|SOREG|SNAME, TLONG|TULONG|TPOINT, + SANY, TWORD, + XSL(A), RESC1, + " ld A1,UL\n", }, + +/* char <-> char: same byte register either way */ +{ SCONV, INDREG, + SDREG, TCHAR|TUCHAR, + SANY, TCHAR|TUCHAR, + 0, RLEFT, + "", }, + +/* char (byte reg) -> int: copy the containing word (ZK, elided when + * NSL sharing makes A1 that word), then sign-extend its low byte in + * place - extsb is a word op, legal on any register. Memory chars are + * first materialized into a byte register by the OPLTYPE rule; with the + * shared A1 the pair is "ldb rlN,mem; extsb rN", same as a fused rule. */ +{ SCONV, INAREG, + SDREG, TCHAR, + SANY, TWORD, + NEEDS(NREG(A, 1), NSL(A)), RESC1, + "ZK extsb A1\n", }, + +/* unsigned char (byte reg) -> (u)int: zero-extend in place. ZU picks + * the native idiom "subb rhN,rhN" (2 bytes) when A1's word has an + * addressable high byte (r0-r7), else the word "and A1,$0xff". */ +{ SCONV, INAREG, + SDREG, TUCHAR, + SANY, TWORD, + NEEDS(NREG(A, 1), NSL(A)), RESC1, + "ZKZU", }, + +/* char -> (u)long: word-copy the containing word into the pair's low + * word, extend byte->word->pair. No NSL: "ld U1,ZG" writes U1 before + * the (word-op) extensions, so A1 must stay disjoint from the source. */ +{ SCONV, INBREG, + SDREG, TCHAR, + SANY, TLNG, + NBREG, RESC1, + " ld U1,ZG\n extsb U1\n exts A1\n", }, + +/* unsigned char -> (u)long */ +{ SCONV, INBREG, + SDREG, TUCHAR, + SANY, TLNG, + NBREG, RESC1, + " ld U1,ZG\n and U1,$0xff\n clr ZM\n", }, + +/* int -> char: word-copy into the result's containing word (ZI, elided + * when NSL sharing makes them coincide); the low byte IS the truncated + * char, the high byte is dead space. */ +{ SCONV, INDREG, + SAREG, TWORD, + SANY, TCHAR|TUCHAR, + NEEDS(NREG(D, 1), NSL(D)), RESC1, + "ZI", }, + +/* (u)long/ptr -> char: word-copy the LOW word (UL) into the result's + * containing word; works for any pair (rr8/rr10 included) and for + * memory (low word at offset+2), unlike a byte-register ldb. */ +{ SCONV, INDREG, + SBREG|SOREG|SNAME, TLNG|TPOINT, + SANY, TCHAR|TUCHAR, + NDREG, RESC1, + " ld ZH,UL\n", }, + +/* + * Floating-point conversions. Integer<->fp conversions are rewritten + * into runtime calls by fixfloatops() in local2.c; only the same-size + * no-ops and the register-based float<->double pack helpers remain as + * table rules. dfpack takes a float in rr0 and returns a double in + * rq0 (clobbering r5); fdpack takes rq0 and returns rr0 (its scratch, + * r2/r3, lies inside the consumed rq0). + */ + +/* double <-> double: no-op */ +{ SCONV, INCREG, + SCREG, TDBL, + SANY, TDBL, + 0, RLEFT, + "", }, + +/* float <-> float: no-op */ +{ SCONV, INBREG, + SBREG, TFLT, + SANY, TFLT, + 0, RLEFT, + "", }, + +/* float -> double: dfpack, rr0 -> rq0 */ +{ SCONV, INCREG, + SBREG, TFLT, + SANY, TDBL, + NEEDS(NREG(C, 1), NLEFT(RR0), NRES(RQ0), NEVER(R5)), RESC1, + " calr dfpack\n", }, + +/* double -> float: fdpack, rq0 -> rr0 */ +{ SCONV, INBREG, + SCREG, TDBL, + SANY, TFLT, + NEEDS(NREG(B, 1), NLEFT(RQ0), NRES(RR0)), RESC1, + " calr fdpack\n", }, + +/* + * Subroutine calls. + * + * Z8001 uses: + * calr sym_ (relative call by name) + * call (rr0) (indirect call through pair register) + * + * ZA expands to the function name (via zzzcode 'A'). + * ZB expands to the stack adjustment after the call (via zzzcode 'B'). + */ + +/* Named call, result is word (int/short) in SAREG */ +{ CALL, INAREG, + SCON, TANY, + SANY, TWORD, + XSL(A), RESC1, + " calr CL\nZB", }, + +{ UCALL, INAREG, + SCON, TANY, + SANY, TWORD, + XSL(A), RESC1, + " calr CL\n", }, + +/* Indirect call (function pointer in pair), word result */ +{ CALL, INAREG, + SBREG, TANY, + SANY, TWORD, + XSL(A), RESC1, + " call (AL)\nZB", }, + +{ UCALL, INAREG, + SBREG, TANY, + SANY, TWORD, + XSL(A), RESC1, + " call (AL)\n", }, + +/* Call with a char result: the value comes back in rl1 (RETREG(CHAR), + * the class-D face of the native "int result in r1" convention) */ +{ CALL, INDREG, + SCON, TANY, + SANY, TCHAR|TUCHAR, + XSL(D), RESC1, + " calr CL\nZB", }, + +{ UCALL, INDREG, + SCON, TANY, + SANY, TCHAR|TUCHAR, + XSL(D), RESC1, + " calr CL\n", }, + +{ CALL, INDREG, + SBREG, TANY, + SANY, TCHAR|TUCHAR, + XSL(D), RESC1, + " call (AL)\nZB", }, + +{ UCALL, INDREG, + SBREG, TANY, + SANY, TCHAR|TUCHAR, + XSL(D), RESC1, + " call (AL)\n", }, + +/* Named call, result is long/ptr/float in SBREG */ +{ CALL, INBREG, + SCON, TANY, + SANY, TLONG|TULONG|TPOINT|TFLT, + NEEDS(NREG(B, 1), NSL(B), NEVER(RR0)), RESC1, + " calr CL\nZB", }, + +{ UCALL, INBREG, + SCON, TANY, + SANY, TLONG|TULONG|TPOINT|TFLT, + NEEDS(NREG(B, 1), NSL(B), NEVER(RR0)), RESC1, + " calr CL\n", }, + +/* Indirect call (function pointer in pair), long/ptr/float result */ +{ CALL, INBREG, + SBREG, TANY, + SANY, TLONG|TULONG|TPOINT|TFLT, + NEEDS(NREG(B, 1), NSL(B), NEVER(RR0)), RESC1, + " call (AL)\nZB", }, + +{ UCALL, INBREG, + SBREG, TANY, + SANY, TLONG|TULONG|TPOINT|TFLT, + NEEDS(NREG(B, 1), NSL(B), NEVER(RR0)), RESC1, + " call (AL)\n", }, + +/* Call with a double result in a quad (rq0 per the native convention; + * doubles are never indirect bases, so coalescing with rq0 is fine) */ +{ CALL, INCREG, + SCON, TANY, + SANY, TDBL, + NEEDS(NREG(C, 1), NSL(C)), RESC1, + " calr CL\nZB", }, + +{ UCALL, INCREG, + SCON, TANY, + SANY, TDBL, + NEEDS(NREG(C, 1), NSL(C)), RESC1, + " calr CL\n", }, + +{ CALL, INCREG, + SBREG, TANY, + SANY, TDBL, + NEEDS(NREG(C, 1), NSL(C)), RESC1, + " call (AL)\nZB", }, + +{ UCALL, INCREG, + SBREG, TANY, + SANY, TDBL, + NEEDS(NREG(C, 1), NSL(C)), RESC1, + " call (AL)\n", }, + +/* + * Struct-return calls (hidden-pointer ABI). ZR pushes the address of + * the frame buffer reserved by myreader() as the hidden argument (its + * 4 bytes are included in the ZB cleanup count n_qual); the callee + * copies the value there and returns the buffer address in rr0, which + * becomes the call's result. USTCALL needs ZB too: even with no + * declared arguments the hidden pointer was pushed. + */ +{ STCALL, INBREG, + SCON, TANY, + SANY, TANY, + NEEDS(NREG(B, 1), NSL(B), NEVER(RR0)), RESC1, + "ZR calr CL\nZB", }, + +{ USTCALL, INBREG, + SCON, TANY, + SANY, TANY, + NEEDS(NREG(B, 1), NSL(B), NEVER(RR0)), RESC1, + "ZR calr CL\nZB", }, + +{ STCALL, INBREG, + SBREG, TANY, + SANY, TANY, + NEEDS(NREG(B, 1), NSL(B), NEVER(RR0)), RESC1, + "ZR call (AL)\nZB", }, + +{ USTCALL, INBREG, + SBREG, TANY, + SANY, TANY, + NEEDS(NREG(B, 1), NSL(B), NEVER(RR0)), RESC1, + "ZR call (AL)\nZB", }, + +/* Named call, for effect only (void return) */ +{ CALL, FOREFF, + SCON, TANY, + SANY, TANY, + 0, 0, + " calr CL\nZB", }, + +{ UCALL, FOREFF, + SCON, TANY, + SANY, TANY, + 0, 0, + " calr CL\n", }, + +/* Indirect call, for effect only */ +{ CALL, FOREFF, + SBREG, TANY, + SANY, TANY, + 0, 0, + " call (AL)\nZB", }, + +{ UCALL, FOREFF, + SBREG, TANY, + SANY, TANY, + 0, 0, + " call (AL)\n", }, + +/* Struct calls for effect only (result ignored): the hidden pointer + * must STILL be pushed (ZR) - the callee unconditionally writes the + * returned value through it - and ZB must clean it up. */ +{ STCALL, FOREFF, + SCON, TANY, + SANY, TANY, + 0, 0, + "ZR calr CL\nZB", }, + +{ USTCALL, FOREFF, + SCON, TANY, + SANY, TANY, + 0, 0, + "ZR calr CL\nZB", }, + +{ STCALL, FOREFF, + SBREG, TANY, + SANY, TANY, + 0, 0, + "ZR call (AL)\nZB", }, + +{ USTCALL, FOREFF, + SBREG, TANY, + SANY, TANY, + 0, 0, + "ZR call (AL)\nZB", }, + +/* + * PLUS - integer addition. + */ + +/* inc word by 1 */ +{ PLUS, FOREFF|INAREG|FORCC, + SAREG|SNAME, TWORD, + SONE, TANY, + 0, RLEFT|RESCC, + " inc AL,$1\n", }, + +/* inc word in memory by 1 (inc dst is R/IR/DA/X - no BA) */ +{ PLUS, FOREFF|FORCC, + SNBA, TWORD, + SONE, TANY, + 0, RLEFT|RESCC, + " inc AL,$1\n", }, + +/* dec word by 1 (represented as PLUS SMONE in PCC tree) */ +{ PLUS, FOREFF|INAREG|FORCC, + SAREG|SNAME, TWORD, + SMONE, TANY, + 0, RLEFT|RESCC, + " dec AL,$1\n", }, + +{ PLUS, FOREFF|FORCC, + SNBA, TWORD, + SMONE, TANY, + 0, RLEFT|RESCC, + " dec AL,$1\n", }, + +/* inc word by 2..16: "inc AL,$k" (2 bytes) instead of "add AL,$k" (4). + * SP16 is numeric 1..16 but $1 already matched SONE above; symbolic and + * out-of-range constants fall to the generic add rule below, which this + * must precede. inc sets flags arithmetically, so RESCC holds. */ +{ PLUS, FOREFF|INAREG|FORCC, + SAREG|SNAME, TWORD, + SP16, TANY, + 0, RLEFT|RESCC, + " inc AL,AR\n", }, + +{ PLUS, FOREFF|FORCC, + SNBA, TWORD, + SP16, TANY, + 0, RLEFT|RESCC, + " inc AL,AR\n", }, + +/* add word reg + reg/name/const (add src is R/IM/IR/DA/X - no BA) */ +{ PLUS, INAREG|FOREFF|FORCC, + SAREG, TWORD, + SAREG|SNAME|SCON, TWORD, + 0, RLEFT|RESCC, + " add AL,AR\n", }, + +/* add word reg + encodable OREG */ +{ PLUS, INAREG|FOREFF|FORCC, + SAREG, TWORD, + SNBA, TWORD, + 0, RLEFT|RESCC, + " add AL,AR\n", }, + +/* + * Pointer + constant: operate on the OFFSET WORD of the pair only (UL = + * low word; the segment word is untouched). Legal under the Coherent + * segmented model: no object crosses a segment boundary, so valid pointer + * arithmetic can never carry out of the 16-bit offset - the same + * assumption native cc makes (it emits inc r9,$1 on pointer pairs). + * PLAIN LONGS ARE EXCLUDED: for a real 32-bit integer the carry is + * arithmetic, so TLONG/TULONG keep the full addl/subl rules below. + * These must precede the generic pair rules (first match wins). + * inc/dec take 1..16 (2 bytes); add/sub take any word immediate + * (4 bytes, vs 6 for addl) but only a REGISTER destination, hence no + * SPCON memory forms. Memory forms use SNAME (DA+2) and SFRAME + * (X mode, off+2) - NOT SNBA: the low word of a zero-displacement + * pair-base OREG would be BA mode, which inc cannot encode. + */ +{ PLUS, INBREG|FOREFF, + SBREG, TPOINT, + SP16, TANY, + 0, RLEFT, + " inc UL,AR\n", }, + +{ PLUS, INBREG|FOREFF, + SBREG, TPOINT, + SMONE, TANY, + 0, RLEFT, + " dec UL,$1\n", }, + +{ PLUS, INBREG|FOREFF, + SBREG, TPOINT, + SPCON, TANY, + 0, RLEFT, + " add UL,AR\n", }, + +/* pointer in memory += 1..16 (via FINDMOPS, like the word inc rules) */ +{ PLUS, FOREFF, + SNAME, TPOINT, + SP16, TANY, + 0, RLEFT, + " inc UL,AR\n", }, + +{ PLUS, FOREFF, + SFRAME, TPOINT, + SP16, TANY, + 0, RLEFT, + " inc UL,AR\n", }, + +/* + * pointer + variable word index: clocal rewrites the index subtree of + * a pointer PLUS/MINUS to word type (stripping the int->long widen - + * the same segment-invariant license as the constant rules above), so + * the add happens on the offset word alone like native "add UL,rN" + * instead of exts+addl on a widened pair. add src is R/IM/IR/DA/X - + * no BA, hence the separate SNBA rule. + */ +{ PLUS, INBREG|FOREFF, + SBREG, TPOINT, + SAREG|SNAME, TWORD|TSHORT|TUSHORT, + 0, RLEFT, + " add UL,AR\n", }, + +{ PLUS, INBREG|FOREFF, + SBREG, TPOINT, + SNBA, TWORD|TSHORT|TUSHORT, + 0, RLEFT, + " add UL,AR\n", }, + +/* + * Address constant + word index (&arr[i] on a global array): lda's + * Indexed (X) mode encodes sym(rIDX) in one instruction where the + * generic path needs ldl $sym + add on the offset word. X mode + * CANNOT encode index r0 (an index field of 0 decodes as DA), and a + * nameless address constant has no DA/X spelling - the ZX escape + * emits the ldl+add fallback for those two cases. A1 never overlaps + * the index register (plain NREG(B,1), no sharing), so the fallback's + * ldl cannot clobber the index before the add reads it. NORIGHT(R0) + * steers the index temp away from r0 (one interference edge per + * matched node), so the allocator's lowest-first preference no longer + * forces the fallback on X-encodable sites. + */ +{ PLUS, INBREG, + SCON, TPOINT, + SAREG, TWORD|TSHORT|TUSHORT, + NEEDS(NREG(B, 1), NORIGHT(R0)), RESC1, + "ZX", }, + +/* add pair + pair (long/ptr); pointer offsets are widened to a pair */ +{ PLUS, INBREG|FOREFF, + SBREG, TLONG|TULONG|TPOINT, + SBREG|SNAME|SCON, TLONG|TULONG|TPOINT, + 0, RLEFT, + " addl AL,AR\n", }, + +{ PLUS, INBREG|FOREFF, + SBREG, TLONG|TULONG|TPOINT, + SNBA, TLONG|TULONG|TPOINT, + 0, RLEFT, + " addl AL,AR\n", }, + +/* + * Address of a frame object. ADDROF(OREG(r13,off)) is lowered by the MIP + * reader to PLUS(REG r13, ICON off) with pointer type. Since r13 is a + * word register (not SBREG), this falls through the addl rule above; the ZF + * escape emits "lda A1,L+off(r13); and A1hi,$32512" using the per-function + * frame-base equate (L=SS|framesize), which supplies the stack segment. + * The left shape must be SR13 (REG r13 exactly), NOT SANY: SANY also gives + * a direct match for NAME/OREG pointers (global_ptr + 1), stealing them + * from the addl rule (which needs a rewrite) and feeding garbage to ZF. + */ +{ PLUS, INBREG, + SR13, TPOINT, + SCON, TANY, + NBREG, RESC1, + "ZF", }, + +/* + * MINUS - integer subtraction. + */ + +/* dec word by 1 */ +{ MINUS, FOREFF|INAREG|FORCC, + SAREG|SNAME, TWORD, + SONE, TANY, + 0, RLEFT|RESCC, + " dec AL,$1\n", }, + +{ MINUS, FOREFF|FORCC, + SNBA, TWORD, + SONE, TANY, + 0, RLEFT|RESCC, + " dec AL,$1\n", }, + +/* inc word by 1 (MINUS SMONE) */ +{ MINUS, FOREFF|INAREG|FORCC, + SAREG|SNAME, TWORD, + SMONE, TANY, + 0, RLEFT|RESCC, + " inc AL,$1\n", }, + +{ MINUS, FOREFF|FORCC, + SNBA, TWORD, + SMONE, TANY, + 0, RLEFT|RESCC, + " inc AL,$1\n", }, + +/* dec word by 2..16: "dec AL,$k" (2 bytes) instead of "sub AL,$k" (4). + * $1 already matched SONE above; symbolic/out-of-range fall to the generic + * sub rule below, which this must precede. dec sets flags arithmetically. */ +{ MINUS, FOREFF|INAREG|FORCC, + SAREG|SNAME, TWORD, + SP16, TANY, + 0, RLEFT|RESCC, + " dec AL,AR\n", }, + +{ MINUS, FOREFF|FORCC, + SNBA, TWORD, + SP16, TANY, + 0, RLEFT|RESCC, + " dec AL,AR\n", }, + +/* subtract word reg - reg/name/const (sub src is R/IM/IR/DA/X - no BA) */ +{ MINUS, INAREG|FOREFF|FORCC, + SAREG, TWORD, + SAREG|SNAME|SCON, TWORD, + 0, RLEFT|RESCC, + " sub AL,AR\n", }, + +/* subtract word reg - encodable OREG */ +{ MINUS, INAREG|FOREFF|FORCC, + SAREG, TWORD, + SNBA, TWORD, + 0, RLEFT|RESCC, + " sub AL,AR\n", }, + +/* pointer - constant on the offset word only (see the PLUS comment: + * Coherent objects never cross a segment boundary; plain longs excluded) */ +{ MINUS, INBREG|FOREFF, + SBREG, TPOINT, + SP16, TANY, + 0, RLEFT, + " dec UL,AR\n", }, + +{ MINUS, INBREG|FOREFF, + SBREG, TPOINT, + SMONE, TANY, + 0, RLEFT, + " inc UL,$1\n", }, + +{ MINUS, INBREG|FOREFF, + SBREG, TPOINT, + SPCON, TANY, + 0, RLEFT, + " sub UL,AR\n", }, + +/* pointer in memory -= 1..16 (via FINDMOPS) */ +{ MINUS, FOREFF, + SNAME, TPOINT, + SP16, TANY, + 0, RLEFT, + " dec UL,AR\n", }, + +{ MINUS, FOREFF, + SFRAME, TPOINT, + SP16, TANY, + 0, RLEFT, + " dec UL,AR\n", }, + +/* pointer - variable word index (see the PLUS twin above) */ +{ MINUS, INBREG|FOREFF, + SBREG, TPOINT, + SAREG|SNAME, TWORD|TSHORT|TUSHORT, + 0, RLEFT, + " sub UL,AR\n", }, + +{ MINUS, INBREG|FOREFF, + SBREG, TPOINT, + SNBA, TWORD|TSHORT|TUSHORT, + 0, RLEFT, + " sub UL,AR\n", }, + +/* subtract pair (long/ptr); pointer offsets are widened to a pair */ +{ MINUS, INBREG|FOREFF, + SBREG, TLONG|TULONG|TPOINT, + SBREG|SNAME|SCON, TLONG|TULONG|TPOINT, + 0, RLEFT, + " subl AL,AR\n", }, + +{ MINUS, INBREG|FOREFF, + SBREG, TLONG|TULONG|TPOINT, + SNBA, TLONG|TULONG|TPOINT, + 0, RLEFT, + " subl AL,AR\n", }, + +/* + * Address of a frame object at a negative offset: &local becomes + * MINUS(REG r13, ICON off) via stref/offplus (cf. the PLUS rule above). + * The ZF escape emits the same "lda A1,L+off(r13); and A1hi,$32512" using + * the frame-base equate. SR13 left shape for the same reason as the PLUS + * rule: SANY would steal global_ptr - 1 from the subl rule. + */ +{ MINUS, INBREG, + SR13, TPOINT, + SCON, TANY, + NBREG, RESC1, + "ZF", }, + +/* + * MUL - multiplication. + */ + +/* + * word multiply: mult rr0,src multiplies the low word (r1) by src, + * leaving the 32-bit product in rr0. We force the multiplicand into + * r1, clobber r0 (the high word), and reclaim the low word r1 as the + * int result via RLEFT (which also avoids the RESCx class check). + */ +{ MUL, INAREG, + SAREG, TWORD, + SAREG|SNAME|SCON, TWORD, + NEEDS(NLEFT(R1), NEVER(R0), NRES(R1), NORIGHT(R0), NORIGHT(R1)), RLEFT, + " mult rr0,AR\n", }, + +/* + * long multiply: multl rq0,src multiplies the low pair (rr2) by src, + * leaving the 64-bit product in the rq0 quad (rr0:rr2). We force the + * multiplicand into rr2, clobber the high pair rr0, and reclaim the low + * pair rr2 as the long result via RLEFT. + */ +{ MUL, INBREG, + SBREG, TLONG|TULONG, + SBREG|SNAME|SCON, TLONG|TULONG, + NEEDS(NLEFT(RR2), NEVER(RR0), NRES(RR2), NORIGHT(RR0), NORIGHT(RR2)), RLEFT, + " multl rq0,AR\n", }, + +/* + * DIV - division. + */ + +/* + * word divide: div r0,src divides the 32-bit dividend in rr0 by the + * word src, leaving the quotient in r1 (low) and remainder in r0 (high). + * The dividend word is forced into r1 then sign/zero-extended into rr0. + * The quotient (r1) is reclaimed as the int result via RLEFT. The Z8001 + * has only a signed divide (div); unsigned divide zero-extends instead. + */ +{ DIV, INAREG, + SAREG, TINT, + SAREG|SNAME|SCON, TINT, + NEEDS(NLEFT(R1), NEVER(R0), NRES(R1), NORIGHT(R0), NORIGHT(R1)), RLEFT, + " exts rr0\n div r0,AR\n", }, + +/* unsigned word divide */ +{ DIV, INAREG, + SAREG, TUNSIGNED, + SAREG|SNAME|SCON, TUNSIGNED, + NEEDS(NLEFT(R1), NEVER(R0), NRES(R1), NORIGHT(R0), NORIGHT(R1)), RLEFT, + " clr r0\n div r0,AR\n", }, + +/* + * long divide: dividend forced into low pair rr2, extended into the rq0 + * quad, divl rr0,src leaves the quotient in rr2 (low) and remainder in + * rr0 (high). Quotient reclaimed via RLEFT. + */ +{ DIV, INBREG, + SBREG, TLONG, + SBREG|SNAME|SCON, TLONG, + NEEDS(NLEFT(RR2), NEVER(RR0), NRES(RR2), NORIGHT(RR0), NORIGHT(RR2)), RLEFT, + " extsl rq0\n divl rr0,AR\n", }, + +/* unsigned long divide */ +{ DIV, INBREG, + SBREG, TULONG, + SBREG|SNAME|SCON, TULONG, + NEEDS(NLEFT(RR2), NEVER(RR0), NRES(RR2), NORIGHT(RR0), NORIGHT(RR2)), RLEFT, + " subl rr0,rr0\n divl rr0,AR\n", }, + +/* + * MOD - remainder. + * Word: div leaves the remainder in r0 (high word); copy it down to r1 + * so the result lands in the register RLEFT reclaims. + */ +{ MOD, INAREG, + SAREG, TINT, + SAREG|SNAME|SCON, TINT, + NEEDS(NLEFT(R1), NEVER(R0), NRES(R1), NORIGHT(R0), NORIGHT(R1)), RLEFT, + " exts rr0\n div r0,AR\n ld r1,r0\n", }, + +{ MOD, INAREG, + SAREG, TUNSIGNED, + SAREG|SNAME|SCON, TUNSIGNED, + NEEDS(NLEFT(R1), NEVER(R0), NRES(R1), NORIGHT(R0), NORIGHT(R1)), RLEFT, + " clr r0\n div r0,AR\n ld r1,r0\n", }, + +/* + * Long remainder: divl leaves the remainder in the high pair rr0; copy + * it down to rr2 so the result lands in the register RLEFT reclaims. + */ +{ MOD, INBREG, + SBREG, TLONG, + SBREG|SNAME|SCON, TLONG, + NEEDS(NLEFT(RR2), NEVER(RR0), NRES(RR2), NORIGHT(RR0), NORIGHT(RR2)), RLEFT, + " extsl rq0\n divl rr0,AR\n ldl rr2,rr0\n", }, + +{ MOD, INBREG, + SBREG, TULONG, + SBREG|SNAME|SCON, TULONG, + NEEDS(NLEFT(RR2), NEVER(RR0), NRES(RR2), NORIGHT(RR0), NORIGHT(RR2)), RLEFT, + " subl rr0,rr0\n divl rr0,AR\n ldl rr2,rr0\n", }, + +/* + * Shifts. + */ + +/* shift left by 1 = self-add, the native idiom: 2 bytes vs 4 for sll. + * add sets all flags arithmetically for the doubled value (unlike sll, + * which leaves V alone), so the FORCC/RESCC claim is sound; the + * CCOKFORCOMP gate limits elided compares on an LS child to EQ/NE + * anyway. Must precede the generic sll rule (first match wins). */ +{ LS, INAREG|FOREFF|FORCC, + SAREG, TWORD, + SONE, TANY, + 0, RLEFT|RESCC, + " add AL,AL\n", }, + +/* logical shift left word, constant count */ +{ LS, INAREG|FOREFF, + SAREG, TWORD, + SCON, TWORD, + 0, RLEFT, + " sll AL,AR\n", }, + +/* arithmetic shift right (signed), constant count */ +{ RS, INAREG|FOREFF, + SAREG, TINT|TSHORT, + SCON, TWORD, + 0, RLEFT, + " sra AL,AR\n", }, + +/* logical shift right (unsigned), constant count */ +{ RS, INAREG|FOREFF, + SAREG, TUNSIGNED|TUSHORT, + SCON, TWORD, + 0, RLEFT, + " srl AL,AR\n", }, + +/* pair shift left by 1 = self-add: 2 bytes vs 4 for slll */ +{ LS, INBREG|FOREFF, + SBREG, TLONG|TULONG, + SONE, TANY, + 0, RLEFT, + " addl AL,AL\n", }, + +/* shift left long pair, constant count */ +{ LS, INBREG|FOREFF, + SBREG, TLONG|TULONG, + SCON, TWORD, + 0, RLEFT, + " slll AL,AR\n", }, + +/* arithmetic shift right long pair (signed), constant count */ +{ RS, INBREG|FOREFF, + SBREG, TLONG, + SCON, TWORD, + 0, RLEFT, + " sral AL,AR\n", }, + +/* logical shift right long pair (unsigned), constant count */ +{ RS, INBREG|FOREFF, + SBREG, TULONG, + SCON, TWORD, + 0, RLEFT, + " srll AL,AR\n", }, + +/* + * Variable (register) counts need the dynamic shift instructions + * sdl/sda/sdll/sdal: count in a word register, SIGNED - positive + * shifts left, negative shifts right. So left shifts use the count + * as-is and right shifts negate a copy of it into a scratch register. + * (sll/sra/srl only accept immediate counts; Coherent as crashes on + * a register operand there.) + */ + +/* logical shift left word, register count */ +{ LS, INAREG|FOREFF, + SAREG, TWORD, + SAREG, TWORD, + 0, RLEFT, + " sdl AL,AR\n", }, + +/* arithmetic shift right (signed), register count */ +{ RS, INAREG|FOREFF, + SAREG, TINT|TSHORT, + SAREG, TWORD, + NAREG, RLEFT, + " ld A1,AR\n neg A1\n sda AL,A1\n", }, + +/* logical shift right (unsigned), register count */ +{ RS, INAREG|FOREFF, + SAREG, TUNSIGNED|TUSHORT, + SAREG, TWORD, + NAREG, RLEFT, + " ld A1,AR\n neg A1\n sdl AL,A1\n", }, + +/* shift left long pair, register count */ +{ LS, INBREG|FOREFF, + SBREG, TLONG|TULONG, + SAREG, TWORD, + 0, RLEFT, + " sdll AL,AR\n", }, + +/* arithmetic shift right long pair (signed), register count */ +{ RS, INBREG|FOREFF, + SBREG, TLONG, + SAREG, TWORD, + NAREG, RLEFT, + " ld A1,AR\n neg A1\n sdal AL,A1\n", }, + +/* logical shift right long pair (unsigned), register count */ +{ RS, INBREG|FOREFF, + SBREG, TULONG, + SAREG, TWORD, + NAREG, RLEFT, + " ld A1,AR\n neg A1\n sdll AL,A1\n", }, + +/* + * AND - bitwise and. + */ + +/* and/or/xor: dst must be a register; src is R/IM/IR/DA/X - no BA. + * The long ZL forms touch both pair halves (off and off+2), so their + * memory operand must be a name or a frame slot (SFRAME), never + * pair-based. */ + +/* + * Single-bit truth test: for "(x & pow2) ==/!= 0" the compare-vs-zero + * elision hands the AND a FORCC cookie (gated to EQ/NE consumers by + * CCOKFORCOMP in macdefs.h - BIT sets ONLY Z, with the same sense as + * the and: Z = 1 iff the tested bit is 0). bit is 2 bytes vs 4 for + * and-immediate, leaves the operand register intact, and the memory + * forms (dst R/IR/DA/X - no BA) absorb the operand load entirely. + * Pure-FORCC rules: never selected in value contexts, and they must + * precede the general and rules (first match at equal shape level). + */ +{ AND, FORCC, + SAREG|SNAME, TWORD, + SPOW2, TANY, + 0, RESCC, + " bit AL,ZJ\n", }, + +{ AND, FORCC, + SNBA, TWORD, + SPOW2, TANY, + 0, RESCC, + " bit AL,ZJ\n", }, + +/* + * Single-bit clear: x &= ~(1 << k). The mask reaches pass 2 as an + * ICON with exactly one bit CLEAR in the operand's width (SNPOW2, + * either sign representation); ZY prints that bit's number. res is + * 2 bytes vs 4 for and-immediate, and the memory forms (dst R/IR/DA/X + * like bit) fire through findmops for "g &= ~mask", absorbing the + * load/store pair. res affects NO flags, so no FORCC/RESCC claims - + * flag consumers fall through to the and rules below. Must precede + * them (first match at equal shape level). + */ +{ AND, INAREG|FOREFF, + SAREG|SNAME, TWORD, + SNPOW2, TANY, + 0, RLEFT, + " res AL,ZY\n", }, + +{ AND, FOREFF, + SNBA, TWORD, + SNPOW2, TANY, + 0, RLEFT, + " res AL,ZY\n", }, + +{ AND, INAREG|FOREFF|FORCC, + SAREG, TWORD, + SAREG|SNAME|SCON, TWORD, + 0, RLEFT|RESCC, + " and AL,AR\n", }, + +{ AND, INAREG|FOREFF|FORCC, + SAREG, TWORD, + SNBA, TWORD, + 0, RLEFT|RESCC, + " and AL,AR\n", }, + +{ AND, INBREG|FOREFF, + SBREG, TLONG|TULONG, + SBREG|SNAME|SCON, TLONG|TULONG, + 0, RLEFT, + "ZL", }, + +{ AND, INBREG|FOREFF, + SBREG, TLONG|TULONG, + SFRAME, TLONG|TULONG, + 0, RLEFT, + "ZL", }, + +/* char AND: clocal narrows char-mask truth tests ((c & m) ==/!= k with + * m,k in 0..255) back to byte width, so these only ever compute an + * EQ/NE condition. andb dst is a register, src R/IM/IR/DA/X (as + * S_RSRC); a byte immediate is replicated into both halves by as + * (op&W==0), the same encoding path as cpb Rb,IM. RESCC is safe for + * the eq/ne consumer only: andb sets Z from the result but P/V is + * parity, exactly like testb. */ + +/* single-bit char truth test: bitb, exactly as the word bit rules + * above (the byte IR form absorbs the ldb of a pair-based deref) */ +{ AND, FORCC, + SDREG|SNAME, TCHAR|TUCHAR, + SPOW2, TANY, + 0, RESCC, + " bitb AL,ZJ\n", }, + +{ AND, FORCC, + SNBA, TCHAR|TUCHAR, + SPOW2, TANY, + 0, RESCC, + " bitb AL,ZJ\n", }, + +{ AND, INDREG|FOREFF|FORCC, + SDREG, TCHAR|TUCHAR, + SDREG|SNAME|SCON, TCHAR|TUCHAR, + 0, RLEFT|RESCC, + " andb AL,AR\n", }, + +{ AND, INDREG|FOREFF|FORCC, + SDREG, TCHAR|TUCHAR, + SNBA, TCHAR|TUCHAR, + 0, RLEFT|RESCC, + " andb AL,AR\n", }, + +/* + * OR - bitwise or. + */ + +/* + * Single-bit set: x |= (1 << k) with a constant single-bit mask + * (SPOW2; ZJ prints it as a bit number, as in the bit rules). set is + * 2 bytes vs 4 for or-immediate and the memory forms fire through + * findmops. Like res: NO flags affected, no FORCC/RESCC, must + * precede the generic or rules. + */ +{ OR, INAREG|FOREFF, + SAREG|SNAME, TWORD, + SPOW2, TANY, + 0, RLEFT, + " set AL,ZJ\n", }, + +{ OR, FOREFF, + SNBA, TWORD, + SPOW2, TANY, + 0, RLEFT, + " set AL,ZJ\n", }, + +{ OR, INAREG|FOREFF|FORCC, + SAREG, TWORD, + SAREG|SNAME|SCON, TWORD, + 0, RLEFT|RESCC, + " or AL,AR\n", }, + +{ OR, INAREG|FOREFF|FORCC, + SAREG, TWORD, + SNBA, TWORD, + 0, RLEFT|RESCC, + " or AL,AR\n", }, + +{ OR, INBREG|FOREFF, + SBREG, TLONG|TULONG, + SBREG|SNAME|SCON, TLONG|TULONG, + 0, RLEFT, + "ZL", }, + +{ OR, INBREG|FOREFF, + SBREG, TLONG|TULONG, + SFRAME, TLONG|TULONG, + 0, RLEFT, + "ZL", }, + +/* + * ER - bitwise exclusive or. + */ + +{ ER, INAREG|FOREFF|FORCC, + SAREG, TWORD, + SAREG|SNAME|SCON, TWORD, + 0, RLEFT|RESCC, + " xor AL,AR\n", }, + +{ ER, INAREG|FOREFF|FORCC, + SAREG, TWORD, + SNBA, TWORD, + 0, RLEFT|RESCC, + " xor AL,AR\n", }, + +{ ER, INBREG|FOREFF, + SBREG, TLONG|TULONG, + SBREG|SNAME|SCON, TLONG|TULONG, + 0, RLEFT, + "ZL", }, + +{ ER, INBREG|FOREFF, + SBREG, TLONG|TULONG, + SFRAME, TLONG|TULONG, + 0, RLEFT, + "ZL", }, + +/* + * ASSIGN - assignments. + */ + +/* word reg <- zero */ +{ ASSIGN, FOREFF|INAREG, + SAREG, TWORD, + SZERO, TANY, + 0, RDEST, + " clr AL\n", }, + +/* word mem <- zero (clr dst is R/IR/DA/X - no BA) */ +{ ASSIGN, FOREFF, + SNAME, TWORD, + SZERO, TANY, + 0, 0, + " clr AL\n", }, + +{ ASSIGN, FOREFF, + SNBA, TWORD, + SZERO, TANY, + 0, 0, + " clr AL\n", }, + +/* pair reg <- zero (long): "subl rrN,rrN" is 2 bytes and zeroes the whole + * pair, vs the 4-byte two-clr ZQ (which memory targets still need, having + * no reg-reg subl). RDEST valid: the cleared pair IS the value. No FORCC: + * subl sets flags for the full long, but nothing here consumes them. */ +{ ASSIGN, FOREFF|INBREG, + SBREG, TLONG|TULONG|TPOINT, + SZERO, TANY, + 0, RDEST, + " subl AL,AL\n", }, + +/* pair in memory <- zero; FOREFF ONLY. ZQ clears the two memory words + * and leaves NO register holding the value, so this must not offer + * INBREG+RDEST: a chained assignment (chars = words = lines = 0) would + * reclaim an uninitialized pair as the inner assignment's value. The + * chained case falls back to the reg path (ldl rrN,$0; ldl mem,rrN). */ +{ ASSIGN, FOREFF, + SNAME, TLONG|TULONG|TPOINT, + SZERO, TANY, + 0, 0, + "ZQ", }, + +{ ASSIGN, FOREFF, + SFRAME, TLONG|TULONG|TPOINT, + SZERO, TANY, + 0, 0, + "ZQ", }, + +/* word reg <- small constant: 2-byte ldk. ldk sets no flags, so no + * FORCC/RESCC here - a compare-context assign falls back to the generic + * rule below. Must precede it (first match wins at equal level). */ +{ ASSIGN, FOREFF|INAREG, + SAREG, TWORD, + SLDK, TANY, + 0, RDEST, + " ldk AL,AR\n", }, + +/* word reg <- reg */ +{ ASSIGN, FOREFF|INAREG|FORCC, + SAREG, TWORD, + SAREG|SNAME|SOREG|SCON, TWORD, + 0, RDEST|RESCC, + " ld AL,AR\n", }, + +/* word mem <- reg (INAREG: the stored value stays available in AR) */ +{ ASSIGN, FOREFF|INAREG|FORCC, + SNAME|SOREG, TWORD, + SAREG, TWORD, + 0, RDEST|RESCC, + " ld AL,AR\n", }, + +/* word mem <- const (ld dst,#imm takes IR/DA/X - no BA; a BA dest falls + * back to the mem <- reg rule with the constant loaded first) */ +{ ASSIGN, FOREFF, + SNAME, TWORD, + SCON, TANY, + 0, 0, + " ld AL,AR\n", }, + +{ ASSIGN, FOREFF, + SNBA, TWORD, + SCON, TANY, + 0, 0, + " ld AL,AR\n", }, + +/* pair reg <- numeric long/ptr constant: Z0 splits into clr/ldk/ld when no + * larger than the 6-byte ldl #imm32 (see the Z0 escape), else emits ldl. + * SLCON is numeric ICON only; symbolic constants fail it and take the + * generic ldl rule below, which this must precede. */ +{ ASSIGN, FOREFF|INBREG, + SBREG, TLONG|TULONG|TPOINT, + SLCON, TANY, + 0, RDEST, + "Z0", }, + +/* pair reg <- pair reg or mem (long/ptr/float). + * NORIGHT(RR0): clocal rewrites return into ASSIGN(REG-rr0, value); for + * a reg-reg assign insnwalk moveadds the right side with the precolored + * rr0 node, and coalescing bypasses the clregs selectable mask (same + * bug class as the session-5 CALL-result coalesce). If the returned + * pair is also used as an indirect base that yields the forbidden + * (rr0). The conflict edge blocks only that coalesce, so such returns + * emit an explicit ldl rr0,rrN like native. */ +{ ASSIGN, FOREFF|INBREG, + SBREG, TLONG|TULONG|TPOINT|TFLT, + SBREG|SNAME|SOREG|SCON, TLONG|TULONG|TPOINT|TFLT, + NEEDS(NORIGHT(RR0)), RDEST, + " ldl AL,AR\n", }, + +/* pair mem <- pair reg (long/ptr/float) */ +{ ASSIGN, FOREFF|INBREG, + SNAME|SOREG, TLONG|TULONG|TPOINT|TFLT, + SBREG, TLONG|TULONG|TPOINT|TFLT, + 0, RDEST, + " ldl AL,AR\n", }, + +/* double: quad reg <- quad reg or mem, and mem <- quad reg. ZD picks + * ldm for frame/name memory and two ldl (IR/BA) for pair-based memory, + * ordering the halves so a base pair inside the target quad survives. */ +{ ASSIGN, FOREFF|INCREG, + SCREG, TDBL, + SCREG|SNAME|SOREG, TDBL, + 0, RDEST, + "ZD", }, + +{ ASSIGN, FOREFF|INCREG, + SNAME|SOREG, TDBL, + SCREG, TDBL, + 0, RDEST, + "ZD", }, + +/* pair mem <- const long: there is NO direct rule on purpose. The + * hardware has no "ldl mem,#imm" form (Coherent as mchld: only word/byte + * have the LDI store path), so the constant is materialized into a pair + * register (OPLTYPE ldl $imm) and stored via the mem <- pair rule above. */ + +/* byte reg <- zero: clrb sets no flags (manual: "No flags affected"), + * so no FORCC/RESCC - a compare-context assign falls back to the + * generic rules below (first match wins at equal level). */ +{ ASSIGN, FOREFF|INDREG, + SDREG, TCHAR|TUCHAR, + SZERO, TANY, + 0, RDEST, + " clrb AL\n", }, + +/* byte mem <- zero (clrb dst is R/IR/DA/X - no BA, like clr) */ +{ ASSIGN, FOREFF, + SNAME, TCHAR|TUCHAR, + SZERO, TANY, + 0, 0, + " clrb AL\n", }, + +{ ASSIGN, FOREFF, + SNBA, TCHAR|TUCHAR, + SZERO, TANY, + 0, 0, + " clrb AL\n", }, + +/* + * BCLR - block memory clear (ANSI zero-fill of a partially-initialized auto + * aggregate's tail, folded from the per-byte clrb run by myreader()). AL is + * the buffer's first byte (a frame OREG); the node's n_lval is the byte + * count. ZA clears byte 0, then propagates the zero forward with one ldirb. + * The pair result slot doubles as the dst scratch (DECRA packs only 3 regs: + * result + A1 count + A2 src pair); the result is discarded (FOREFF). + */ +{ BCLR, FOREFF, + SOREG|SNAME, TANY, + SANY, TANY, + NEEDS(NREG(A, 1), NREG(B, 1)), 0, + "ZA", }, + +/* byte/char reg <- reg, mem or const (byte registers print as rlN) */ +{ ASSIGN, FOREFF|INDREG|FORCC, + SDREG, TCHAR|TUCHAR, + SDREG|SNAME|SOREG|SCON, TCHAR|TUCHAR, + 0, RDEST|RESCC, + " ldb AL,AR\n", }, + +/* byte mem <- reg */ +{ ASSIGN, FOREFF|INDREG|FORCC, + SNAME|SOREG, TCHAR|TUCHAR, + SDREG, TCHAR|TUCHAR, + 0, RDEST|RESCC, + " ldb AL,AR\n", }, + +/* byte mem <- const (ldb dst,#imm takes IR/DA/X - no BA) */ +{ ASSIGN, FOREFF, + SNAME, TCHAR|TUCHAR, + SCON, TANY, + 0, 0, + " ldb AL,AR\n", }, + +{ ASSIGN, FOREFF, + SNBA, TCHAR|TUCHAR, + SCON, TANY, + 0, 0, + " ldb AL,AR\n", }, + +/* struct assignment: block-copy via ldirb. A1 = dest-address pair, + * A2 = byte count; the source pointer (right) is a pair. NORIGHT(RR0) + * keeps the source out of rr0 - it becomes an ldirb indirect base, and + * rr0 cannot be one. A struct-call result coalesces with the + * precolored rr0 (bypassing the clregs mask), so without this the + * copy-out of a struct return emits "ldirb ...,(rr0)". */ +{ STASG, FOREFF|INAREG, + SOREG|SNAME, TANY, + SBREG, TPTRTO|TANY, + NEEDS(NREG(A, 1), NREG(B, 1), NORIGHT(RR0)), RDEST, + "ZS", }, + +/* + * UMUL - indirection (load through pointer). + * On Z8001 the address MUST be in a 32-bit pair register. + * SOREG is used by the MIP layer after offstar() converts an UMUL + * whose operand is a pair-register OREG. + */ + +/* load word through pair register (SOREG) */ +{ UMUL, INAREG, + SANY, TANY, + SOREG, TWORD|TSHORT|TUSHORT, + XSL(A), RESC1, + " ld A1,AL\n", }, + +/* load long/ptr/float through pair register */ +{ UMUL, INBREG, + SANY, TANY, + SOREG, TLONG|TULONG|TPOINT|TFLT, + XSL(B), RESC1, + " ldl A1,AL\n", }, + +/* load double through pair register. No NSL: the result quad spans + * two pairs and ZE's ordered two-ldl only protects the base pair's own + * half; keeping the temp disjoint from the left operand is the safe + * default (the base may also be a regw-less OREG). */ +{ UMUL, INCREG, + SANY, TANY, + SOREG, TDBL, + NEEDS(NREG(C, 1)), RESC1, + "ZE", }, + +/* load char through pair register into a byte register. No extension + * here: promotion to int is a separate SCONV (usually fused back to + * "ldb rlN,(rrM); extsb rN" by NSL sharing). A single ldb reads its + * address before writing the destination, so even a destination whose + * word lies inside the base pair is safe. */ +{ UMUL, INDREG, + SANY, TANY, + SOREG, TCHAR|TUCHAR, + NDREG, RESC1, + " ldb A1,AL\n", }, + +/* + * Logical/comparison operators. + * OPLOG covers EQ, NE, LE, GE, LT, GT, ULE, UGE, ULT, UGT. + */ + +/* + * Compare vs zero. For the ORDERED conditions we must use cp/cpl (a real + * subtraction), NOT test/testl: TEST performs "dst OR 0" (Z8000 manual), so + * it sets S and Z but leaves the P/V flag as parity rather than signed- + * overflow. The signed conditions lt/ge/gt/le are (S xor V), so after TEST + * they misfire at the 0 boundary (e.g. "0 < 0" wrongly taken). cp/cpl set + * S, V, C and Z correctly for all signed and unsigned conditions. + * + * EQ/NE read ONLY the Z flag, which TEST does set correctly - so equality + * against zero uses test/testl/testb like native cc. test takes dst = + * R/IR/DA/X (as S_CLR class), so it also works directly on memory: testl + * mem replaces an ldl+cpl pair and testb mem a ldb/extsb/cp triple. The + * EQ/NE rules must precede the OPLOG cp rules: for an equality compare + * both match at the same level and the first table entry wins. + */ +/* + * cp/cpb have two hardware forms (Coherent as S_CP): "cp Rd,src" with + * src = R/IM/IR/DA/X, and the immediate-compare "cp dst,#imm" with + * dst = IR/DA/X. There is NO mem-vs-reg compare and NO BA operand. + * cpl has ONLY the register-dst form: "cpl RRd,src". + */ + +/* equality vs zero, word */ +{ EQ, FORCC, + SAREG|SNAME, TWORD, + SZERO, TANY, + 0, RESCC, + " test AL\n", }, + +{ NE, FORCC, + SAREG|SNAME, TWORD, + SZERO, TANY, + 0, RESCC, + " test AL\n", }, + +{ EQ, FORCC, + SNBA, TWORD, + SZERO, TANY, + 0, RESCC, + " test AL\n", }, + +{ NE, FORCC, + SNBA, TWORD, + SZERO, TANY, + 0, RESCC, + " test AL\n", }, + +/* equality vs zero, long/pointer: unlike cpl, testl also takes memory */ +{ EQ, FORCC, + SBREG|SNAME, TLONG|TULONG|TPOINT, + SZERO, TANY, + 0, RESCC, + " testl AL\n", }, + +{ NE, FORCC, + SBREG|SNAME, TLONG|TULONG|TPOINT, + SZERO, TANY, + 0, RESCC, + " testl AL\n", }, + +{ EQ, FORCC, + SNBA, TLONG|TULONG|TPOINT, + SZERO, TANY, + 0, RESCC, + " testl AL\n", }, + +{ NE, FORCC, + SNBA, TLONG|TULONG|TPOINT, + SZERO, TANY, + 0, RESCC, + " testl AL\n", }, + +/* equality vs zero, char (byte registers print as rlN) */ +{ EQ, FORCC, + SDREG|SNAME, TCHAR|TUCHAR, + SZERO, TANY, + 0, RESCC, + " testb AL\n", }, + +{ NE, FORCC, + SDREG|SNAME, TCHAR|TUCHAR, + SZERO, TANY, + 0, RESCC, + " testb AL\n", }, + +{ EQ, FORCC, + SNBA, TCHAR|TUCHAR, + SZERO, TANY, + 0, RESCC, + " testb AL\n", }, + +{ NE, FORCC, + SNBA, TCHAR|TUCHAR, + SZERO, TANY, + 0, RESCC, + " testb AL\n", }, + +/* + * Sign test vs zero: LT and GE against 0 are decided by the S flag + * alone, which test/testl set correctly - the ZO escape makes cbgen + * branch on mi/pl instead of the generic lt/ge (those are S xor V, + * and TEST leaves P/V as parity/stale; native cc uses the same + * "test; jr mi/pl" idiom). GT/LE also need Z and V, so they fall + * through to the cp/cpl rules below. Like the EQ/NE test rules + * these must precede the OPLOG cp rules. + */ +{ LT, FORCC, + SAREG|SNAME, TWORD, + SZERO, TANY, + 0, RESCC, + " test AL\nZO", }, + +{ GE, FORCC, + SAREG|SNAME, TWORD, + SZERO, TANY, + 0, RESCC, + " test AL\nZO", }, + +{ LT, FORCC, + SNBA, TWORD, + SZERO, TANY, + 0, RESCC, + " test AL\nZO", }, + +{ GE, FORCC, + SNBA, TWORD, + SZERO, TANY, + 0, RESCC, + " test AL\nZO", }, + +{ LT, FORCC, + SBREG|SNAME, TLONG|TULONG|TPOINT, + SZERO, TANY, + 0, RESCC, + " testl AL\nZO", }, + +{ GE, FORCC, + SBREG|SNAME, TLONG|TULONG|TPOINT, + SZERO, TANY, + 0, RESCC, + " testl AL\nZO", }, + +{ LT, FORCC, + SNBA, TLONG|TULONG|TPOINT, + SZERO, TANY, + 0, RESCC, + " testl AL\nZO", }, + +{ GE, FORCC, + SNBA, TLONG|TULONG|TPOINT, + SZERO, TANY, + 0, RESCC, + " testl AL\nZO", }, + +/* compare word vs zero */ +{ OPLOG, FORCC, + SAREG|SNAME, TWORD, + SZERO, TANY, + 0, RESCC, + " cp AL,$0\n", }, + +{ OPLOG, FORCC, + SNBA, TWORD, + SZERO, TANY, + 0, RESCC, + " cp AL,$0\n", }, + +/* compare pair vs zero: register only (no cpl mem,#imm form exists) */ +{ OPLOG, FORCC, + SBREG, TLONG|TULONG|TPOINT, + SZERO, TANY, + 0, RESCC, + " cpl AL,$0\n", }, + +/* compare word reg vs reg/name/const */ +{ OPLOG, FORCC, + SAREG, TWORD, + SAREG|SNAME|SCON, TWORD, + 0, RESCC, + " cp AL,AR\n", }, + +{ OPLOG, FORCC, + SAREG, TWORD, + SNBA, TWORD, + 0, RESCC, + " cp AL,AR\n", }, + +/* compare word mem vs const (immediate-compare form) */ +{ OPLOG, FORCC, + SNAME, TWORD, + SCON, TWORD, + 0, RESCC, + " cp AL,AR\n", }, + +{ OPLOG, FORCC, + SNBA, TWORD, + SCON, TWORD, + 0, RESCC, + " cp AL,AR\n", }, + +/* compare pair vs pair (long/ptr): left must be a register */ +{ OPLOG, FORCC, + SBREG, TLONG|TULONG|TPOINT, + SBREG|SNAME|SCON, TLONG|TULONG|TPOINT, + 0, RESCC, + " cpl AL,AR\n", }, + +{ OPLOG, FORCC, + SBREG, TLONG|TULONG|TPOINT, + SNBA, TLONG|TULONG|TPOINT, + 0, RESCC, + " cpl AL,AR\n", }, + +/* compare char vs char (byte registers print as rlN) */ +{ OPLOG, FORCC, + SDREG, TCHAR|TUCHAR, + SDREG|SNAME|SCON, TCHAR|TUCHAR, + 0, RESCC, + " cpb AL,AR\n", }, + +{ OPLOG, FORCC, + SDREG, TCHAR|TUCHAR, + SNBA, TCHAR|TUCHAR, + 0, RESCC, + " cpb AL,AR\n", }, + +/* compare char mem vs const (immediate-compare form) */ +{ OPLOG, FORCC, + SNAME, TCHAR|TUCHAR, + SCON, TCHAR|TUCHAR, + 0, RESCC, + " cpb AL,AR\n", }, + +{ OPLOG, FORCC, + SNBA, TCHAR|TUCHAR, + SCON, TCHAR|TUCHAR, + 0, RESCC, + " cpb AL,AR\n", }, + +/* + * Value-context relationals (kept in the tree by KEEPLOGOPVALUE): + * materialize the 0/1 result with "compare ; clr A1 ; tcc cc,A1". + * TCC cc,Rd sets only bit 0 of Rd when cc holds - all other bits and + * all flags untouched - and CLR sets no flags, so the order compare/ + * clr/tcc is mandatory and also makes it safe for A1 to share a + * register with a compare operand (XSL sharing; the compare reads + * first). ZV prints the relop's own cc name, non-negated (value + * sense). + * + * Like the FORCC set: EQ/NE against zero go through test/testl/testb + * (2 bytes shorter than the cp $0 immediate form, and the memory forms + * absorb the operand load); everything else through cp/cpl/cpb, which + * set all flags correctly for the ordered and unsigned conditions + * (LT/GE-vs-0 need no mi/pl escape here: cp's V is valid). The + * result is always a word (relop type is INT), so the tcc is always + * the word form regardless of operand width. + */ + +/* equality vs zero -> 0/1, word */ +{ EQ, INAREG, + SAREG|SNAME, TWORD, + SZERO, TANY, + XSL(A), RESC1, + " test AL\n clr A1\n tcc ZV,A1\n", }, + +{ NE, INAREG, + SAREG|SNAME, TWORD, + SZERO, TANY, + XSL(A), RESC1, + " test AL\n clr A1\n tcc ZV,A1\n", }, + +{ EQ, INAREG, + SNBA, TWORD, + SZERO, TANY, + XSL(A), RESC1, + " test AL\n clr A1\n tcc ZV,A1\n", }, + +{ NE, INAREG, + SNBA, TWORD, + SZERO, TANY, + XSL(A), RESC1, + " test AL\n clr A1\n tcc ZV,A1\n", }, + +/* equality vs zero -> 0/1, long/pointer */ +{ EQ, INAREG, + SBREG|SNAME, TLONG|TULONG|TPOINT, + SZERO, TANY, + XSL(A), RESC1, + " testl AL\n clr A1\n tcc ZV,A1\n", }, + +{ NE, INAREG, + SBREG|SNAME, TLONG|TULONG|TPOINT, + SZERO, TANY, + XSL(A), RESC1, + " testl AL\n clr A1\n tcc ZV,A1\n", }, + +{ EQ, INAREG, + SNBA, TLONG|TULONG|TPOINT, + SZERO, TANY, + XSL(A), RESC1, + " testl AL\n clr A1\n tcc ZV,A1\n", }, + +{ NE, INAREG, + SNBA, TLONG|TULONG|TPOINT, + SZERO, TANY, + XSL(A), RESC1, + " testl AL\n clr A1\n tcc ZV,A1\n", }, + +/* equality vs zero -> 0/1, char */ +{ EQ, INAREG, + SDREG|SNAME, TCHAR|TUCHAR, + SZERO, TANY, + XSL(A), RESC1, + " testb AL\n clr A1\n tcc ZV,A1\n", }, + +{ NE, INAREG, + SDREG|SNAME, TCHAR|TUCHAR, + SZERO, TANY, + XSL(A), RESC1, + " testb AL\n clr A1\n tcc ZV,A1\n", }, + +{ EQ, INAREG, + SNBA, TCHAR|TUCHAR, + SZERO, TANY, + XSL(A), RESC1, + " testb AL\n clr A1\n tcc ZV,A1\n", }, + +{ NE, INAREG, + SNBA, TCHAR|TUCHAR, + SZERO, TANY, + XSL(A), RESC1, + " testb AL\n clr A1\n tcc ZV,A1\n", }, + +/* any relop vs zero -> 0/1, word (ordered/unsigned land here) */ +{ OPLOG, INAREG, + SAREG|SNAME, TWORD, + SZERO, TANY, + XSL(A), RESC1, + " cp AL,$0\n clr A1\n tcc ZV,A1\n", }, + +{ OPLOG, INAREG, + SNBA, TWORD, + SZERO, TANY, + XSL(A), RESC1, + " cp AL,$0\n clr A1\n tcc ZV,A1\n", }, + +/* any relop -> 0/1, word reg vs reg/name/const */ +{ OPLOG, INAREG, + SAREG, TWORD, + SAREG|SNAME|SCON, TWORD, + XSL(A), RESC1, + " cp AL,AR\n clr A1\n tcc ZV,A1\n", }, + +{ OPLOG, INAREG, + SAREG, TWORD, + SNBA, TWORD, + XSL(A), RESC1, + " cp AL,AR\n clr A1\n tcc ZV,A1\n", }, + +/* any relop -> 0/1, word mem vs const (immediate-compare form) */ +{ OPLOG, INAREG, + SNAME, TWORD, + SCON, TWORD, + XSL(A), RESC1, + " cp AL,AR\n clr A1\n tcc ZV,A1\n", }, + +{ OPLOG, INAREG, + SNBA, TWORD, + SCON, TWORD, + XSL(A), RESC1, + " cp AL,AR\n clr A1\n tcc ZV,A1\n", }, + +/* any relop vs zero -> 0/1, pair: register only (no cpl mem,#imm) */ +{ OPLOG, INAREG, + SBREG, TLONG|TULONG|TPOINT, + SZERO, TANY, + XSL(A), RESC1, + " cpl AL,$0\n clr A1\n tcc ZV,A1\n", }, + +/* any relop -> 0/1, pair vs pair */ +{ OPLOG, INAREG, + SBREG, TLONG|TULONG|TPOINT, + SBREG|SNAME|SCON, TLONG|TULONG|TPOINT, + XSL(A), RESC1, + " cpl AL,AR\n clr A1\n tcc ZV,A1\n", }, + +{ OPLOG, INAREG, + SBREG, TLONG|TULONG|TPOINT, + SNBA, TLONG|TULONG|TPOINT, + XSL(A), RESC1, + " cpl AL,AR\n clr A1\n tcc ZV,A1\n", }, + +/* any relop -> 0/1, char vs char */ +{ OPLOG, INAREG, + SDREG, TCHAR|TUCHAR, + SDREG|SNAME|SCON, TCHAR|TUCHAR, + XSL(A), RESC1, + " cpb AL,AR\n clr A1\n tcc ZV,A1\n", }, + +{ OPLOG, INAREG, + SDREG, TCHAR|TUCHAR, + SNBA, TCHAR|TUCHAR, + XSL(A), RESC1, + " cpb AL,AR\n clr A1\n tcc ZV,A1\n", }, + +/* any relop -> 0/1, char mem vs const (immediate-compare form) */ +{ OPLOG, INAREG, + SNAME, TCHAR|TUCHAR, + SCON, TCHAR|TUCHAR, + XSL(A), RESC1, + " cpb AL,AR\n clr A1\n tcc ZV,A1\n", }, + +{ OPLOG, INAREG, + SNBA, TCHAR|TUCHAR, + SCON, TCHAR|TUCHAR, + XSL(A), RESC1, + " cpb AL,AR\n clr A1\n tcc ZV,A1\n", }, + +/* + * GOTO - unconditional jump. + */ +{ GOTO, FOREFF, + SCON, TANY, + SANY, TANY, + 0, RNOP, + " jr LL\n", }, + +/* indirect jump through pair register */ +{ GOTO, FOREFF, + SBREG, TANY, + SANY, TANY, + 0, RNOP, + " jp (AL)\n", }, + +/* + * SWDISP - sparse-switch cpir dispatch (built by myreader() from the + * mygenswitch() marks). The switch value AL (a word A-register) is searched + * against the case-value table; the matched target address is produced in + * the result pair A1, through which the enclosing GOTO(SWDISP) jumps. + * Resources are ordered by class (like STASG's ZS): A1 = A-class case count + * (word); A2 = B-class search pointer / reloaded target (the pair result, so + * RESC2). No NSL: the default resource<->left interference keeps the value + * register out of both the count and the pair, as cpir requires. + */ +{ SWDISP, INBREG, + SAREG, TWORD, + SANY, TANY, + NEEDS(NREG(A, 1), NREG(B, 1)), RESC2, + "ZZ", }, + +/* + * OPLTYPE - load leaf type into register. + * Used to materialize NAME/ICON/OREG into a register. + */ + +/* load small word constant: ldk takes exactly 0..15 (2 bytes vs 4 for + * ld $imm; as S_LDK). Must precede the generic rule - findleaf takes + * the first direct match. */ +{ OPLTYPE, INAREG, + SANY, TANY, + SLDK, TWORD|TSHORT|TUSHORT, + NAREG, RESC1, + " ldk A1,AL\n", }, + +/* load word constant/name/oreg into word register */ +{ OPLTYPE, INAREG, + SANY, TANY, + SAREG|SCON|SOREG|SNAME, TWORD|TSHORT|TUSHORT, + NAREG, RESC1, + " ld A1,AL\n", }, + +/* materialize a numeric long/ptr constant into a pair register: Z0 splits + * into clr/ldk/ld when no larger than the 6-byte ldl #imm32. Must precede + * the generic rule; symbolic constants fail SLCON and take the ldl there. */ +{ OPLTYPE, INBREG, + SANY, TANY, + SLCON, TLONG|TULONG|TPOINT, + NBREG, RESC1, + "Z0", }, + +/* load long/ptr/float constant/name/oreg into pair register */ +{ OPLTYPE, INBREG, + SANY, TANY, + SBREG|SCON|SOREG|SNAME, TLONG|TULONG|TPOINT|TFLT, + NBREG, RESC1, + " ldl A1,AL\n", }, + +/* load double from memory (or move a quad) into a quad register. + * No double constants reach pass 2: myp2tree materializes FCONs into + * the data segment as NAMEs. */ +{ OPLTYPE, INCREG, + SANY, TANY, + SCREG|SOREG|SNAME, TDBL, + NEEDS(NREG(C, 1)), RESC1, + "ZE", }, + +/* materialize a char leaf (reg, mem or constant) into a byte register. + * No extension here: byte registers hold raw bytes, promotion is an + * SCONV. ldb rlN,$imm is the byte immediate-load form. */ +{ OPLTYPE, INDREG, + SANY, TANY, + SDREG|SOREG|SNAME|SCON, TCHAR|TUCHAR, + NDREG, RESC1, + " ldb A1,AL\n", }, + +/* load address (lda) of SOREG/SNAME into pair register */ +{ OPLTYPE, INBREG, + SANY, TANY, + SOREG|SNAME, TPOINT|TLONG|TULONG, + NBREG, RESC1, + " lda A1,AL\n and ZM,$32512\n", }, + +/* + * UMINUS - unary negation. + */ + +{ UMINUS, INAREG|FOREFF, + SAREG, TWORD, + SANY, TANY, + 0, RLEFT, + " neg AL\n", }, + +/* negate a char: neg its containing word (a word op, any register); + * the low byte is the two's-complement byte result */ +{ UMINUS, INDREG|FOREFF, + SDREG, TCHAR|TUCHAR, + SANY, TANY, + 0, RLEFT, + " neg ZG\n", }, + +{ UMINUS, INBREG|FOREFF, + SBREG, TLONG|TULONG, + SANY, TANY, + 0, RLEFT, + "ZN", }, + +/* fp negation is an inline sign-bit flip on the high word (native: + * "xor r0,$-32768" in modf.s), not a runtime call */ +{ UMINUS, INCREG|FOREFF, + SCREG, TDBL, + SANY, TANY, + 0, RLEFT, + " xor ZW,$-32768\n", }, + +{ UMINUS, INBREG|FOREFF, + SBREG, TFLT, + SANY, TANY, + 0, RLEFT, + " xor ZW,$-32768\n", }, + +/* + * COMPL - bitwise complement. + */ + +{ COMPL, INAREG, + SAREG, TWORD, + SANY, TANY, + 0, RLEFT, + " com AL\n", }, + +{ COMPL, INBREG, + SBREG, TLONG|TULONG, + SANY, TANY, + 0, RLEFT, + "ZC", }, + +/* + * FUNARG - push argument onto stack before a call. + * + * Z8001 ABI: pure stack, right-to-left, caller cleans. + * Stack pointer is rr14 (pair), push uses: + * push (rr14), rN (word) + * pushl (rr14), rrN (long/ptr) + * + * "AL" in template expands to the argument source operand. + */ + +/* push word register */ +{ FUNARG, FOREFF, + SAREG, TWORD|TSHORT|TUSHORT, + SANY, TWORD, + 0, RNULL, + " push (rr14),AL\n", }, + +/* push src is R/IM/IR/DA/X - no BA (same encodable set as pushl below, + * as machine.c S_PUSH). Constants, globals and frame words push + * directly like native (`push (rr14), $1` / `, errno_` / `, L5+4(r13)`); + * only BA OREGs (pair base + displacement) need the register detour. + * SNBA is a special shape so it cannot be OR'd into one rule with + * SCON/SNAME (tshape switches on the whole shape value). */ +{ FUNARG, FOREFF, + SCON, TWORD|TSHORT|TUSHORT, + SANY, TWORD, + 0, RNULL, + " push (rr14),AL\n", }, + +{ FUNARG, FOREFF, + SNAME, TWORD|TSHORT|TUSHORT, + SANY, TWORD, + 0, RNULL, + " push (rr14),AL\n", }, + +{ FUNARG, FOREFF, + SNBA, TWORD|TSHORT|TUSHORT, + SANY, TWORD, + 0, RNULL, + " push (rr14),AL\n", }, + +/* push word from BA memory */ +{ FUNARG, FOREFF, + SOREG, TWORD|TSHORT|TUSHORT, + SANY, TWORD, + NAREG, RNULL, + " ld A1,AL\n push (rr14),A1\n", }, + +/* push long/ptr/float pair register */ +{ FUNARG, FOREFF, + SBREG, TLONG|TULONG|TPOINT|TFLT, + SANY, TLONG|TULONG|TPOINT|TFLT, + 0, RNULL, + " pushl (rr14),AL\n", }, + +/* pushl globals and frame longs directly (DA/X/IR; native uses all + * three). Long constants keep the ldl detour: native never emits + * pushl-immediate, and symbolic address constants would need the + * assembler's long-form segmented-immediate path. */ +{ FUNARG, FOREFF, + SNAME, TLONG|TULONG|TPOINT|TFLT, + SANY, TLONG|TULONG|TPOINT|TFLT, + 0, RNULL, + " pushl (rr14),AL\n", }, + +{ FUNARG, FOREFF, + SNBA, TLONG|TULONG|TPOINT|TFLT, + SANY, TLONG|TULONG|TPOINT|TFLT, + 0, RNULL, + " pushl (rr14),AL\n", }, + +/* push long/ptr/float from a constant or BA memory */ +{ FUNARG, FOREFF, + SCON|SOREG, TLONG|TULONG|TPOINT|TFLT, + SANY, TLONG|TULONG|TPOINT|TFLT, + NBREG, RNULL, + " ldl A1,AL\n pushl (rr14),A1\n", }, + +/* push a double (8 bytes): low pair first so the sign+exponent word + * lands at the lower address (native: "pushl rr2; pushl rr0"). Name + * and frame memory push directly (pushl src is R/IM/IR/DA/X - no BA, + * so pair-based OREGs go through a quad register instead). */ +{ FUNARG, FOREFF, + SCREG, TDBL, + SANY, TDBL, + 0, RNULL, + "ZP", }, + +/* NB: SFRAME is a special shape - OR'ing it with SNAME makes a value + * tshape/special() match nothing (the old combined rule was dead and + * doubles always took the SCREG path); they must be separate rules. */ +{ FUNARG, FOREFF, + SNAME, TDBL, + SANY, TDBL, + 0, RNULL, + "ZP", }, + +{ FUNARG, FOREFF, + SFRAME, TDBL, + SANY, TDBL, + 0, RNULL, + "ZP", }, + +/* push char: push its containing word (the value is in the low byte of + * the word-sized argument slot; the high byte is don't-care, exactly as + * with the old word-register convention) */ +{ FUNARG, FOREFF, + SDREG, TCHAR|TUCHAR, + SANY, TCHAR|TUCHAR, + 0, RNULL, + " push (rr14),ZG\n", }, + +/* push zero word */ +{ FUNARG, FOREFF, + SZERO, TANY, + SANY, TANY, + 0, RNULL, + " push (rr14),$0\n", }, + +/* struct argument (by-value struct copy onto stack) via ldirb. + * A1 = scratch dest-address pair, A2 = byte count; n_left = source pair. + * NOLEFT(RR0): the source becomes an ldirb indirect base (see STASG). */ +{ STARG, FOREFF, + SBREG, TPTRTO|TANY, + SANY, TSTRUCT, + NEEDS(NREG(A, 1), NREG(B, 1), NOLEFT(RR0)), 0, + "ZT", }, + +/* + * Catch-all rewrite rules: delegate complex patterns back to the + * generic rewriter, which will break them into simpler pieces. + */ +#define DF(x) FORREW, SANY, TANY, SANY, TANY, NEEDS(NREWRITE), x, "" + +{ UMUL, DF(UMUL), }, +{ ASSIGN, DF(ASSIGN), }, +{ STASG, DF(STASG), }, +{ FLD, DF(FLD), }, +{ OPLEAF, DF(NAME), }, +{ OPUNARY, DF(UMINUS), }, +{ OPANY, DF(BITYPE), }, + +{ FREE, FREE, FREE, FREE, FREE, FREE, 0, FREE, "help; I'm in trouble\n" }, +}; + +int tablesize = sizeof(table)/sizeof(table[0]); diff --git a/cc/cc/cc.1 b/cc/cc/cc.1 index 9e69c0b26..6acdba5c1 100644 --- a/cc/cc/cc.1 +++ b/cc/cc/cc.1 @@ -162,6 +162,9 @@ Output is sent to standard output unless the option is used. .It Fl ffreestanding Assume a freestanding environment. +.It Fl ftraditional +Accept some pre-ANSI (K&R) constructs that are otherwise errors, +diagnosing them with a warning instead. .It Fl fPIC Generate PIC code. .\" TODO: document about avoiding machine-specific maximum size? diff --git a/cc/cc/cc.c b/cc/cc/cc.c index 9b2c0cd43..fca371735 100644 --- a/cc/cc/cc.c +++ b/cc/cc/cc.c @@ -299,6 +299,7 @@ char *win32commandline(struct strlist *l); #endif int sspflag; int freestanding; +int traditional; int Sflag; int cflag; int gflag; @@ -627,6 +628,8 @@ main(int argc, char *argv[]) kflag = j ? 0 : *u == 'P' ? F_PIC : F_pic; } else if (match(u, "freestanding")) { freestanding = j ? 0 : 1; + } else if (match(u, "traditional")) { + traditional = j ? 0 : 1; } else if (match(u, "signed-char")) { xuchar = j ? 1 : 0; } else if (match(u, "unsigned-char")) { @@ -1029,6 +1032,23 @@ main(int argc, char *argv[]) signal(SIGTERM, idexit); /* after arg parsing */ +#ifdef _WIN32 + /* also search the directory the driver itself lives in, so the + * other tools and the target libraries can be kept in one folder; + * -B directories were added during arg parsing and take precedence */ + { + char exepath[MAX_PATH]; + char *p; + + if (GetModuleFileName(NULL, exepath, sizeof(exepath)) > 0 && + (p = strrchr(exepath, '\\')) != NULL) { + *p = 0; + strlist_append(&progdirs, exepath); + strlist_append(&crtdirs, exepath); + strlist_append(&libdirs, exepath); + } + } +#endif strlist_append(&progdirs, LIBEXECDIR); if (pcclibdir) strlist_append(&crtdirs, pcclibdir); @@ -1258,6 +1278,17 @@ find_file(const char *file, struct strlist *path, int mode) memcpy(f + lp + need_sep, file, lf + 1); if (access(f, mode) == 0) return f; +#ifdef _WIN32 + /* Windows executables only match with the .exe suffix */ + { + char *fx = cat(f, ".exe"); + if (access(fx, mode) == 0) { + free(f); + return fx; + } + free(fx); + } +#endif free(f); } return xstrdup(file); @@ -1955,6 +1986,7 @@ struct flgcheck ccomflgcheck[] = { { &Oflag, 1, "-xdce" }, { &Oflag, 1, "-xssa" }, { &freestanding, 1, "-ffreestanding" }, + { &traditional, 1, "-ftraditional" }, { &pgflag, 1, "-p" }, { &gflag, 1, "-g" }, { &xgnu89, 1, "-xgnu89" }, @@ -2053,7 +2085,7 @@ struct flgcheck ldflgcheck[] = { #else { &Bstatic, 1, "-Bstatic" }, #endif -#if !defined(os_darwin) && !defined(os_sunos) +#if !defined(os_darwin) && !defined(os_sunos) && !defined(os_coherent) { &gflag, 1, "-g" }, #endif { &pthreads, 1, "-lpthread" }, @@ -2090,7 +2122,9 @@ setup_ld_flags(void) strlist_append(&early_linker_flags, dynlinkarg); strlist_append(&early_linker_flags, dynlinklib); } -#ifndef os_darwin + /* Coherent ld enters at the first object (crts0.o); its "start" + * label is not global, so -e cannot name it */ +#if !defined(os_darwin) && !defined(os_coherent) strlist_append(&early_linker_flags, "-e"); strlist_append(&early_linker_flags, STARTLABEL); #endif @@ -2107,12 +2141,15 @@ setup_ld_flags(void) strlist_append(&early_linker_flags, cat("--sysroot=", sysroot)); if (!nostdlib) { /* library search paths */ + /* Coherent ld has no search-path option: -L means "large model" */ +#ifndef os_coherent if (pcclibdir) strlist_append(&late_linker_flags, cat("-L", pcclibdir)); for (i = 0; deflibdirs[i]; i++) strlist_append(&late_linker_flags, cat("-L", deflibdirs[i])); +#endif /* standard libraries */ if (pgflag) { for (i = 0; defproflibs[i]; i++) @@ -2123,8 +2160,16 @@ setup_ld_flags(void) strlist_append(&late_linker_flags, defcxxlibs[i]); } else { - for (i = 0; deflibs[i]; i++) - strlist_append(&late_linker_flags, deflibs[i]); + for (i = 0; deflibs[i]; i++) { + if (deflibs[i][0] == '-') + strlist_append(&late_linker_flags, + deflibs[i]); + else + /* bare library file: resolve it here, + * for linkers without -l/-L */ + strap(&late_linker_flags, &libdirs, + deflibs[i], 'a'); + } } } if (!nostartfiles) { diff --git a/cc/ccom/ccom.1 b/cc/ccom/ccom.1 index 863c13dd3..d35d8b9e8 100644 --- a/cc/ccom/ccom.1 +++ b/cc/ccom/ccom.1 @@ -70,6 +70,17 @@ If no value is given, the default is 1. .It Sy freestanding Emit code for a freestanding environment. Currently not implemented. +.It Sy traditional +Accept some pre-ANSI (K&R) constructs that are otherwise errors: +external declarations with no type specifier (defaulting to +.Sy int ) , +a missing +.Sy return +value in a non-void function, a scalar initializer for an array +(initializing its first element), and mismatched pointer types in a +.Sy ?: +expression. +Each is diagnosed with a warning instead of an error. .El .It Fl g Include debugging information in the output code for use by diff --git a/cc/ccom/cgram.y b/cc/ccom/cgram.y index d297facaa..f896525d2 100644 --- a/cc/ccom/cgram.y +++ b/cc/ccom/cgram.y @@ -288,7 +288,7 @@ struct savbc { %type gen_ass_list gen_assoc %type string svstr moe %type str_head -%type xnfdeclarator enum_head +%type xnfdeclarator enum_head notype_xnfdcl %type begbr clbrace %% @@ -299,11 +299,59 @@ ext_def_list: ext_def_list external_def external_def: funtype kr_args compoundstmt { fend(); } | declaration { blevel = 0; symclear(0); } + | notype_init_dcl_list ';' { blevel = 0; symclear(0); } | asmstatement ';' | ';' | error { blevel = 0; } ; +/* + * K&R-style external declaration with no type specifier at all: + * unixbug = 0; extlist(), extfn(); + * The type defaults to int (as gcc does, with a warning). No conflict + * with funtype: its reduction is chosen on lookahead '{' or a + * declaration specifier (K&R argument declarations), while these + * productions reduce on '=', ',' or ';'. + */ +notype_init_dcl_list: + notype_init_dcl { symclear(blevel); } + | notype_init_dcl_list ',' notype_init_dcl { symclear(blevel); } + ; + +notype_init_dcl: declarator attr_var { + P1ND *tn = mkty(INT, 0, 0); + if (traditional) + werror("type defaults to int in declaration"); + else + uerror("type specifier missing"); + init_declarator(tn, $1, 0, $2, 0); + p1tfree(tn); + } + | notype_xnfdcl '=' e { + if ($1->sclass == STATIC || $1->sclass == EXTDEF) + statinit++; + simpleinit($1, eve($3)); + if ($1->sclass == STATIC || $1->sclass == EXTDEF) + statinit--; + xnf = NULL; + } + | notype_xnfdcl '=' begbr init_list optcomma '}' { + endinit($3, 0); + xnf = NULL; + } + ; + +notype_xnfdcl: declarator attr_var { + P1ND *tn = mkty(INT, 0, 0); + if (traditional) + werror("type defaults to int in declaration"); + else + uerror("type specifier missing"); + $$ = xnf = init_declarator(tn, $1, 1, $2, 0); + p1tfree(tn); + } + ; + funtype: /* no type given */ declarator { fundef(mkty(INT, 0, 0), $1); cftnsp->sflags |= NORETYP; @@ -940,10 +988,16 @@ statement: e ';' { ecomp(eve($1)); symclear(blevel); } } | C_RETURN ';' { branch(retlab); - if (cftnsp->stype != VOID && + if (cftnsp->stype != VOID && (cftnsp->sflags & NORETYP) == 0 && - cftnsp->stype != VOID+FTN) - uerror("return value required"); + cftnsp->stype != VOID+FTN) { + /* legal in C89 (constraint is C99); + * common in K&R code */ + if (traditional) + werror("return value required"); + else + uerror("return value required"); + } rch: if (!reached) warner(Wunreachable_code); @@ -1234,7 +1288,7 @@ term: term C_INCOP { $$ = biop($2, $1, bcon(1)); } $$ = $5; } $$ = biop(ADDROF, $$, NULL); - $3 = block(NAME, NULL, NULL, ENUNSIGN(INTPTR), 0, 0); + $3 = block(NAME, NULL, NULL, ENUNSIGN(SIZET), 0, 0); $$ = biop(CAST, $3, $$); } | C_ICON { $$ = bdty(ICON, &($1)); } @@ -1925,6 +1979,7 @@ olddecl(P1ND *p, P1ND *a) s->stype = p->n_type; s->sdf = p->n_df; + s->sss = p->pss; s->sap = p->n_ap; if (a) attr_add(s->sap, gcc_attr_wrapper(a)); diff --git a/cc/ccom/init.c b/cc/ccom/init.c index a08d6f10c..e4436bafe 100644 --- a/cc/ccom/init.c +++ b/cc/ccom/init.c @@ -1225,6 +1225,18 @@ simpleinit(struct symtab *sp, NODE *p) return; } + /* K&R laxness: a scalar initializer for an array initializes + * its first element, as if braced ("char tapedev[10] = '\0';"). + * Falling through would build ASSIGN(array, scalar) and give + * "lvalue required". */ + if (traditional && ISARY(sp->stype) && !ISARY(p->n_type)) { + werror("array initialized with scalar; braces assumed"); + ctx = beginit(sp); + scalinit(ctx, p, NULL); + endinit(ctx, 0); + return; + } + nt = nametree(sp); switch (sp->sclass) { case STATIC: diff --git a/cc/ccom/main.c b/cc/ccom/main.c index 34e833b25..be9f3c86e 100644 --- a/cc/ccom/main.c +++ b/cc/ccom/main.c @@ -48,6 +48,7 @@ int sspflag; int xscp, xssa, xtailcall, xtemps, xdeljumps, xdce, xinline, xccp, xgnu89, xgnu99; int xuchar; int freestanding; +int traditional; char *prgname, *ftitle; static void prtstats(void); @@ -121,6 +122,8 @@ fflags(char *str) pragma_allpacked = (strlen(str) > 12 ? atoi(str+12) : 1); else if (strcmp(str, "freestanding") == 0) freestanding = flagval; + else if (strcmp(str, "traditional") == 0) + traditional = flagval; else { fprintf(stderr, "unknown -f option '%s'\n", str); usage(); diff --git a/cc/ccom/optim.c b/cc/ccom/optim.c index 13c8fc5a8..d7f07bd1d 100644 --- a/cc/ccom/optim.c +++ b/cc/ccom/optim.c @@ -368,7 +368,7 @@ again: o = p->n_op; /* remove ops with RHS 0 */ if ((o == PLUS || o == MINUS || o == OR || o == ER) && - nncon(p->n_right) && RV(p) == 0) { + nncon(p->n_right) && RV(p) == 0 && !OPTIM_KEEPZERO(p)) { goto zapright; } break; diff --git a/cc/ccom/params.c b/cc/ccom/params.c index 9dbebfe7c..9754374b5 100644 --- a/cc/ccom/params.c +++ b/cc/ccom/params.c @@ -344,7 +344,15 @@ static void pr_wr(int w) { if (pdebug > 2) printf("pr_wr: %#x\n", w); - if (putw(w, pr_file)) + /* + * putw() return value is not portable: POSIX returns 0 on + * success, but MinGW/Windows _putw returns the value written, + * so checking the return value spuriously fails for any + * nonzero word. Check the stream error flag instead, like + * pr_rd() below. + */ + putw(w, pr_file); + if (ferror(pr_file)) pr_err("pr_wr"); } diff --git a/cc/ccom/pass1.h b/cc/ccom/pass1.h index a80cd024c..52221c0c4 100644 --- a/cc/ccom/pass1.h +++ b/cc/ccom/pass1.h @@ -702,6 +702,42 @@ void dwarf_end(void); #error int size unknown #endif +/* + * Type of a sizeof/offsetof result and of a pointer difference. + * Both default to the pointer-sized integer, but a target may override + * them in macdefs.h: ABIs built around a K&R libc pass sizes and counts + * as plain (16-bit) ints even where pointers are wider, so sizeof + * results fed to unprototyped functions must not widen. + */ +#ifndef SIZET +#define SIZET INTPTR +#endif +#ifndef PTRDIFFT +#define PTRDIFFT INTPTR +#endif + +/* + * optim() folds x+0 and x-0 to plain x. A target where a bare frame + * register is not a valid pointer value (segmented addressing needs an + * explicit address computation to supply the segment) can override this + * to keep the op when the left side is such a register. + */ +#ifndef OPTIM_KEEPZERO +#define OPTIM_KEEPZERO(p) 0 +#endif + +/* + * rmcops() rewrites relationals used for their value (f = (a < b)) into + * a branch diamond storing 1/0 to a temp. A target whose instruction + * set can materialize a truth value directly (Z8000 tcc, ...) overrides + * this to keep the bare relop in the tree; pass2 then needs table rules + * matching OPLOG in value (register) context. ANDAND/OROR/NOT are + * still rewritten regardless: they require lazy evaluation. + */ +#ifndef KEEPLOGOPVALUE +#define KEEPLOGOPVALUE(p) 0 +#endif + /* Generate a bitmask from a given type size */ #define SZMASK(y) ((((1LL << ((y)-1))-1) << 1) | 1) diff --git a/cc/ccom/trees.c b/cc/ccom/trees.c index 0797d419d..762a3ba42 100644 --- a/cc/ccom/trees.c +++ b/cc/ccom/trees.c @@ -1166,6 +1166,10 @@ chkpun(P1ND *p) if (ISARY(t2)) ++d2; } else if (ISFTN(t1)) { + if (!ISFTN(t2)) + break; /* t2 has no df: (char *)0 + * assigned to a function + * pointer crashed here */ if (pr_ckproto(d1->dlst, d2->dlst, 0)) { werror("illegal function " "pointer combination"); @@ -1460,6 +1464,13 @@ oconvert(register P1ND *p) case MINUS: p->n_type = INTPTR; p = (clocal(VBLOCK(p, bpsize(p->n_left), INT, 0, 0))); + /* + * The subtraction and the element-size division stay + * pointer-sized; only the result narrows to the target's + * pointer-difference type. + */ + if (PTRDIFFT != INTPTR) + p = clocal(makety(p, mkqtyp(PTRDIFFT))); return( p ); } @@ -1528,6 +1539,13 @@ ptmatch(P1ND *p) if (BTYPE(td2->type) == VOID) break; } + if (traditional && ISPTR(td1->type) && ISPTR(td2->type)) { + /* mismatched pointers: K&R code does + * e.g. "m ? (char *)0 : fp"; warn and + * use the left type */ + werror("illegal pointer combination"); + break; + } uerror("illegal types in :"); } break; @@ -1980,8 +1998,8 @@ doszof(P1ND *p) df++; ty = DECREF(ty); } - rv = buildtree(MUL, rv, - xbcon(tsize(ty, p->n_df, p->pss)/SZCHAR, NULL, INTPTR)); + rv = buildtree(MUL, rv, + xbcon(tsize(ty, p->n_df, p->pss)/SZCHAR, NULL, SIZET)); p1tfree(p); return rv; } @@ -2365,6 +2383,20 @@ rmcops(P1ND *p) case LT: case GE: case GT: + /* + * A relational used for its value. Targets that can + * materialize a truth value directly keep the bare relop + * in the tree (pass2 matches it with a value-context + * OPLOG rule); everyone else gets the branch diamond + * below. The operands are ordinary expressions - just + * clean them up recursively. + */ + if (KEEPLOGOPVALUE(p)) { + rmcops(p->n_left); + rmcops(p->n_right); + break; + } + /* FALLTHROUGH */ case ANDAND: case OROR: case NOT: diff --git a/cc/cxxcom/builtins.c b/cc/cxxcom/builtins.c index 4a71ebf40..f90117b90 100644 --- a/cc/cxxcom/builtins.c +++ b/cc/cxxcom/builtins.c @@ -594,10 +594,17 @@ builtin_nanx(const struct bitable *bt, NODE *a) uerror("%s bad argument", bt->name); a = bcon(0); } else if (a->n_op == STRING && *a->n_name == '\0') { + /* nLDOUBLE holds only the meaningful representation bytes, + * which may be fewer than sizeof(long double) (e.g. a + * 10-byte x87 pattern in a 16-byte long double): bound the + * copy like VALX does and zero-fill the rest. */ + long double d = 0.0; + a->n_op = FCON; a->n_type = bt->rt; a->n_dcon = fltallo(); - memcpy(&FCAST(a->n_dcon)->fp, nLDOUBLE, sizeof(long double)); + memcpy(&d, nLDOUBLE, MIN(sizeof(nLDOUBLE), sizeof(d))); + FCAST(a->n_dcon)->fp = d; } else a = binhelp(eve(a), bt->rt, &bt->name[10]); return a; diff --git a/config.sub b/config.sub index e047ac41e..7544dfdf7 100644 --- a/config.sub +++ b/config.sub @@ -317,7 +317,7 @@ case $basic_machine in | visium \ | we32k \ | x86 | xc16x | xstormy16 | xtensa \ - | z8k | z80) + | z8k | z8001 | z80) basic_machine=$basic_machine-unknown ;; c54x) @@ -449,7 +449,7 @@ case $basic_machine in | x86-* | x86_64-* | xc16x-* | xps100-* \ | xstormy16-* | xtensa*-* \ | ymp-* \ - | z8k-* | z80-*) + | z8k-* | z8001-* | z80-*) ;; # Recognize the basic CPU types without company name, with glob match. xtensa*) @@ -1379,7 +1379,7 @@ case $os in | -hpux* | -unos* | -osf* | -luna* | -dgux* | -auroraux* | -solaris* \ | -sym* | -kopensolaris* | -plan9* \ | -amigaos* | -amigados* | -msdos* | -newsos* | -unicos* | -aof* \ - | -aos* | -aros* | -cloudabi* | -sortix* \ + | -aos* | -aros* | -cloudabi* | -sortix* | -coherent* \ | -nindy* | -vxsim* | -vxworks* | -ebmon* | -hms* | -mvs* \ | -clix* | -riscos* | -uniplus* | -iris* | -rtu* | -xenix* \ | -hiux* | -386bsd* | -knetbsd* | -mirbsd* | -netbsd* \ diff --git a/configure b/configure index 340c774d6..1da311d82 100755 --- a/configure +++ b/configure @@ -3142,6 +3142,13 @@ case "$target_os" in esac ;; + coherent*) + targos=coherent + case "$target_cpu" in + z8001*) targmach=z8001 endian=big ;; + esac + ;; + sysv4*) targos=sysv4 abi=elf diff --git a/configure.ac b/configure.ac index 3e647fc8a..70c514718 100644 --- a/configure.ac +++ b/configure.ac @@ -253,6 +253,13 @@ case "$target_os" in esac ;; + coherent*) + targos=coherent + case "$target_cpu" in + z8001*) targmach=z8001 endian=big ;; + esac + ;; + sysv4*) targos=sysv4 abi=elf diff --git a/mip/common.c b/mip/common.c index 6a911688b..551102e59 100644 --- a/mip/common.c +++ b/mip/common.c @@ -568,6 +568,8 @@ struct dopest { { USTCALL, "USTCALL", UTYPE|CALLFLG, }, { STCLR, "STCLR", BITYPE, }, { ADDROF, "U&", UTYPE, }, + { SWDISP, "SWDISP", UTYPE, }, + { BCLR, "BCLR", UTYPE, }, { -1, "", 0 }, }; diff --git a/mip/manifest.h b/mip/manifest.h index ee27a4018..4378363fd 100644 --- a/mip/manifest.h +++ b/mip/manifest.h @@ -174,6 +174,7 @@ */ extern int gflag, kflag, pflag; extern int sspflag; +extern int traditional; extern int xscp, xssa, xtailcall, xtemps, xdeljumps, xdce; extern int xuchar; diff --git a/mip/match.c b/mip/match.c index 77a1cd341..377e116ae 100644 --- a/mip/match.c +++ b/mip/match.c @@ -679,8 +679,13 @@ findops(NODE *p, int cookie) F2WALK(r); /* Help register assignment after SSA by preferring */ - /* 2-op insns instead of 3-ops */ + /* 2-op insns instead of 3-ops. Not for SPECIAL shapes: + * their low bits are a shape number, not register-class + * bits, and demoting a direct special match forces the + * operand into a register class picked from those bits + * (e.g. SPECIAL|8 reads as INCREG). */ if (xssa && (q->rewrite & RLEFT) == 0 && + (n2osh(q->lshape) & SPECIAL) == 0 && (n2osh(q->lshape) & (INREGS)) && shl == SRDIR) shl = SRREG; @@ -715,8 +720,12 @@ findops(NODE *p, int cookie) qq->rewrite & RRIGHT, gor); if (sh == -1) { - if (cookie == FOREFF || cookie == FORCC) - sh = 0; + if (cookie == FOREFF || + (cookie & qq->visit & INREGS) == 0) + sh = 0; /* no result register: FOREFF, or a rule + * visited only for its condition codes + * (ffs()-1 on an empty mask would collide + * with FFAIL) */ else sh = ffs(cookie & qq->visit & INREGS)-1; } @@ -745,7 +754,7 @@ findops(NODE *p, int cookie) * REG REG 4 # put both in reg */ int -relops(NODE *p) +relops(NODE *p, int cookie) { extern int *qtable[]; struct optab *q; @@ -767,6 +776,14 @@ relops(NODE *p) if (!acceptable(q)) /* target-dependent filter */ continue; + /* Historically every relop rule was FORCC-only and the + * context was implied. A target keeping relops in value + * context adds rules that materialize the truth value in + * a register; those must not be picked under a branch, + * nor a flags-only rule where a result is wanted. */ + if (cookie != FOREFF && (q->visit & cookie) == 0) + continue; + if (ttype(l->n_type, q->ltype) == 0 || ttype(r->n_type, q->rtype) == 0) continue; /* Types must be correct */ @@ -811,11 +828,19 @@ relops(NODE *p) sh = ffs(n2osh(q->lshape) & INREGS)-1; else if (q->rewrite & RRIGHT) sh = ffs(n2osh(q->rshape) & INREGS)-1; + else if (cookie != FORCC && (cookie & q->visit & INREGS) != 0) + /* value-context relop delivering its 0/1 result in a + * scratch register (RESC1-style rule): the class comes + * from the rule's visit mask, exactly as in findops */ + sh = ffs(cookie & q->visit & INREGS)-1; F2DEBUG(("relops: node %p\n", p)); p->n_su = MKIDX(idx, 0); SCLASS(p->n_su, sh); - return 0; + /* like findops: the return value is the result class - shswitch + * feeds it to a rewriting parent (a value-context relop under + * RLEFT would otherwise leave the parent classless, n_reg -1) */ + return sh; } /* diff --git a/mip/node.h b/mip/node.h index 8faed4593..06d634baa 100644 --- a/mip/node.h +++ b/mip/node.h @@ -222,7 +222,9 @@ typedef struct node { #define STCLR 56 #define FUNARG 57 #define ADDROF 58 +#define SWDISP 59 /* sparse-switch cpir dispatch (z8001) */ +#define BCLR 60 /* block memory clear (z8001 overlapping ldirb) */ -#define MAXOP 58 +#define MAXOP 60 #endif diff --git a/mip/optim2.c b/mip/optim2.c index 5cfdd1d6f..8f3459b8f 100644 --- a/mip/optim2.c +++ b/mip/optim2.c @@ -55,6 +55,15 @@ static int dfsnum; +/* + * Enables tail merging (comjump/xjump) inside deljumps. Only set for + * the deljumps runs that happen before register allocation: after + * ngenregs the trees carry register assignments (n_reg/n_su), and two + * structurally equal trees may still use different scratch registers, + * so merging them would be wrong there. + */ +static int tailmerge; + void saveip(struct interpass *ip); void deljumps(struct p2env *); void optdump(struct interpass *ip); @@ -114,8 +123,11 @@ optimize(struct p2env *p2e) printip(ipole); } - if (xdeljumps) + if (xdeljumps) { + tailmerge = 1; deljumps(p2e); /* Delete redundant jumps and dead code */ + tailmerge = 0; + } if (xssa) add_labels(p2e) ; @@ -203,8 +215,11 @@ optimize(struct p2env *p2e) #endif /* Now, clean up the gotos we do not need any longer */ - if (xdeljumps) - deljumps(p2e); /* Delete redundant jumps and dead code */ + if (xdeljumps) { + tailmerge = 1; + deljumps(p2e); /* Delete jumps and dead code */ + tailmerge = 0; + } bblocks_build(p2e); BDEBUG(("Calling cfg_build\n")); @@ -561,10 +576,196 @@ codemove(struct dlnod *p) #endif } +/* + * Strict statement tree equality for tail merging. Unlike treecmp() + * in match.c (which deliberately matches loosely for FINDMOPS), a + * false match here becomes wrong code, so n_type must agree on every + * node and any op not explicitly handled compares unequal. Ops that + * carry state outside the common node fields (struct size/alignment, + * bitfield descriptors packed in n_rval, xasm constraint strings) + * never merge. + */ +static int +dltcmp(NODE *p1, NODE *p2) +{ + if (p1->n_op != p2->n_op || p1->n_type != p2->n_type) + return 0; + + switch (p1->n_op) { + case NAME: + case ICON: + if (getlval(p1) != getlval(p2) || + strcmp(p1->n_name, p2->n_name) != 0) + return 0; + return 1; + + case OREG: + if (getlval(p1) != getlval(p2) || p1->n_rval != p2->n_rval || + strcmp(p1->n_name, p2->n_name) != 0) + return 0; + return 1; + + case REG: + case TEMP: + return p1->n_rval == p2->n_rval; + + case STASG: + case STARG: + case STCALL: + case USTCALL: + case STCLR: + case FLD: + case XASM: + case XARG: + return 0; + } + + switch (optype(p1->n_op)) { + case BITYPE: + if (dltcmp(p1->n_right, p2->n_right) == 0) + return 0; + /* FALLTHROUGH */ + case UTYPE: + return dltcmp(p1->n_left, p2->n_left); + default: + return 0; /* unknown leaf, play safe */ + } +} + +/* + * Two dlnods are mergeable if both are plain statement trees + * (IP_ASM also hides behind STMT op) that compare equal. + */ +static int +equop(struct dlnod *p1, struct dlnod *p2) +{ + if (p1 == 0 || p2 == 0 || p1->op != STMT || p2->op != STMT) + return 0; + if (p1->dlip->type != IP_NODE || p2->dlip->type != IP_NODE) + return 0; + return dltcmp(p1->dlip->ip_node, p2->dlip->ip_node); +} + +/* + * Insert a new label directly before the statement p, in both the + * dlnod list and the interpass list. The interpass is tmpalloc'd + * and must survive this pass, which is why deljumps skips its + * markset/markfree when tail merging is enabled. + */ +static struct dlnod * +insertl(struct dlnod *p) +{ + struct interpass *ip; + struct dlnod *lp; + + ip = tmpalloc(sizeof(struct interpass)); + ip->type = IP_DEFLAB; + ip->lineno = p->dlip->lineno; + ip->ip_lbl = getlab2(); + + lp = tmpalloc(sizeof(struct dlnod)); + lp->op = LABEL; + lp->labno = ip->ip_lbl; + lp->dlip = ip; + lp->ref = 0; + lp->refc = 0; + + lp->back = p->back; + lp->forw = p; + p->back->forw = lp; + p->back = lp; + + DLIST_INSERT_BEFORE(p->dlip, ip, qelem); + return lp; +} + +/* + * Cross-jumping: when the statement ahead of a jump is identical to + * the statement ahead of the jump's target label, put a new label in + * front of the target's copy and replace our copy with a jump there; + * repeat until the tails differ. Labels between the merged statement + * and the jump (or the target) are unaffected: paths entering there + * never executed the merged statement. + */ +static void +xjump(struct dlnod *p) +{ + struct dlnod *p1, *p2, *p3; + + if ((p2 = p->ref) == 0) + return; + p1 = p; + for (;;) { + while ((p1 = p1->back) && p1->op == LABEL) + ; + while ((p2 = p2->back) && p2->op == LABEL) + ; + if (p1 == p2 || equop(p1, p2) == 0) + return; + p3 = insertl(p2); + tfree(p1->dlip->ip_node); + p1->dlip->ip_node = mkunode(GOTO, + mklnode(ICON, p3->labno, 0, INT), 0, INT); + p1->op = JBR; + p1->labno = p3->labno; + p1->ref = p3; + p3->refc++; + nchange++; + } +} + +/* + * Merge the identical tails of two jumps to the same label: insert + * a label ahead of the earlier jump's copy of the tail and let the + * later jump enter the shared tail there instead, one statement at + * a time. Deletions happen only on the later side; a label found + * there means another path joins in, which ends the merge. + */ +static void +backjmp(struct dlnod *p1, struct dlnod *p2) +{ + struct dlnod *p3; + + for (;;) { + while (p1->back && p1->back->op == LABEL) + p1 = p1->back; + p1 = p1->back; + p2 = p2->back; + if (p1 == p2 || equop(p1, p2) == 0) + return; + p3 = insertl(p1); + iprem(p2); /* frees tree, unlinks interpass */ + p2->back->forw = p2->forw; + p2->forw->back = p2->back; + p2 = p2->forw; /* the jump that lost its tail */ + decref(p2->ref); + setlab(p2, p3->labno); + p2->ref = p3; + p3->refc++; + nchange++; + } +} + +/* + * Find pairs of jumps to the same (shared) label and merge their + * identical tails. + */ +static void +comjump(struct dlnod *dl) +{ + struct dlnod *p1, *p2, *p3; + + for (p1 = dl->forw; p1 != 0; p1 = p1->forw) + if (p1->op == JBR && (p2 = p1->ref) != 0 && p2->refc > 1) + for (p3 = p1->forw; p3 != 0; p3 = p3->forw) + if (p3->op == JBR && p3->ref == p2) + backjmp(p1, p3); +} + static void iterate(struct p2env *p2e, struct dlnod *dl) { - NODE *q, *r; + NODE *q; struct dlnod *p, *rp, *p1; int i; @@ -605,20 +806,27 @@ iterate(struct p2env *p2e, struct dlnod *dl) p1->forw->back = p; p->forw = p1->forw; + /* + * Invert the branch condition by negating + * the operator, keeping the operand order: + * the old c2-style LE/GT/ULE/UGT cases + * swapped the operands instead, which moved + * compare-against-constant operands to the + * left where no imm-compare insn matches. + */ q = p->dlip->ip_node->n_left; i = q->n_op; -#define SWAP(p) r = p->n_left, p->n_left = p->n_right, p->n_right = r switch (i) { case EQ: i = NE; break; case NE: i = EQ; break; - case LE: SWAP(q); i = LT; break; + case LE: i = GT; break; case LT: i = GE; break; case GE: i = LT; break; - case GT: SWAP(q); i = GE; break; - case ULE: SWAP(q); i = ULT; break; + case GT: i = LE; break; + case ULE: i = UGT; break; case ULT: i = UGE; break; case UGE: i = ULT; break; - case UGT: SWAP(q); i = UGE; break; + case UGT: i = ULE; break; default: comperr("deljumps: unexpected op"); } @@ -654,7 +862,8 @@ iterate(struct p2env *p2e, struct dlnod *dl) } } if (p->op == JBR) { - /* xjump(p); * needs tree rewrite; not yet */ + if (tailmerge) + xjump(p); p = codemove(p); } } @@ -667,7 +876,14 @@ deljumps(struct p2env *p2e) struct dlnod dln; MARK mark; - markset(&mark); + /* + * Tail merging creates label interpasses that must survive + * this pass, so the temp heap cannot be rolled back then. + * The dlnods are then freed with everything else when the + * function is done. + */ + if (tailmerge == 0) + markset(&mark); memset(&dln, 0, sizeof(dln)); listsetup(ipole, &dln); @@ -676,12 +892,12 @@ deljumps(struct p2env *p2e) do { iterate(p2e, &dln); } while (nchange); -#ifdef notyet - comjump(); -#endif + if (tailmerge) + comjump(&dln); } while (nchange); - markfree(&mark); + if (tailmerge == 0) + markfree(&mark); } void @@ -872,8 +1088,24 @@ cfg_build(struct p2env *p2e) SLIST_INSERT_LAST(&bb->child, cnode, chld); } else { int *l; - /* XXX assume all labels are valid as dest */ - for (l = p2e->epp->ip_labels; *l; l++) { + /* + * Computed goto. A switch-dispatch GOTO(SWDISP) + * carries its OWN null-terminated target list in + * the GOTO's n_name - edge only to those, so the + * case bodies of one switch are not falsely made + * predecessors-of/reachable-from every other + * computed goto in the function (that over- + * connection makes every case body a multi-pred + * merge, and SSA removephi then floods the single + * dispatch point with phi copies -> spills). A + * true "goto *p" (no SWDISP) still assumes any + * address-taken label in ip_labels. + */ + if (p->n_left->n_op == SWDISP) + l = (int *)p->n_name; + else + l = p2e->epp->ip_labels; + for (; *l; l++) { cnode->bblock = p2e->labinfo.arr[*l - p2e->labinfo.low]; SLIST_INSERT_LAST(&cnode->bblock->parents, pnode, cfgelem); SLIST_INSERT_LAST(&bb->child, cnode, chld); diff --git a/mip/pass2.h b/mip/pass2.h index d4fec3b6f..61e3e9668 100644 --- a/mip/pass2.h +++ b/mip/pass2.h @@ -221,6 +221,7 @@ void prologue(struct interpass_prolog *); void e2print(NODE *p, int down, int *a, int *b); void myoptim(struct interpass *); void cbgen(int op, int label); +int cbfuse(NODE *p); /* optional TARGET_CBRANCH_FUSE hook (backend) */ int match(NODE *p, int cookie); int acceptable(struct optab *); int special(NODE *, int); @@ -245,7 +246,7 @@ int findasg(NODE *p, int); int finduni(NODE *p, int); int findumul(NODE *p, int); int findleaf(NODE *p, int); -int relops(NODE *p); +int relops(NODE *p, int); #ifdef FINDMOPS int findmops(NODE *p, int); int treecmp(NODE *p1, NODE *p2); diff --git a/mip/reader.c b/mip/reader.c index 2bad7ff9f..b657858c8 100644 --- a/mip/reader.c +++ b/mip/reader.c @@ -277,6 +277,7 @@ isuseless(NODE *n) case USTCALL: case STASG: case STARG: + case BCLR: /* block memory clear - has a side effect */ return 0; default: return 1; @@ -776,6 +777,17 @@ pass2_compile(struct interpass *ip) emit(ip); } +#ifndef TARGET_CBRANCH_FUSE +/* + * A target may fuse an RESCC compare-and-branch into a single instruction + * (e.g. the Z8000 djnz for a decrement-and-test loop). The hook is called + * with the CBRANCH node before its compare and branch are generated; it + * returns non-zero once it has emitted the whole branch itself, otherwise + * the default two-instruction path (gencode + cbgen) runs. + */ +#define TARGET_CBRANCH_FUSE(p) 0 +#endif + void emit(struct interpass *ip) { @@ -808,8 +820,10 @@ emit(struct interpass *ip) } if (op->rewrite & RESCC) { o = p->n_left->n_op; - gencode(r, FORCC); - cbgen(o, getlval(p->n_right)); + if (!TARGET_CBRANCH_FUSE(p)) { + gencode(r, FORCC); + cbgen(o, getlval(p->n_right)); + } } else { gencode(r, FORCC); } @@ -902,6 +916,20 @@ prcook(int cookie) } #endif +#ifndef CCOKFORCOMP +/* + * May a compare against constant zero be elided by computing its child + * with FORCC, so that the child insn's own condition codes feed the + * branch? The child rule claiming FORCC guarantees correct flags only + * for the conditions its instruction actually sets: on machines where + * e.g. the logical ops leave the overflow flag untouched, an ordered + * (signed) branch after an elided compare would read a stale flag. + * Targets override this with the compare op and the child op to veto + * such pairs; the default keeps the historic behavior. + */ +#define CCOKFORCOMP(o, ch) 1 +#endif + int geninsn(NODE *p, int cookie) { @@ -931,17 +959,44 @@ again: switch (o = p->n_op) { case UGT: p1 = p->n_left; p2 = p->n_right; - if (p2->n_op == ICON && getlval(p2) == 0 && *p2->n_name == 0 && - (dope[p1->n_op] & (FLOFLG|DIVFLG|SIMPFLG|SHFFLG))) { + /* The compare-vs-zero elision only makes sense when the + * relop is evaluated for its condition codes: in value + * context (a target keeping value relops in the tree) + * the child's flags alone produce no register result. */ + if (cookie == FORCC && + p2->n_op == ICON && getlval(p2) == 0 && *p2->n_name == 0 && + (dope[p1->n_op] & (FLOFLG|DIVFLG|SIMPFLG|SHFFLG)) && + CCOKFORCOMP(o, p1->n_op)) { #ifdef mach_pdp11 /* XXX all targets? */ if ((rv = geninsn(p1, FORCC|QUIET)) != FFAIL) break; #else - if (findops(p1, FORCC) > 0) + /* 0 is a successful match on a rule with no result + * register (visit FORCC only, e.g. a bit test) */ + if (findops(p1, FORCC) >= 0) break; #endif } - rv = relops(p); +#ifdef FINDMOPS + /* Branch on a value just stored: + * CBRANCH(relop(ASSIGN(x, op(x, e)), 0)) + * (built by pass1 for "if ((x = expr))" or by a target's + * myreader fusing an assignment with the following + * compare). When a read-modify-write insn computes the + * stored value, its flags ARE the compare result - the + * same contract as the plain-op elision above, with + * CCOKFORCOMP keyed on the modifying op. */ + if (cookie == FORCC && + p2->n_op == ICON && getlval(p2) == 0 && *p2->n_name == 0 && + p1->n_op == ASSIGN && + optype(p1->n_right->n_op) == BITYPE && + (dope[p1->n_right->n_op] & (FLOFLG|DIVFLG|SIMPFLG|SHFFLG)) && + CCOKFORCOMP(o, p1->n_right->n_op)) { + if (findmops(p1, FORCC) != FFAIL) + break; + } +#endif + rv = relops(p, cookie); break; case PLUS: @@ -999,6 +1054,8 @@ again: switch (o = p->n_op) { case UCALL: case USTCALL: case ADDROF: + case SWDISP: + case BCLR: rv = finduni(p, cookie); break; @@ -1355,7 +1412,12 @@ gencode(NODE *p, int cookie) expand(p, cookie, q->cstring); #ifdef FINDMOPS - if (ismops && DECRA(p->n_reg, 0) != regno(l) && cookie != FOREFF) { + /* Copy the RMW result to the allocated register only when a + * register result was actually requested: an ASSIGN elided under + * a compare (matched with FORCC, evaluated here with cookie 0) + * has no result register at all - n_reg is -1 and DECRA of it + * would name a garbage register. */ + if (ismops && DECRA(p->n_reg, 0) != regno(l) && (cookie & INREGS)) { CDEBUG(("gencode(%p) rmove\n", p)); rmove(regno(l), DECRA(p->n_reg, 0), p->n_type); } else diff --git a/mip/regs.c b/mip/regs.c index b8bbcf70a..9181f0093 100644 --- a/mip/regs.c +++ b/mip/regs.c @@ -2407,7 +2407,13 @@ colfind(int okColors, REGW *r) RDEBUG(("colfind: Recommend color from %d\n", ASGNUM(w))); return COLOR(w); } +#ifdef PICKCOLOR + /* let the target choose among the remaining colors (e.g. to keep + * the callee-save range in the prologue minimal) */ + return color2reg(PICKCOLOR(CLASS(r), okColors), CLASS(r)); +#else return color2reg(ffs(okColors)-1, CLASS(r)); +#endif } static void @@ -2444,6 +2450,28 @@ AssignColors(struct interpass *ip) okColors &= ~c; } } +#ifdef SPILLSHADOW + /* + * A permreg shadow that cannot keep its own register is + * cheaper spilled (the target prologue saves the register) + * than parked in another callee-save register with entry/ + * exit moves. Force the spill path; the ONLYPERM rewrite + * then marks the register in nsavregs and recolors. + */ + if (okColors != 0 && + w >= &nblock[tempmin] && w < &nblock[basetemp]) { + int own = permregs[(int)(w - nblock) - tempmin]; + for (c = 0; c < regK[CLASS(w)]; c++) + if (color2reg(c, CLASS(w)) == own) + break; + if (c == regK[CLASS(w)] || + ((1 << c) & okColors) == 0) { + RDEBUG(("shadow %d loses %s: spill\n", + ASGNUM(w), rnames[own])); + okColors = 0; + } + } +#endif if (okColors == 0) { PUSHWLIST(w, spilledNodes); #ifdef PCC_DEBUG @@ -3020,9 +3048,20 @@ ngenregs(struct p2env *p2e) memset(edgehash, 0, sizeof(edgehash)); /* clear adjacent node list */ - for (i = 0; i < MAXREGS; i++) + for (i = 0; i < MAXREGS; i++) { for (j = 0; j < NUMCLASS+1; j++) NCLASS(&ablock[i], j) = 0; + /* + * Clear the precolored move lists like the nblock memset + * below does for the temporaries: a stale entry from the + * previous round makes moveadd()'s already-there check + * drop the move, so it never re-enters worklistMoves and + * the temp loses its coalesce hint (a "return 0" temp + * colored away from the return register emitted + * "clr r0; ld r1,r0" instead of "clr r1"). + */ + ablock[i].r_moveList = NULL; + } if (tbits) { memset(nblock+tempmin, 0, tbits * sizeof(REGW)); diff --git a/os/coherent/ccconfig.h b/os/coherent/ccconfig.h new file mode 100644 index 000000000..b5e202f53 --- /dev/null +++ b/os/coherent/ccconfig.h @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2026 Michal Pleban. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* + * Various settings that control how the C compiler works for + * the Coherent OS on the Zilog Z8001 (Commodore 900). + */ + +/* cpp predefines: Coherent identifies itself with these macros */ +#define CPPADD { NULL } +#define CPPMDADD { "-DZ8001", "-Dcoherent", "-Dunix", NULL } + +/* Startup object: segmented Z8001 C run-time start-off (csu/crts0.s), + * entry point "start" which calls main_. No ELF-style crti/crtn. + * A bare name: the driver resolves it through the crt search dirs + * (-B directories, and on Windows the driver's own directory). */ +#define CRT0 "crts0.o" +#define CRTBEGIN 0 +#define CRTEND 0 +#define CRTI 0 +#define CRTN 0 + +/* Default library linked into every C program. A bare file name: + * Coherent ld hardcodes /lib and /usr/lib for -l (useless when + * cross-linking), so the driver resolves the file itself. */ +#define DEFLIBS { "libc.a", 0 } + +/* + * No -L default search paths: to Coherent ld -L means "large memory + * model", not a library path. Libraries and crt files are resolved + * by the driver through -B directories (and the driver's directory). + */ +#define DEFLIBDIRS { 0 } + +/* + * The Coherent assembler treats undefined symbols as errors unless + * -g is given, which turns them into external references (the native + * cc driver relies on this too). + */ +#define PCC_EARLY_AS_ARGS strlist_append(&args, "-g"); + +/* + * System include paths: /include first, then /usr/include. + * Assembler and linker paths are not set here; they are + * configured at build time via --with-assembler / --with-linker. + */ +#define STDINC "/include/" +#define STDINCS { "/include/", "/usr/include/", 0 } diff --git a/ztests/e73.c b/ztests/e73.c new file mode 100644 index 000000000..5728fbb9a --- /dev/null +++ b/ztests/e73.c @@ -0,0 +1,160 @@ +/* + * e73.c - djnz loop-counter fusion + non-loop RMW compare elision. + * + * Two related optimizations exercised here: + * + * (A) cbfuse (arch/z8001/local2.c) turns a fused countdown loop branch + * "dec r,$1 ; jr ne,L" into a single "djnz r,L" (backward only; the + * Coherent as relaxes an out-of-range djnz to "dec;jp nz"). + * + * (B) rmwrenamesd (arch/z8001/local2.c) recovers the read-modify-write + * shape for a straight-line "if ((x op= y))" so the compare-vs-zero + * is elided: "sub r,rY ; jr ne" instead of "sub;test;jr". + * + * The oki() checks verify SEMANTICS. To confirm the optimizations FIRED, + * inspect the generated e73.s: + * - dj_while/dj_do/dj_expr -> contain "djnz" and NO "jr ne" loop close + * - rmw_sub/rmw_and/rmw_or -> "sub/and/or ...; jr ne/eq" and NO "test" + * - ctl_byte -> "dbjnz" is NOT emitted (byte counter kept as decb;jr) + * - ctl_step2 -> NO djnz (decrement by 2) + * - ctl_live -> keeps a separate test (source stays live) + * + * Build (cross): + * ./cc/cpp/z8001-coherent-cpp.exe ztests/e73.c > f.i + * ./cc/ccom/z8001-coherent-ccom.exe -xtemps -xdeljumps -xssa < f.i | tr -d '\r' > e73.s + * On Coherent: + * /bin/as -g -o e73.o e73.s + * /bin/ld -o e73 /lib/crts0.o e73.o -lc + * ./e73 + */ + +int nfail = 0; + +void +oki(char *name, int got, int want) +{ + if (got == want) + printf("ok %s\n", name); + else { + printf("FAIL %s: got %d want %d\n", name, got, want); + nfail++; + } +} + +/* ---- (A) djnz countdown loops ----------------------------------------- */ + +/* while (--n): decrement THEN test; classic djnz shape */ +int +dj_while(int n) +{ + int c = 0; + while (--n) + c++; + return c; +} + +/* do { } while (--n): body runs first, backward branch on nonzero */ +int +dj_do(int n) +{ + int c = 0; + do { + c++; + } while (--n); + return c; +} + +/* count-down accumulator with a live-across-loop value (dj still applies) */ +int +dj_expr(int n) +{ + int sum = 0; + int k = n; + while (--k) + sum += k; + return sum; +} + +/* ---- (B) non-loop read-modify-write compare elision -------------------- */ + +int +rmw_sub(int x, int y) +{ + if ((x -= y)) + return 100 + x; + return x; /* x == 0 here */ +} + +int +rmw_and(int x, int mask) +{ + if ((x &= mask)) + return 200 + x; + return 999; +} + +int +rmw_or(int x, int bits) +{ + if ((x |= bits) == 0) + return -1; + return x; +} + +/* ---- controls that must NOT transform --------------------------------- */ + +/* byte counter: dbjnz not handled -> stays decb;jr (semantics still ok) */ +int +ctl_byte(int n) +{ + unsigned char b = (unsigned char)n; + int c = 0; + while (--b) + c++; + return c; +} + +/* decrement by 2: not expressible as djnz */ +int +ctl_step2(int n) +{ + int c = 0; + while ((n -= 2) > 0) + c++; + return c; +} + +/* source stays live after the RMW: rename must be rejected */ +int +ctl_live(int x, int y) +{ + int r = 0; + int t = x - y; + if (t) + r = 1; + return r + (x - y); /* recomputes x-y: x,y still live */ +} + +int +main() +{ + oki("dj_while.5", dj_while(5), 4); + oki("dj_while.1", dj_while(1), 0); + oki("dj_do.5", dj_do(5), 5); + oki("dj_do.1", dj_do(1), 1); + oki("dj_expr.5", dj_expr(5), 4 + 3 + 2 + 1); + + oki("rmw_sub.ne", rmw_sub(7, 3), 104); + oki("rmw_sub.eq", rmw_sub(4, 4), 0); + oki("rmw_and.ne", rmw_and(0xF3, 0x0F), 200 + 3); + oki("rmw_and.eq", rmw_and(0xF0, 0x0F), 999); + oki("rmw_or.ne", rmw_or(0, 6), 6); + + oki("ctl_byte.5", ctl_byte(5), 4); + oki("ctl_step2", ctl_step2(6), 2); /* 6->4(c1)->2(c2)->0 stop : 2 */ + oki("ctl_live", ctl_live(9, 4), 1 + 5); + + if (nfail == 0) + printf("DJNZ OK\n"); + return nfail; +}