From 98ca3a725e3c619fb3dfa340bcfd4784cfc79c34 Mon Sep 17 00:00:00 2001 From: Michal Pleban Date: Tue, 30 Jun 2026 23:29:54 +0200 Subject: [PATCH 01/67] Add Z8001 (Coherent/Commodore 900) architecture backend New pass-2 backend in arch/z8001/ targeting the Coherent ABI on the Zilog Z8001 (segmented, 16-bit, big-endian, 32-bit segmented pointers): macdefs.h storage sizes, register model (word class A r0-r12, pair class B rr0..rr10), frame/stack macros code.c prologue data directives, Coherent segment directives (.shri/.strn/.prvd/.bssd), .byte string emission, .file suppression local.c pass-1 clocal, trailing-underscore name mangling, ctype local2.c prologue/epilogue, register naming, COLORMAP, adrput/conput order.c instruction ordering and OREG formation table.c instruction selection table OS/ABI config in os/coherent/ccconfig.h (include/library paths, crts0.o startup, trailing-underscore convention). Build wiring: configure.ac/configure recognise the coherent OS and config.sub the z8001 CPU, so --target=z8001-coherent selects the backend. Verified end to end: a Hello World program compiles through the cross PCC to Z8001 assembly that the native Coherent as/ld assemble and link into a working executable, at both -O0 and -O1 (SSA). Co-Authored-By: Claude Opus 4.8 (1M context) --- arch/z8001/code.c | 289 +++++++++++++ arch/z8001/local.c | 365 ++++++++++++++++ arch/z8001/local2.c | 614 +++++++++++++++++++++++++++ arch/z8001/macdefs.h | 267 ++++++++++++ arch/z8001/order.c | 201 +++++++++ arch/z8001/table.c | 932 +++++++++++++++++++++++++++++++++++++++++ config.sub | 6 +- configure | 7 + configure.ac | 7 + os/coherent/ccconfig.h | 58 +++ 10 files changed, 2743 insertions(+), 3 deletions(-) create mode 100644 arch/z8001/code.c create mode 100644 arch/z8001/local.c create mode 100644 arch/z8001/local2.c create mode 100644 arch/z8001/macdefs.h create mode 100644 arch/z8001/order.c create mode 100644 arch/z8001/table.c create mode 100644 os/coherent/ccconfig.h diff --git a/arch/z8001/code.c b/arch/z8001/code.c new file mode 100644 index 000000000..95eddf8d6 --- /dev/null +++ b/arch/z8001/code.c @@ -0,0 +1,289 @@ +/* + * Copyright (c) 2025 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" + +#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); +} + +/* + * End-of-function code. + * For struct/union return, Coherent uses a hidden first argument: + * the caller passes a pointer to the result area, and the callee + * copies the return value there, then returns the pointer in rr0. + * PCC handles the caller side automatically; here we just need to + * copy the local struct to the hidden pointer and load rr0 with it. + */ +void +efcode(void) +{ + NODE *p, *q; + + if (cftnsp->stype != STRTY+FTN && cftnsp->stype != UNIONTY+FTN) + return; + + /* + * The hidden pointer arrives as the first argument. + * Build: *hidden_ptr = return_value; rr0 = hidden_ptr. + */ + q = block(REG, NIL, NIL, INCREF(cftnsp->stype - FTN), + cftnsp->sdf, cftnsp->sap); + slval(q, 0); + regno(q) = RR0; + + p = buildtree(UMUL, tcopy(q), NIL); + p = buildtree(ASSIGN, p, + block(REG, NIL, NIL, cftnsp->stype - FTN, + cftnsp->sdf, cftnsp->sap)); + ecomp(p); + + /* return the pointer in rr0 (already there from the hidden arg) */ +} + +/* + * Beginning-of-function code. + * Z8001 ABI is pure stack-based (no argument registers), so 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; + int i; + + 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. + */ +void +ejobcode(int flag) +{ +} + +/* + * 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; + + astypnames[SHORT] = astypnames[USHORT] = "\t.word"; + astypnames[INT] = astypnames[UNSIGNED] = "\t.word"; + astypnames[LONG] = astypnames[ULONG] = "\t.long"; + asspace = "\t.blkb"; +} + +/* + * 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"); +} + +/* + * 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) + r->n_right = block(FUNARG, r->n_right, NIL, + r->n_right->n_type, r->n_right->n_df, + r->n_right->n_ap); + } + if (r->n_op != STARG) { + l = talloc(); + *l = *r; + r->n_op = FUNARG; + r->n_left = l; + r->n_type = l->n_type; + } + return p; +} + +/* Fix up type of a bit-field. */ +void +fldty(struct symtab *p) +{ +} + +/* Use PCC's default switch generation. */ +int +mygenswitch(int num, TWORD type, struct swents **p, int n) +{ + return 0; +} + +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..133359fea --- /dev/null +++ b/arch/z8001/local.c @@ -0,0 +1,365 @@ +/* + * Copyright (c) 2025 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 + +/* + * 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: + /* + * Convert auto/register to frame-relative OREG. + * soffset is in bits; divide by SZCHAR to get bytes. + */ + p->n_op = OREG; + slval(p, q->soffset / SZCHAR); + p->n_rval = FPREG; + break; + + case PARAM: + /* + * Parameters arrive above the frame pointer. + * ARGINIT = 32 bits = 4 bytes above R13 for the first arg. + */ + p->n_op = OREG; + slval(p, q->soffset / SZCHAR); + p->n_rval = FPREG; + 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 SCONV: + /* + * Scalar conversion: try to eliminate no-ops. + */ + l = p->n_left; + m = l->n_type; + + /* 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 <-> long in same class: no-op */ + if ((m == LONG || m == ULONG) && + (p->n_type == LONG || p->n_type == ULONG)) { + l->n_type = p->n_type; + nfree(p); + return l; + } + break; + + case FORCE: + /* + * FORCE ensures the return value is in the correct register. + * On Z8001: int/short in R1, long/ptr in RR0. + */ + 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 and long long require 4 word registers — not supported. + */ +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. + */ +int +ninval(CONSZ off, int fsz, NODE *p) +{ + return 0; /* let generic inval() handle all types */ +} + +/* + * 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; + + if (p->n_op != FCON) + return; + + sp = isinlining ? permalloc(sizeof(struct symtab)) + : tmpalloc(sizeof(struct symtab)); + sp->sclass = STATIC; + sp->sap = 0; + sp->slevel = 1; /* fake numeric label */ + sp->soffset = getlab(); + sp->sflags = 0; + sp->stype = p->n_type; + sp->squal = (CON >> TSHIFT); + sp->sname = NULL; + + 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 uses ".comm " (whitespace separated). + */ +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->slevel == 0) + printf("\t.comm\t%s\t%d\n", name, off); + else + printf("\t.comm\t" LABFMT "\t%d\n", sp->soffset, off); +} + diff --git a/arch/z8001/local2.c b/arch/z8001/local2.c new file mode 100644 index 000000000..1495db66d --- /dev/null +++ b/arch/z8001/local2.c @@ -0,0 +1,614 @@ +/* + * Copyright (c) 2025 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 (our PCC-compatible model, different from reference compiler): + * + * high address + * [caller args] at r13+4, r13+6, ... (ARGINIT=32 bits = 4 bytes) + * [return addr] at r13+0 .. r13+3 (4-byte segmented return addr) + * -------------- r13 = entry SP = frame pointer + * [locals] at r13-2, r13-4, ... (AUTOINIT=0) + * [callee saves] at r13-fsize-2*nsave .. r13-fsize-2 + * -------------- r15 = current SP + * low address + * + * Prologue sequence: + * dec/sub r15, $total (total = fsize + 2*nsave) + * [ld (rr14), r13 (for nsave == 1)] + * [ldm (rr14), rX, $nsave (for nsave > 1, saves rX..r13)] + * ld r13, r15 + * inc/add r13, $total (r13 = entry SP) + * + * 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); + +/* + * Register names indexed by PCC register number. + * r0-r15 are 16-bit word registers; rr0,rr2,...,rr10 are 32-bit pairs. + * Indices: 0-15 = r0-r15, 16=rr0, 17=rr2, 18=rr4, 19=rr6, 20=rr8, 21=rr10. + */ +char *rnames[] = { + "r0", "r1", "r2", "r3", "r4", "r5", + "r6", "r7", "r8", "r9", "r10", "r11", + "r12", "r13", "r14", "r15", + "rr0", "rr2", "rr4", "rr6", "rr8", "rr10" +}; + +void +deflab(int label) +{ + printf(LABFMT ":\n", label); +} + +/* + * Emit the function prologue. + * + * Callee-saved word registers are R6-R12. We always save R13 (the frame + * pointer) because we change its value. We find the lowest-numbered + * callee-saved register that was actually used and save a contiguous run + * from that register through R13 with a single ldm or ld instruction. + */ +/* + * 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; + } + } + return firstsave; +} + +void +prologue(struct interpass_prolog *ipp) +{ + int firstsave, nsave, fsize, total; + + firstsave = firstsavereg(); + + /* Save firstsave..R13 inclusive (always save R13 since we clobber it) */ + nsave = R13 - firstsave + 1; + fsize = (p2maxautooff + 1) & ~1; /* p2maxautooff is bytes; round to word */ + total = fsize + nsave * 2; + + /* Step 1: allocate entire frame */ + if (total > 0) { + if (total <= 16) + printf("\tdec\tr15,$%d\n", total); + else + printf("\tsub\tr15,$%d\n", total); + } + + /* Step 2: save callee-saved registers at current SP (no auto-decrement) */ + if (nsave == 1) + printf("\tld\t(rr14),r13\n"); + else + printf("\tldm\t(rr14),r%d,$%d\n", firstsave, nsave); + + /* Step 3: r13 = current SP */ + printf("\tld\tr13,r15\n"); + + /* Step 4: r13 = entry SP (frame pointer for PCC convention) */ + if (total > 0) { + if (total <= 16) + printf("\tinc\tr13,$%d\n", total); + else + printf("\tadd\tr13,$%d\n", total); + } +} + +/* + * Emit the function epilogue. + */ +void +eoftn(struct interpass_prolog *ipp) +{ + int firstsave, nsave, fsize, total; + + if (ipp->ipp_ip.ip_lbl == 0) + return; + + /* Recompute the same values as the prologue */ + firstsave = firstsavereg(); + nsave = R13 - firstsave + 1; + fsize = (p2maxautooff + 1) & ~1; + total = fsize + nsave * 2; + + /* Restore callee-saved registers (neither ld nor ldm changes r15) */ + if (nsave == 1) + printf("\tld\tr13,(rr14)\n"); + else + printf("\tldm\tr%d,(rr14),$%d\n", firstsave, nsave); + + /* Reclaim the whole frame */ + if (total > 0) { + if (total <= 16) + printf("\tinc\tr15,$%d\n", total); + else + printf("\tadd\tr15,$%d\n", 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; + } +} + +/* + * zzzcode: handle special escape sequences in instruction templates. + * + * ZB stack cleanup after call: add/inc r15, $n_qual + * ZT struct argument (not yet implemented) + */ +void +zzzcode(NODE *p, int c) +{ + int n; + + switch (c) { + 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 'T': + /* Struct argument by value: copy struct onto stack */ + /* TODO: implement struct-by-value argument passing */ + comperr("zzzcode ZT: struct arg not supported"); + 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 (val != 0) + fprintf(io, CONFMT, val); + 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; + } +} + +/* + * cbgen: emit a conditional branch. + */ +static char *ccnames[] = { + "eq", /* EQ */ + "ne", /* NE */ + "le", /* LE */ + "lt", /* LT */ + "ge", /* GE */ + "gt", /* GT */ + "ule", /* ULE */ + "ult", /* ULT */ + "uge", /* UGE */ + "ugt", /* UGT */ +}; + +void +cbgen(int o, int lab) +{ + if (o < EQ || o > UGT) + comperr("cbgen: bad op %d", o); + printf("\tjr\t%s," LABFMT "\n", ccnames[o - EQ], lab); +} + +/* + * rmove: emit a register-to-register move. + */ +void +rmove(int s, int d, TWORD t) +{ + if (s == d) + return; + 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 (rr0,rr2,rr4,rr6,rr8,rr10); each pair overlaps two word registers. + */ +int +COLORMAP(int c, int *r) +{ + int num; + + switch (c) { + case CLASSA: + /* each interfering pair blocks two word registers */ + num = r[CLASSA] + 2 * r[CLASSB]; + return num < 13; + case CLASSB: + /* each interfering word can block at most one pair */ + num = r[CLASSB] + r[CLASSA]; + return num < 6; + } + return 0; +} + +/* + * Return the register class suitable for a value of type t. + * Consistent with PCLASS in macdefs.h: 32-bit values (long/ptr/float) + * use class B (pairs), everything else uses class A (words). + */ +int +gclass(TWORD t) +{ + return 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) + return attr_find(p->n_ap, ATTR_P2STRUCT)->iarg(0); + 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; + if (p->n_right == NIL) + return; /* no arguments */ + 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. Z8001 has no special address modes that need + * this, so always decline. + */ +int +special(NODE *p, int shape) +{ + 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; +} + +/* No target-specific pass2 optimisation. */ +void +myoptim(struct interpass *ip) +{ +} + +void +myreader(struct interpass *ip) +{ +} + +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..eac65068d --- /dev/null +++ b/arch/z8001/macdefs.h @@ -0,0 +1,267 @@ +/* + * Copyright (c) 2025 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). + */ +#define ALCHAR 8 +#define ALBOOL 8 +#define ALINT 16 +#define ALFLOAT 32 /* 32-bit float in even register pair */ +#define ALDOUBLE 32 /* 64-bit double aligned to even pair */ +#define ALLDOUBLE 32 +#define ALLONG 32 /* long in even register pair */ +#define ALLONGLONG 32 +#define ALSHORT 16 +#define ALPOINT 32 /* segmented pointer in even register pair */ +#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) + */ +#define szty(t) ((t) == DOUBLE || (t) == LDOUBLE || \ + (t) == LONGLONG || (t) == ULONGLONG ? 4 : \ + (t) == LONG || (t) == ULONG || \ + (t) == FLOAT || ISPTR(t) ? 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 + */ +#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 MAXREGS 22 + +/* + * 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|TEMPREG, /* rr0 */ \ + SBREG|TEMPREG, /* rr2 */ \ + SBREG|TEMPREG, /* 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 */ + +/* + * ROVERLAP: which other registers each register aliases. + * A pair RRn aliases the two word registers Rn and R(n+1). + */ +#define ROVERLAP \ + { RR0, -1 }, /* r0 */ \ + { RR0, -1 }, /* r1 */ \ + { RR2, -1 }, /* r2 */ \ + { RR2, -1 }, /* r3 */ \ + { RR4, -1 }, /* r4 */ \ + { RR4, -1 }, /* r5 */ \ + { RR6, -1 }, /* r6 */ \ + { RR6, -1 }, /* r7 */ \ + { RR8, -1 }, /* r8 */ \ + { RR8, -1 }, /* r9 */ \ + { RR10, -1 }, /* r10 */ \ + { RR10, -1 }, /* r11 */ \ + { -1 }, /* r12 - no pair */ \ + { -1 }, /* r13 - reserved */ \ + { -1 }, /* r14 - reserved */ \ + { -1 }, /* r15 - reserved */ \ + { R0, R1, -1 }, /* rr0 */ \ + { R2, R3, -1 }, /* rr2 */ \ + { R4, R5, -1 }, /* rr4 */ \ + { R6, R7, -1 }, /* rr6 */ \ + { R8, R9, -1 }, /* rr8 */ \ + { R10, R11, -1 }, /* rr10 */ + +/* Return the register class for a node's type */ +#define PCLASS(p) (szty((p)->n_type) == 2 ? SBREG : SAREG) + +#define NUMCLASS 2 /* two register classes: A (word) and B (pair) */ + +int COLORMAP(int c, int *r); +#define GCLASS(x) ((x) < 16 ? CLASSA : CLASSB) +#define DECRA(x,y) (((x) >> ((y)*5)) & 31) /* decode register from n_reg */ +#define ENCRD(x) (x) /* encode dest register */ +#define ENCRA1(x) ((x) << 5) +#define ENCRA2(x) ((x) << 10) +#define ENCRA(x,y) ((x) << (5+(y)*5)) /* encode source register */ + +/* + * Return register for each type: + * int/short/char -> r1 + * long/ptr/float -> rr0 (r0:r1) + */ +#define RETREG(x) (szty(x) == 2 ? RR0 : R1) + +#define FPREG R13 /* frame pointer */ +#define STKREG R15 /* stack pointer (low word of rr14) */ + +/* 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..031fdc803 --- /dev/null +++ b/arch/z8001/order.c @@ -0,0 +1,201 @@ +/* + * Copyright (c) 2025 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..1706ad747 --- /dev/null +++ b/arch/z8001/table.c @@ -0,0 +1,932 @@ +/* + * Copyright (c) 2025 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 + * + * Key calling convention facts: + * - Pure stack, right-to-left push, caller cleans + * - int result in r1; long/ptr result in rr0 (r0:r1) + * - 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) + +/* Convenience NEEDS shorthands (new-style) */ +#define NAREG NEEDS(NREG(A, 1)) +#define NBREG NEEDS(NREG(B, 1)) +#define XSL(c) NEEDS(NREG(c, 1), NSL(c)) +#define XSR(c) NEEDS(NREG(c, 1), NSR(c)) + +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 (zero-extend) */ +{ PCONV, INBREG, + SAREG, TWORD, + SANY, TPOINT, + XSL(B), RESC1, + " ld A1,AL\n clr U1\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 -> long: sign-extend word into pair */ +{ SCONV, INBREG, + SAREG, TINT, + SANY, TLONG, + XSL(B), RESC1, + " ld U1,AL\n ext0l A1\n", }, + +/* unsigned int -> unsigned long: zero-extend word into pair */ +{ SCONV, INBREG, + SAREG, TUNSIGNED, + SANY, TULONG, + XSL(B), RESC1, + " ld U1,AL\n clr A1\n", }, + +/* int/short from memory -> long */ +{ SCONV, INBREG, + SOREG|SNAME, TINT|TSHORT, + SANY, TLONG, + NBREG, RESC1, + " ld U1,AL\n ext0l A1\n", }, + +/* unsigned/ushort from memory -> unsigned long */ +{ SCONV, INBREG, + SOREG|SNAME, TUNSIGNED|TUSHORT, + SANY, TULONG, + NBREG, RESC1, + " ld U1,AL\n clr A1\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 in reg -> int (sign-extend) */ +{ SCONV, INAREG, + SAREG, TCHAR, + SANY, TINT|TUNSIGNED, + 0, RLEFT, + " extsb AL\n", }, + +/* unsigned char in reg -> (u)int (zero-extend) */ +{ SCONV, INAREG, + SAREG, TUCHAR, + SANY, TINT|TUNSIGNED, + 0, RLEFT, + " andb AL,$0xff\n", }, + +/* char from memory -> int */ +{ SCONV, INAREG, + SOREG|SNAME, TCHAR, + SANY, TINT|TUNSIGNED, + NAREG, RESC1, + " ldb A1,AL\n extsb A1\n", }, + +/* unsigned char from memory -> (u)int */ +{ SCONV, INAREG, + SOREG|SNAME, TUCHAR, + SANY, TINT|TUNSIGNED, + NAREG, RESC1, + " ldb A1,AL\n", }, + +/* char -> long */ +{ SCONV, INBREG, + SAREG, TCHAR, + SANY, TLONG, + XSL(B), RESC1, + " extsb AL\n ld U1,AL\n ext0l A1\n", }, + +/* unsigned char -> unsigned long */ +{ SCONV, INBREG, + SAREG, TUCHAR, + SANY, TULONG, + XSL(B), RESC1, + " andb AL,$0xff\n ld U1,AL\n clr A1\n", }, + +/* int -> char: keep low byte (already in register) */ +{ SCONV, INAREG, + SAREG, TWORD, + SANY, TCHAR|TUCHAR, + 0, RLEFT, + "", }, + +/* + * 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/char/ptr-lo) in SAREG */ +{ CALL, INAREG, + SCON, TANY, + SANY, TWORD|TCHAR|TUCHAR, + XSL(A), RESC1, + " calr CL\nZB", }, + +{ UCALL, INAREG, + SCON, TANY, + SANY, TWORD|TCHAR|TUCHAR, + XSL(A), RESC1, + " calr CL\n", }, + +/* Indirect call (function pointer in pair), word result */ +{ CALL, INAREG, + SBREG, TANY, + SANY, TWORD|TCHAR|TUCHAR, + XSL(A), RESC1, + " call (AL)\nZB", }, + +{ UCALL, INAREG, + SBREG, TANY, + SANY, TWORD|TCHAR|TUCHAR, + XSL(A), RESC1, + " call (AL)\n", }, + +/* Named call, result is long/ptr in SBREG */ +{ CALL, INBREG, + SCON, TANY, + SANY, TLONG|TULONG|TPOINT, + XSL(B), RESC1, + " calr CL\nZB", }, + +{ UCALL, INBREG, + SCON, TANY, + SANY, TLONG|TULONG|TPOINT, + XSL(B), RESC1, + " calr CL\n", }, + +/* Indirect call (function pointer in pair), long/ptr result */ +{ CALL, INBREG, + SBREG, TANY, + SANY, TLONG|TULONG|TPOINT, + XSL(B), RESC1, + " call (AL)\nZB", }, + +{ UCALL, INBREG, + SBREG, TANY, + SANY, TLONG|TULONG|TPOINT, + XSL(B), RESC1, + " call (AL)\n", }, + +/* 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 call (result by hidden pointer) */ +{ STCALL, INAREG, + SCON, TANY, + SANY, TANY, + XSL(A), RESC1, + " calr CL\nZB", }, + +{ USTCALL, INAREG, + SCON, TANY, + SANY, TANY, + XSL(A), RESC1, + " calr CL\n", }, + +{ STCALL, FOREFF, + SCON, TANY, + SANY, TANY, + 0, 0, + " calr CL\nZB", }, + +{ USTCALL, FOREFF, + SCON, TANY, + SANY, TANY, + 0, 0, + " calr CL\n", }, + +/* + * PLUS - integer addition. + */ + +/* inc word by 1 */ +{ PLUS, FOREFF|INAREG|FORCC, + SAREG|SOREG|SNAME, 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|SOREG|SNAME, TWORD, + SMONE, TANY, + 0, RLEFT|RESCC, + " dec AL,$1\n", }, + +/* add word reg + reg */ +{ PLUS, INAREG|FOREFF|FORCC, + SAREG, TWORD, + SAREG|SNAME|SOREG|SCON, TWORD, + 0, RLEFT|RESCC, + " add AL,AR\n", }, + +/* add word mem + reg/const (for side effects) */ +{ PLUS, FOREFF|FORCC, + SNAME|SOREG, TWORD, + SAREG|SCON, TWORD, + 0, RLEFT|RESCC, + " add AL,AR\n", }, + +/* add pair + pair (long) */ +{ PLUS, INBREG|FOREFF, + SBREG, TLONG|TULONG, + SBREG|SOREG|SNAME|SCON, TLONG|TULONG, + 0, RLEFT, + " addl AL,AR\n", }, + +/* pointer + int offset */ +{ PLUS, INBREG|FOREFF, + SBREG, TPOINT, + SAREG|SCON, TWORD, + 0, RLEFT, + " addl AL,AR\n", }, + +/* + * MINUS - integer subtraction. + */ + +/* dec word by 1 */ +{ MINUS, FOREFF|INAREG|FORCC, + SAREG|SOREG|SNAME, TWORD, + SONE, TANY, + 0, RLEFT|RESCC, + " dec AL,$1\n", }, + +/* inc word by 1 (MINUS SMONE) */ +{ MINUS, FOREFF|INAREG|FORCC, + SAREG|SOREG|SNAME, TWORD, + SMONE, TANY, + 0, RLEFT|RESCC, + " inc AL,$1\n", }, + +/* subtract word reg - reg */ +{ MINUS, INAREG|FOREFF|FORCC, + SAREG, TWORD, + SAREG|SNAME|SOREG|SCON, TWORD, + 0, RLEFT|RESCC, + " sub AL,AR\n", }, + +/* subtract word mem (side effect) */ +{ MINUS, FOREFF|FORCC, + SNAME|SOREG, TWORD, + SAREG|SCON, TWORD, + 0, RLEFT|RESCC, + " sub AL,AR\n", }, + +/* subtract pair (long) */ +{ MINUS, INBREG|FOREFF, + SBREG, TLONG|TULONG, + SBREG|SOREG|SNAME|SCON, TLONG|TULONG, + 0, RLEFT, + " subl AL,AR\n", }, + +/* pointer - word offset */ +{ MINUS, INBREG|FOREFF, + SBREG, TPOINT, + SAREG|SCON, TWORD, + 0, RLEFT, + " subl AL,AR\n", }, + +/* + * 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|SOREG|SCON, TWORD, + NEEDS(NLEFT(R1), NEVER(R0), NRES(R1)), RLEFT, + " mult rr0,AR\n", }, + +/* long multiply */ +{ MUL, INBREG, + SBREG, TLONG|TULONG, + SBREG|SNAME|SOREG|SCON, TLONG|TULONG, + NEEDS(NREG(B, 1), NSL(B), NRES(RR0)), RESC1, + " multl AL,AR\n", }, + +/* + * DIV - division. + */ + +/* + * word divide: divs rr0,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. + */ +{ DIV, INAREG, + SAREG, TINT, + SAREG|SNAME|SOREG|SCON, TINT, + NEEDS(NLEFT(R1), NEVER(R0), NRES(R1)), RLEFT, + " exts rr0\n divs rr0,AR\n", }, + +/* unsigned word divide */ +{ DIV, INAREG, + SAREG, TUNSIGNED, + SAREG|SNAME|SOREG|SCON, TUNSIGNED, + NEEDS(NLEFT(R1), NEVER(R0), NRES(R1)), RLEFT, + " clr r0\n divu rr0,AR\n", }, + +/* signed long divide */ +{ DIV, INBREG, + SBREG, TLONG, + SBREG|SNAME|SOREG|SCON, TLONG, + NEEDS(NREG(B, 1), NSL(B), NRES(RR0)), RESC1, + " divsl AL,AR\n", }, + +/* unsigned long divide */ +{ DIV, INBREG, + SBREG, TULONG, + SBREG|SNAME|SOREG|SCON, TULONG, + NEEDS(NREG(B, 1), NSL(B), NRES(RR0)), RESC1, + " divul AL,AR\n", }, + +/* + * MOD - remainder. + * divs/divu leaves the remainder in r0 (high word). We copy it down to + * r1 so the result lands in the same register that RLEFT reclaims. + */ +{ MOD, INAREG, + SAREG, TINT, + SAREG|SNAME|SOREG|SCON, TINT, + NEEDS(NLEFT(R1), NEVER(R0), NRES(R1)), RLEFT, + " exts rr0\n divs rr0,AR\n ld r1,r0\n", }, + +{ MOD, INAREG, + SAREG, TUNSIGNED, + SAREG|SNAME|SOREG|SCON, TUNSIGNED, + NEEDS(NLEFT(R1), NEVER(R0), NRES(R1)), RLEFT, + " clr r0\n divu rr0,AR\n ld r1,r0\n", }, + +/* + * Shifts. + */ + +/* logical shift left word */ +{ LS, INAREG|FOREFF, + SAREG, TWORD, + SAREG|SCON, TWORD, + 0, RLEFT, + " sll AL,AR\n", }, + +/* arithmetic shift right (signed) */ +{ RS, INAREG|FOREFF, + SAREG, TINT|TSHORT, + SAREG|SCON, TWORD, + 0, RLEFT, + " sra AL,AR\n", }, + +/* logical shift right (unsigned) */ +{ RS, INAREG|FOREFF, + SAREG, TUNSIGNED|TUSHORT, + SAREG|SCON, TWORD, + 0, RLEFT, + " srl AL,AR\n", }, + +/* shift left long pair */ +{ LS, INBREG|FOREFF, + SBREG, TLONG|TULONG, + SAREG|SCON, TWORD, + 0, RLEFT, + " slll AL,AR\n", }, + +/* arithmetic shift right long pair (signed) */ +{ RS, INBREG|FOREFF, + SBREG, TLONG, + SAREG|SCON, TWORD, + 0, RLEFT, + " sral AL,AR\n", }, + +/* logical shift right long pair (unsigned) */ +{ RS, INBREG|FOREFF, + SBREG, TULONG, + SAREG|SCON, TWORD, + 0, RLEFT, + " srll AL,AR\n", }, + +/* + * AND - bitwise and. + */ + +{ AND, INAREG|FOREFF|FORCC, + SAREG, TWORD, + SAREG|SNAME|SOREG|SCON, TWORD, + 0, RLEFT|RESCC, + " and AL,AR\n", }, + +{ AND, FOREFF|FORCC, + SNAME|SOREG, TWORD, + SAREG|SCON, TWORD, + 0, RLEFT|RESCC, + " and AL,AR\n", }, + +{ AND, INBREG|FOREFF, + SBREG, TLONG|TULONG, + SBREG|SNAME|SOREG|SCON, TLONG|TULONG, + 0, RLEFT, + " andl AL,AR\n", }, + +/* + * OR - bitwise or. + */ + +{ OR, INAREG|FOREFF|FORCC, + SAREG, TWORD, + SAREG|SNAME|SOREG|SCON, TWORD, + 0, RLEFT|RESCC, + " or AL,AR\n", }, + +{ OR, FOREFF|FORCC, + SNAME|SOREG, TWORD, + SAREG|SCON, TWORD, + 0, RLEFT|RESCC, + " or AL,AR\n", }, + +{ OR, INBREG|FOREFF, + SBREG, TLONG|TULONG, + SBREG|SNAME|SOREG|SCON, TLONG|TULONG, + 0, RLEFT, + " orl AL,AR\n", }, + +/* + * ER - bitwise exclusive or. + */ + +{ ER, INAREG|FOREFF|FORCC, + SAREG, TWORD, + SAREG|SNAME|SOREG|SCON, TWORD, + 0, RLEFT|RESCC, + " xor AL,AR\n", }, + +{ ER, FOREFF|FORCC, + SNAME|SOREG, TWORD, + SAREG|SCON, TWORD, + 0, RLEFT|RESCC, + " xor AL,AR\n", }, + +{ ER, INBREG|FOREFF, + SBREG, TLONG|TULONG, + SBREG|SNAME|SOREG|SCON, TLONG|TULONG, + 0, RLEFT, + " xorl AL,AR\n", }, + +/* + * ASSIGN - assignments. + */ + +/* word reg <- zero */ +{ ASSIGN, FOREFF|INAREG, + SAREG, TWORD, + SZERO, TANY, + 0, RDEST, + " clr AL\n", }, + +/* word mem <- zero */ +{ ASSIGN, FOREFF, + SNAME|SOREG, TWORD, + SZERO, TANY, + 0, 0, + " clr AL\n", }, + +/* pair <- zero (long) */ +{ ASSIGN, FOREFF|INBREG, + SBREG|SNAME|SOREG, TLONG|TULONG|TPOINT, + SZERO, TANY, + 0, RDEST, + " clrl AL\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 */ +{ ASSIGN, FOREFF|FORCC, + SNAME|SOREG, TWORD, + SAREG, TWORD, + 0, RESCC, + " ld AL,AR\n", }, + +/* word mem <- const */ +{ ASSIGN, FOREFF, + SNAME|SOREG, TWORD, + SCON, TANY, + 0, 0, + " ld AL,AR\n", }, + +/* pair reg <- pair reg or mem (long/ptr) */ +{ ASSIGN, FOREFF|INBREG, + SBREG, TLONG|TULONG|TPOINT, + SBREG|SNAME|SOREG|SCON, TLONG|TULONG|TPOINT, + 0, RDEST, + " ldl AL,AR\n", }, + +/* pair mem <- pair reg (long/ptr) */ +{ ASSIGN, FOREFF, + SNAME|SOREG, TLONG|TULONG|TPOINT, + SBREG, TLONG|TULONG|TPOINT, + 0, 0, + " ldl AL,AR\n", }, + +/* pair mem <- const long */ +{ ASSIGN, FOREFF, + SNAME|SOREG, TLONG|TULONG, + SCON, TANY, + 0, 0, + " ldl AL,AR\n", }, + +/* byte/char reg <- reg or mem */ +{ ASSIGN, FOREFF|INAREG|FORCC, + SAREG, TCHAR|TUCHAR, + SAREG|SNAME|SOREG|SCON, TCHAR|TUCHAR, + 0, RDEST|RESCC, + " ldb AL,AR\n", }, + +/* byte mem <- reg */ +{ ASSIGN, FOREFF|FORCC, + SNAME|SOREG, TCHAR|TUCHAR, + SAREG, TCHAR|TUCHAR, + 0, RESCC, + " ldb AL,AR\n", }, + +/* byte mem <- const */ +{ ASSIGN, FOREFF, + SNAME|SOREG, TCHAR|TUCHAR, + SCON, TANY, + 0, 0, + " ldb AL,AR\n", }, + +/* struct assignment */ +{ STASG, FOREFF|INAREG, + SOREG|SNAME, TANY, + SAREG, TPTRTO|TANY, + NEEDS(NREG(B, 1), NSL(B)), 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 through pair register */ +{ UMUL, INBREG, + SANY, TANY, + SOREG, TLONG|TULONG|TPOINT, + XSL(B), RESC1, + " ldl A1,AL\n", }, + +/* load signed char through pair register */ +{ UMUL, INAREG, + SANY, TANY, + SOREG, TCHAR, + XSL(A), RESC1, + " ldb A1,AL\n extsb A1\n", }, + +/* load unsigned char through pair register */ +{ UMUL, INAREG, + SANY, TANY, + SOREG, TUCHAR, + XSL(A), RESC1, + " ldb A1,AL\n", }, + +/* + * Logical/comparison operators. + * OPLOG covers EQ, NE, LE, GE, LT, GT, ULE, UGE, ULT, UGT. + */ + +/* compare word vs zero: use test */ +{ OPLOG, FORCC, + SAREG|SNAME|SOREG, TWORD, + SZERO, TANY, + 0, RESCC, + " test AL\n", }, + +/* compare pair vs zero: use testl */ +{ OPLOG, FORCC, + SBREG|SNAME|SOREG, TLONG|TULONG|TPOINT, + SZERO, TANY, + 0, RESCC, + " testl AL\n", }, + +/* compare word vs word */ +{ OPLOG, FORCC, + SAREG|SNAME|SOREG, TWORD, + SAREG|SCON, TWORD, + 0, RESCC, + " cp AL,AR\n", }, + +/* compare pair vs pair (long/ptr) */ +{ OPLOG, FORCC, + SBREG|SNAME|SOREG, TLONG|TULONG|TPOINT, + SBREG|SCON, TLONG|TULONG|TPOINT, + 0, RESCC, + " cpl AL,AR\n", }, + +/* compare char vs char */ +{ OPLOG, FORCC, + SAREG|SNAME|SOREG, TCHAR|TUCHAR, + SAREG|SCON, TCHAR|TUCHAR, + 0, RESCC, + " cpb AL,AR\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", }, + +/* + * OPLTYPE - load leaf type into register. + * Used to materialize NAME/ICON/OREG into a register. + */ + +/* 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", }, + +/* load long/ptr constant/name/oreg into pair register */ +{ OPLTYPE, INBREG, + SANY, TANY, + SBREG|SCON|SOREG|SNAME, TLONG|TULONG|TPOINT, + NBREG, RESC1, + " ldl A1,AL\n", }, + +/* load signed char from mem into word register */ +{ OPLTYPE, INAREG, + SANY, TANY, + SOREG|SNAME, TCHAR, + NAREG, RESC1, + " ldb A1,AL\n extsb A1\n", }, + +/* load unsigned char from mem into word register */ +{ OPLTYPE, INAREG, + SANY, TANY, + SOREG|SNAME, TUCHAR, + NAREG, 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", }, + +/* + * UMINUS - unary negation. + */ + +{ UMINUS, INAREG|FOREFF, + SAREG, TWORD|TCHAR|TUCHAR, + SANY, TANY, + 0, RLEFT, + " neg AL\n", }, + +{ UMINUS, INBREG|FOREFF, + SBREG, TLONG|TULONG, + SANY, TANY, + 0, RLEFT, + " negl AL\n", }, + +/* + * COMPL - bitwise complement. + */ + +{ COMPL, INAREG, + SAREG, TWORD, + SANY, TANY, + 0, RLEFT, + " com AL\n", }, + +{ COMPL, INBREG, + SBREG, TLONG|TULONG, + SANY, TANY, + 0, RLEFT, + " coml AL\n", }, + +/* + * 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 word constant or memory */ +{ FUNARG, FOREFF, + SCON|SNAME|SOREG, TWORD|TSHORT|TUSHORT, + SANY, TWORD, + NAREG, RNULL, + " ld A1,AL\n push (rr14),A1\n", }, + +/* push long/ptr pair register */ +{ FUNARG, FOREFF, + SBREG, TLONG|TULONG|TPOINT, + SANY, TLONG|TULONG|TPOINT, + 0, RNULL, + " pushl (rr14),AL\n", }, + +/* push long/ptr from memory or constant */ +{ FUNARG, FOREFF, + SCON|SNAME|SOREG, TLONG|TULONG|TPOINT, + SANY, TLONG|TULONG|TPOINT, + NBREG, RNULL, + " ldl A1,AL\n pushl (rr14),A1\n", }, + +/* push char (promoted to word) */ +{ FUNARG, FOREFF, + SAREG, TCHAR|TUCHAR, + SANY, TCHAR|TUCHAR, + 0, RNULL, + " push (rr14),AL\n", }, + +/* push zero word */ +{ FUNARG, FOREFF, + SZERO, TANY, + SANY, TANY, + 0, RNULL, + " push (rr14),$0\n", }, + +/* struct argument (by-value struct copy onto stack) */ +{ STARG, FOREFF, + SBREG, TPTRTO|TANY, + SANY, TSTRUCT, + 0, 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/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/os/coherent/ccconfig.h b/os/coherent/ccconfig.h new file mode 100644 index 000000000..462fdf2be --- /dev/null +++ b/os/coherent/ccconfig.h @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2025 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. */ +#define CRT0 "/lib/crts0.o" +#define CRTBEGIN 0 +#define CRTEND 0 +#define CRTI 0 +#define CRTN 0 + +/* Default library linked into every C program */ +#define DEFLIBS { "-lc", 0 } + +/* + * Library search paths: /lib first, then /usr/lib. + * Confirmed from linker source (_examples/cmd/ld/main.c). + */ +#define DEFLIBDIRS { "/lib/", "/usr/lib/", 0 } + +/* + * 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 } From a5ca6916e207f33f25ca5c1482b641379f642027 Mon Sep 17 00:00:00 2001 From: Michal Pleban Date: Thu, 2 Jul 2026 21:29:29 +0200 Subject: [PATCH 02/67] z8001: fix code generation for the Coherent segmented ABI Fixes six code-generation bugs, each verified against the Coherent assembler/linker sources and the reference-compiler output. With these, the full C test surface (integer/long arithmetic, div/mod, shifts, bitwise, pointers, control flow, switch, globals/static/arrays/strings, char parameters, and structs by pointer and by value) compiles, links with the native as/ld, and runs correctly on a Commodore 900. - local.c: emit ".comm name,size" with a comma; the assembler's S_COMM handler requires it (getid then "if (getnb()!=',') qerr()"). - local2.c/table.c: address frame objects in the stack segment. Set the frame pointer r13 to the frame bottom and define a per-function equate "L = SS|framesize", referencing every slot as "L+off(r13)" (matching the reference compiler). Taking the address of a local (&x) uses the same equate so the resulting pointer carries the stack segment SS. - local2.c/table.c: byte operations must name the low-byte register "rlN"; a plain "rN" selects the high byte "rhN". extsb/exts keep the word name; unsigned char zero-extension uses word "and rN,$0xff". - table.c: compare against zero with cp/cpl, not test/testl. TEST computes "dst OR 0", leaving the P/V flag as parity rather than signed overflow, so the signed jr lt/ge conditions misfire at the 0 boundary. - local2.c: struct-by-value argument copy builds the destination pointer as (source segment):r15 rather than copying rr14, whose segment word is not a usable data-segment value. - local2.c: a pair-register base with a displacement is Based addressing, written "rrN(disp)" (base pair first); "disp(rrN)" is parsed as Indexed mode and uses the pair as a word index. Fixes p->member and p[i]. Also update the copyright year to 2026. Co-Authored-By: Claude Opus 4.8 (1M context) --- arch/z8001/code.c | 17 ++- arch/z8001/local.c | 56 ++++++-- arch/z8001/local2.c | 318 +++++++++++++++++++++++++++++++++++++---- arch/z8001/macdefs.h | 5 +- arch/z8001/order.c | 2 +- arch/z8001/table.c | 199 ++++++++++++++++---------- os/coherent/ccconfig.h | 2 +- 7 files changed, 476 insertions(+), 123 deletions(-) diff --git a/arch/z8001/code.c b/arch/z8001/code.c index 95eddf8d6..4eefcd828 100644 --- a/arch/z8001/code.c +++ b/arch/z8001/code.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Michal Pleban. + * Copyright (c) 2026 Michal Pleban. * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -185,11 +185,26 @@ 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) */ } /* diff --git a/arch/z8001/local.c b/arch/z8001/local.c index 133359fea..5fb34f7b9 100644 --- a/arch/z8001/local.c +++ b/arch/z8001/local.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Michal Pleban. + * Copyright (c) 2026 Michal Pleban. * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -74,22 +74,40 @@ clocal(NODE *p) switch (q->sclass) { case AUTO: case REGISTER: + case PARAM: /* - * Convert auto/register to frame-relative OREG. - * soffset is in bits; divide by SZCHAR to get bytes. + * An aggregate local or parameter (struct, union, or + * array) must stay an addressable object so that member + * access (p.x), indexing (a[i]), array decay and &p all + * work: build a frame structure reference *(r13 + off). + * &(*(r13+off)) then cancels to the lda form rather than + * hitting ADDROF(OREG). Same idiom as arch/i86. */ - p->n_op = OREG; - slval(p, q->soffset / SZCHAR); - p->n_rval = FPREG; - break; - - case PARAM: + if (ISARY(p->n_type) || + p->n_type == STRTY || p->n_type == UNIONTY) { + 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; + } /* - * Parameters arrive above the frame pointer. - * ARGINIT = 32 bits = 4 bytes above R13 for the first arg. + * Scalar auto/param: frame-relative OREG. soffset is + * in bits; divide by SZCHAR to get bytes. Parameters + * arrive above the frame pointer (ARGINIT = 4 bytes). + * + * Each argument occupies at least a 16-bit slot (the + * caller promotes char to a pushed word). On big-endian + * a char value lives in the low-order byte of that slot, + * so a char parameter's access offset is slot start + 1. */ p->n_op = OREG; - slval(p, q->soffset / SZCHAR); + o = q->soffset / SZCHAR; + if (q->sclass == PARAM && + (p->n_type == CHAR || p->n_type == UCHAR || + p->n_type == BOOL)) + o += (SZINT - SZCHAR) / SZCHAR; + slval(p, o); p->n_rval = FPREG; break; @@ -344,7 +362,9 @@ extdec(struct symtab *q) /* * Emit an uninitialized (common) data definition. - * Coherent's assembler uses ".comm " (whitespace separated). + * 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) @@ -357,9 +377,15 @@ defzero(struct symtab *sp) SETOFF(off, SZCHAR); off /= SZCHAR; + /* 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\t%d\n", name, off); + printf("\t.comm\t%s,%d\n", name, off); else - printf("\t.comm\t" LABFMT "\t%d\n", sp->soffset, off); + printf("\t.comm\t" LABFMT ",%d\n", sp->soffset, off); } diff --git a/arch/z8001/local2.c b/arch/z8001/local2.c index 1495db66d..67c2f1601 100644 --- a/arch/z8001/local2.c +++ b/arch/z8001/local2.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Michal Pleban. + * Copyright (c) 2026 Michal Pleban. * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -26,23 +26,34 @@ /* * pass2 local routines for the Zilog Z8001 / Coherent target. * - * Frame layout (our PCC-compatible model, different from reference compiler): + * 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] at r13+4, r13+6, ... (ARGINIT=32 bits = 4 bytes) - * [return addr] at r13+0 .. r13+3 (4-byte segmented return addr) - * -------------- r13 = entry SP = frame pointer - * [locals] at r13-2, r13-4, ... (AUTOINIT=0) - * [callee saves] at r13-fsize-2*nsave .. r13-fsize-2 - * -------------- r15 = current SP + * [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: - * dec/sub r15, $total (total = fsize + 2*nsave) + * 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 - * inc/add r13, $total (r13 = entry SP) + * 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 @@ -73,6 +84,16 @@ deflab(int label) 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; + /* * Emit the function prologue. * @@ -121,6 +142,16 @@ prologue(struct interpass_prolog *ipp) fsize = (p2maxautooff + 1) & ~1; /* p2maxautooff is bytes; round to word */ total = fsize + nsave * 2; + /* + * Frame-base equate for stack-segment addressing (see framelab above). + * L = SS|total; slots are then 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 SS segment. total >= 2 always + * (R13 is always saved), so SS|total is never SS|0. + */ + framelab = getlab2(); + printf(LABFMT "=SS|%d\n", framelab, total); + /* Step 1: allocate entire frame */ if (total > 0) { if (total <= 16) @@ -135,16 +166,13 @@ prologue(struct interpass_prolog *ipp) else printf("\tldm\t(rr14),r%d,$%d\n", firstsave, nsave); - /* Step 3: r13 = current SP */ + /* + * Step 3: r13 = current SP = frame bottom. (Native keeps the frame + * pointer at the bottom and reaches slots via the SS|total equate; + * node offsets remain entry-SP-relative and are corrected by the +total + * baked into the equate.) + */ printf("\tld\tr13,r15\n"); - - /* Step 4: r13 = entry SP (frame pointer for PCC convention) */ - if (total > 0) { - if (total <= 16) - printf("\tinc\tr13,$%d\n", total); - else - printf("\tadd\tr13,$%d\n", total); - } } /* @@ -232,18 +260,169 @@ tlen(NODE *p) } } +/* + * blput: print an operand for a BYTE instruction. + * + * The Z8001 only has byte registers for r0..r7: in a byte instruction the + * register field 8+N names the LOW byte of word register rN ("rlN"), while + * field N names the HIGH byte ("rhN"). A char value held in word register rN + * lives in its low byte, so byte ops (ldb/andb/cpb/...) must name it "rlN" -- + * printing the plain word name "rN" would (mis)select the high byte rhN. + * Non-register operands (memory, constants) print exactly as adrput. + */ +static void +blput(NODE *p) +{ + if (p->n_op == REG) { + if (p->n_rval > R7) + comperr("blput: byte value in non-byte-addressable " + "register r%d (needs r0-r7)", p->n_rval); + printf("rl%d", p->n_rval); + } else + adrput(stdout, p); +} + +/* + * 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); + } +} + /* * zzzcode: handle special escape sequences in instruction templates. * * ZB stack cleanup after call: add/inc r15, $n_qual - * ZT struct argument (not yet implemented) + * 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 + * ZS struct assignment: ldirb block copy + * ZT struct argument: allocate stack slot + ldirb block copy */ void zzzcode(NODE *p, int c) { int n; + char *op; switch (c) { + case 'H': /* byte-register form of the result operand (A1) */ + blput(getlr(p, '1')); + break; + case 'G': /* byte-register form of the left operand (AL) */ + blput(getlr(p, 'L')); + break; + case 'J': /* byte-register form of the right operand (AR) */ + blput(getlr(p, 'R')); + 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; @@ -255,10 +434,59 @@ zzzcode(NODE *p, int c) printf("\tadd\tr15,$%d\n", n); break; - case 'T': - /* Struct argument by value: copy struct onto stack */ - /* TODO: implement struct-by-value argument passing */ - comperr("zzzcode ZT: struct arg not supported"); + case 'S': /* struct assignment: ldirb (dest),(src),count */ + { + struct attr *ap = attr_find(p->n_ap, ATTR_P2STRUCT); + int sz = ap->iarg(0); + + /* 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. */ + 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 '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; default: @@ -409,9 +637,38 @@ adrput(FILE *io, NODE *p) case OREG: val = getlval(p); - if (val != 0) + 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); - fprintf(io, "(%s)", rnames[p->n_rval]); + fputc(')', io); + } else { + /* Pair-register base, zero displacement: indirect. */ + fprintf(io, "(%s)", rnames[p->n_rval]); + } return; case ICON: @@ -499,9 +756,9 @@ COLORMAP(int c, int *r) num = r[CLASSA] + 2 * r[CLASSB]; return num < 13; case CLASSB: - /* each interfering word can block at most one pair */ + /* 5 allocatable pairs (rr2,rr4,rr6,rr8,rr10; rr0 reserved) */ num = r[CLASSB] + r[CLASSA]; - return num < 6; + return num < 5; } return 0; } @@ -530,7 +787,8 @@ argsiz(NODE *p) if (t == DOUBLE || t == LDOUBLE || t == LONGLONG || t == ULONGLONG) return 8; if (t == STRTY || t == UNIONTY) - return attr_find(p->n_ap, ATTR_P2STRUCT)->iarg(0); + /* 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 */ } diff --git a/arch/z8001/macdefs.h b/arch/z8001/macdefs.h index eac65068d..3c0362cb0 100644 --- a/arch/z8001/macdefs.h +++ b/arch/z8001/macdefs.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Michal Pleban. + * Copyright (c) 2026 Michal Pleban. * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -200,7 +200,8 @@ typedef long long OFFSZ; 0, /* r13 - frame pointer */ \ 0, /* r14 - SP high */ \ 0, /* r15 - SP low */ \ - SBREG|TEMPREG, /* rr0 */ \ + SBREG|TEMPREG, /* rr0 - valid color (RETREG) but cleared from clregs + in bjobcode so never allocated (RR0 can't be a base) */ \ SBREG|TEMPREG, /* rr2 */ \ SBREG|TEMPREG, /* rr4 */ \ SBREG, /* rr6 - preserved via its words r6/r7 */ \ diff --git a/arch/z8001/order.c b/arch/z8001/order.c index 031fdc803..7559f073f 100644 --- a/arch/z8001/order.c +++ b/arch/z8001/order.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Michal Pleban. + * Copyright (c) 2026 Michal Pleban. * All rights reserved. * * Redistribution and use in source and binary forms, with or without diff --git a/arch/z8001/table.c b/arch/z8001/table.c index 1706ad747..61411b3ce 100644 --- a/arch/z8001/table.c +++ b/arch/z8001/table.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Michal Pleban. + * Copyright (c) 2026 Michal Pleban. * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -120,7 +120,7 @@ struct optab table[] = { SAREG, TINT, SANY, TLONG, XSL(B), RESC1, - " ld U1,AL\n ext0l A1\n", }, + " ld U1,AL\n exts A1\n", }, /* unsigned int -> unsigned long: zero-extend word into pair */ { SCONV, INBREG, @@ -134,7 +134,7 @@ struct optab table[] = { SOREG|SNAME, TINT|TSHORT, SANY, TLONG, NBREG, RESC1, - " ld U1,AL\n ext0l A1\n", }, + " ld U1,AL\n exts A1\n", }, /* unsigned/ushort from memory -> unsigned long */ { SCONV, INBREG, @@ -162,35 +162,35 @@ struct optab table[] = { SAREG, TUCHAR, SANY, TINT|TUNSIGNED, 0, RLEFT, - " andb AL,$0xff\n", }, + " and AL,$0xff\n", }, /* char from memory -> int */ { SCONV, INAREG, SOREG|SNAME, TCHAR, SANY, TINT|TUNSIGNED, NAREG, RESC1, - " ldb A1,AL\n extsb A1\n", }, + " ldb ZH,AL\n extsb A1\n", }, /* unsigned char from memory -> (u)int */ { SCONV, INAREG, SOREG|SNAME, TUCHAR, SANY, TINT|TUNSIGNED, NAREG, RESC1, - " ldb A1,AL\n", }, + " clr A1\n ldb ZH,AL\n", }, /* char -> long */ { SCONV, INBREG, SAREG, TCHAR, SANY, TLONG, XSL(B), RESC1, - " extsb AL\n ld U1,AL\n ext0l A1\n", }, + " extsb AL\n ld U1,AL\n exts A1\n", }, /* unsigned char -> unsigned long */ { SCONV, INBREG, SAREG, TUCHAR, SANY, TULONG, XSL(B), RESC1, - " andb AL,$0xff\n ld U1,AL\n clr A1\n", }, + " and AL,$0xff\n ld U1,AL\n clr A1\n", }, /* int -> char: keep low byte (already in register) */ { SCONV, INAREG, @@ -345,19 +345,25 @@ struct optab table[] = { 0, RLEFT|RESCC, " add AL,AR\n", }, -/* add pair + pair (long) */ +/* add pair + pair (long/ptr); pointer offsets are widened to a pair */ { PLUS, INBREG|FOREFF, - SBREG, TLONG|TULONG, - SBREG|SOREG|SNAME|SCON, TLONG|TULONG, + SBREG, TLONG|TULONG|TPOINT, + SBREG|SOREG|SNAME|SCON, TLONG|TULONG|TPOINT, 0, RLEFT, " addl AL,AR\n", }, -/* pointer + int offset */ -{ PLUS, INBREG|FOREFF, - SBREG, TPOINT, - SAREG|SCON, TWORD, - 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. + */ +{ PLUS, INBREG, + SANY, TPOINT, + SCON, TANY, + NBREG, RESC1, + "ZF", }, /* * MINUS - integer subtraction. @@ -391,19 +397,24 @@ struct optab table[] = { 0, RLEFT|RESCC, " sub AL,AR\n", }, -/* subtract pair (long) */ +/* subtract pair (long/ptr); pointer offsets are widened to a pair */ { MINUS, INBREG|FOREFF, - SBREG, TLONG|TULONG, - SBREG|SOREG|SNAME|SCON, TLONG|TULONG, + SBREG, TLONG|TULONG|TPOINT, + SBREG|SOREG|SNAME|SCON, TLONG|TULONG|TPOINT, 0, RLEFT, " subl AL,AR\n", }, -/* pointer - word offset */ -{ MINUS, INBREG|FOREFF, - SBREG, TPOINT, - SAREG|SCON, TWORD, - 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. + */ +{ MINUS, INBREG, + SANY, TPOINT, + SCON, TANY, + NBREG, RESC1, + "ZF", }, /* * MUL - multiplication. @@ -418,69 +429,95 @@ struct optab table[] = { { MUL, INAREG, SAREG, TWORD, SAREG|SNAME|SOREG|SCON, TWORD, - NEEDS(NLEFT(R1), NEVER(R0), NRES(R1)), RLEFT, + NEEDS(NLEFT(R1), NEVER(R0), NRES(R1), NORIGHT(R0), NORIGHT(R1)), RLEFT, " mult rr0,AR\n", }, -/* long multiply */ +/* + * 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|SOREG|SCON, TLONG|TULONG, - NEEDS(NREG(B, 1), NSL(B), NRES(RR0)), RESC1, - " multl AL,AR\n", }, + NEEDS(NLEFT(RR2), NEVER(RR0), NRES(RR2), NORIGHT(RR0), NORIGHT(RR2)), RLEFT, + " multl rq0,AR\n", }, /* * DIV - division. */ /* - * word divide: divs rr0,src divides the 32-bit dividend in rr0 by the + * 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 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|SOREG|SCON, TINT, - NEEDS(NLEFT(R1), NEVER(R0), NRES(R1)), RLEFT, - " exts rr0\n divs rr0,AR\n", }, + 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|SOREG|SCON, TUNSIGNED, - NEEDS(NLEFT(R1), NEVER(R0), NRES(R1)), RLEFT, - " clr r0\n divu rr0,AR\n", }, + NEEDS(NLEFT(R1), NEVER(R0), NRES(R1), NORIGHT(R0), NORIGHT(R1)), RLEFT, + " clr r0\n div r0,AR\n", }, -/* signed long divide */ +/* + * 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|SOREG|SCON, TLONG, - NEEDS(NREG(B, 1), NSL(B), NRES(RR0)), RESC1, - " divsl AL,AR\n", }, + 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|SOREG|SCON, TULONG, - NEEDS(NREG(B, 1), NSL(B), NRES(RR0)), RESC1, - " divul AL,AR\n", }, + NEEDS(NLEFT(RR2), NEVER(RR0), NRES(RR2), NORIGHT(RR0), NORIGHT(RR2)), RLEFT, + " subl rr0,rr0\n divl rr0,AR\n", }, /* * MOD - remainder. - * divs/divu leaves the remainder in r0 (high word). We copy it down to - * r1 so the result lands in the same register that RLEFT reclaims. + * 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|SOREG|SCON, TINT, - NEEDS(NLEFT(R1), NEVER(R0), NRES(R1)), RLEFT, - " exts rr0\n divs rr0,AR\n ld r1,r0\n", }, + 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|SOREG|SCON, TUNSIGNED, - NEEDS(NLEFT(R1), NEVER(R0), NRES(R1)), RLEFT, - " clr r0\n divu rr0,AR\n ld r1,r0\n", }, + 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|SOREG|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|SOREG|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. @@ -548,7 +585,7 @@ struct optab table[] = { SBREG, TLONG|TULONG, SBREG|SNAME|SOREG|SCON, TLONG|TULONG, 0, RLEFT, - " andl AL,AR\n", }, + "ZL", }, /* * OR - bitwise or. @@ -570,7 +607,7 @@ struct optab table[] = { SBREG, TLONG|TULONG, SBREG|SNAME|SOREG|SCON, TLONG|TULONG, 0, RLEFT, - " orl AL,AR\n", }, + "ZL", }, /* * ER - bitwise exclusive or. @@ -592,7 +629,7 @@ struct optab table[] = { SBREG, TLONG|TULONG, SBREG|SNAME|SOREG|SCON, TLONG|TULONG, 0, RLEFT, - " xorl AL,AR\n", }, + "ZL", }, /* * ASSIGN - assignments. @@ -617,7 +654,7 @@ struct optab table[] = { SBREG|SNAME|SOREG, TLONG|TULONG|TPOINT, SZERO, TANY, 0, RDEST, - " clrl AL\n", }, + "ZQ", }, /* word reg <- reg */ { ASSIGN, FOREFF|INAREG|FORCC, @@ -661,19 +698,19 @@ struct optab table[] = { 0, 0, " ldl AL,AR\n", }, -/* byte/char reg <- reg or mem */ +/* byte/char reg <- reg or mem (dest reg -> low byte; src reg -> low byte) */ { ASSIGN, FOREFF|INAREG|FORCC, SAREG, TCHAR|TUCHAR, SAREG|SNAME|SOREG|SCON, TCHAR|TUCHAR, 0, RDEST|RESCC, - " ldb AL,AR\n", }, + " ldb ZG,ZJ\n", }, -/* byte mem <- reg */ +/* byte mem <- reg (src reg -> low byte) */ { ASSIGN, FOREFF|FORCC, SNAME|SOREG, TCHAR|TUCHAR, SAREG, TCHAR|TUCHAR, 0, RESCC, - " ldb AL,AR\n", }, + " ldb AL,ZJ\n", }, /* byte mem <- const */ { ASSIGN, FOREFF, @@ -682,11 +719,12 @@ struct optab table[] = { 0, 0, " ldb AL,AR\n", }, -/* struct assignment */ +/* struct assignment: block-copy via ldirb. A1 = dest-address pair, + * A2 = byte count; the source pointer (right) is a pair. */ { STASG, FOREFF|INAREG, SOREG|SNAME, TANY, - SAREG, TPTRTO|TANY, - NEEDS(NREG(B, 1), NSL(B)), RDEST, + SBREG, TPTRTO|TANY, + NEEDS(NREG(A, 1), NREG(B, 1)), RDEST, "ZS", }, /* @@ -715,33 +753,40 @@ struct optab table[] = { SANY, TANY, SOREG, TCHAR, XSL(A), RESC1, - " ldb A1,AL\n extsb A1\n", }, + " ldb ZH,AL\n extsb A1\n", }, /* load unsigned char through pair register */ { UMUL, INAREG, SANY, TANY, SOREG, TUCHAR, XSL(A), RESC1, - " ldb A1,AL\n", }, + " clr A1\n ldb ZH,AL\n", }, /* * Logical/comparison operators. * OPLOG covers EQ, NE, LE, GE, LT, GT, ULE, UGE, ULT, UGT. */ -/* compare word vs zero: use test */ +/* + * Compare vs zero. 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. + */ { OPLOG, FORCC, SAREG|SNAME|SOREG, TWORD, SZERO, TANY, 0, RESCC, - " test AL\n", }, + " cp AL,$0\n", }, -/* compare pair vs zero: use testl */ +/* compare pair vs zero */ { OPLOG, FORCC, SBREG|SNAME|SOREG, TLONG|TULONG|TPOINT, SZERO, TANY, 0, RESCC, - " testl AL\n", }, + " cpl AL,$0\n", }, /* compare word vs word */ { OPLOG, FORCC, @@ -757,12 +802,12 @@ struct optab table[] = { 0, RESCC, " cpl AL,AR\n", }, -/* compare char vs char */ +/* compare char vs char (reg operands -> low byte) */ { OPLOG, FORCC, SAREG|SNAME|SOREG, TCHAR|TUCHAR, SAREG|SCON, TCHAR|TUCHAR, 0, RESCC, - " cpb AL,AR\n", }, + " cpb ZG,ZJ\n", }, /* * GOTO - unconditional jump. @@ -799,26 +844,33 @@ struct optab table[] = { NBREG, RESC1, " ldl A1,AL\n", }, +/* char already in a register, or a char constant: copy as a word */ +{ OPLTYPE, INAREG, + SANY, TANY, + SAREG|SCON, TCHAR|TUCHAR, + NAREG, RESC1, + " ld A1,AL\n", }, + /* load signed char from mem into word register */ { OPLTYPE, INAREG, SANY, TANY, SOREG|SNAME, TCHAR, NAREG, RESC1, - " ldb A1,AL\n extsb A1\n", }, + " ldb ZH,AL\n extsb A1\n", }, /* load unsigned char from mem into word register */ { OPLTYPE, INAREG, SANY, TANY, SOREG|SNAME, TUCHAR, NAREG, RESC1, - " ldb A1,AL\n", }, + " clr A1\n ldb ZH,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", }, + " lda A1,AL\n and ZM,$32512\n", }, /* * UMINUS - unary negation. @@ -834,7 +886,7 @@ struct optab table[] = { SBREG, TLONG|TULONG, SANY, TANY, 0, RLEFT, - " negl AL\n", }, + "ZN", }, /* * COMPL - bitwise complement. @@ -850,7 +902,7 @@ struct optab table[] = { SBREG, TLONG|TULONG, SANY, TANY, 0, RLEFT, - " coml AL\n", }, + "ZC", }, /* * FUNARG - push argument onto stack before a call. @@ -905,11 +957,12 @@ struct optab table[] = { 0, RNULL, " push (rr14),$0\n", }, -/* struct argument (by-value struct copy onto stack) */ +/* struct argument (by-value struct copy onto stack) via ldirb. + * A1 = scratch dest-address pair, A2 = byte count; n_left = source pair. */ { STARG, FOREFF, SBREG, TPTRTO|TANY, SANY, TSTRUCT, - 0, 0, + NEEDS(NREG(A, 1), NREG(B, 1)), 0, "ZT", }, /* diff --git a/os/coherent/ccconfig.h b/os/coherent/ccconfig.h index 462fdf2be..87169fe81 100644 --- a/os/coherent/ccconfig.h +++ b/os/coherent/ccconfig.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Michal Pleban. + * Copyright (c) 2026 Michal Pleban. * All rights reserved. * * Redistribution and use in source and binary forms, with or without From 1e4ca7fbd739fbafeec00d5daf5c4b2b981f5963 Mon Sep 17 00:00:00 2001 From: Michal Pleban Date: Thu, 2 Jul 2026 23:23:18 +0200 Subject: [PATCH 03/67] z8001: variable-count shifts, addressing-mode legality, word alignment Three fixes, all verified on real Coherent hardware (Commodore 900): - Variable (register-count) shifts: sll/sra/srl and friends take only an immediate count, so register counts now use the dynamic shift instructions sdl/sda/sdll/sdal. Their count is signed (positive = left), so right shifts negate a copy of the count into a scratch register first. - Addressing-mode legality (from the Coherent as sources): the based mode rrN(disp) exists only for ld/ldl/ldb/lda. New special shapes SNBA (OREG a non-load insn can encode: frame X mode, or pair base with zero displacement) and SFRAME (frame only, for the synthesized long ops that touch both pair halves) replace plain SOREG in every non-load rule. Removed rules for forms the hardware lacks entirely: mem-destination add/sub/and/or/xor (dst must be a register), cpl with any memory operand, cp/cpb mem-vs-reg, and ldl mem,#imm (long constants now store through a pair register). - The Coherent ABI is word-aligned throughout (struct stat places the long st_size at offset 14), so ALLONG/ALPOINT/ALDOUBLE/ALFLOAT/ ALLONGLONG drop from 32 to 16. ALLONG=32 also broke our own calling convention for mixed int/long argument lists: the caller pushes arguments contiguously while pass 1 padded the callee's arg offsets. Co-Authored-By: Claude Fable 5 --- arch/z8001/local2.c | 21 ++- arch/z8001/macdefs.h | 37 ++++- arch/z8001/table.c | 363 +++++++++++++++++++++++++++++++++---------- 3 files changed, 328 insertions(+), 93 deletions(-) diff --git a/arch/z8001/local2.c b/arch/z8001/local2.c index 67c2f1601..450c1fa90 100644 --- a/arch/z8001/local2.c +++ b/arch/z8001/local2.c @@ -815,12 +815,29 @@ lastcall(NODE *p) } /* - * Special shape matching. Z8001 has no special address modes that need - * this, so always decline. + * 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 != 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; + } return SRNOPE; } diff --git a/arch/z8001/macdefs.h b/arch/z8001/macdefs.h index 3c0362cb0..7011bd527 100644 --- a/arch/z8001/macdefs.h +++ b/arch/z8001/macdefs.h @@ -60,17 +60,26 @@ /* * 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 32 /* 32-bit float in even register pair */ -#define ALDOUBLE 32 /* 64-bit double aligned to even pair */ -#define ALLDOUBLE 32 -#define ALLONG 32 /* long in even register pair */ -#define ALLONGLONG 32 +#define ALFLOAT 16 +#define ALDOUBLE 16 +#define ALLDOUBLE 16 +#define ALLONG 16 +#define ALLONGLONG 16 #define ALSHORT 16 -#define ALPOINT 32 /* segmented pointer in even register pair */ +#define ALPOINT 16 #define ALSTRUCT 16 /* struct: strictest member, minimum word */ #define ALSTACK 16 /* stack pointer kept word-aligned */ @@ -259,6 +268,22 @@ int COLORMAP(int c, int *r); #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. + */ +#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 */ + /* Soft-float: big-endian IEEE binary32 and binary64 */ #define USE_IEEEFP_32 #define FLT_PREFIX IEEEFP_32 diff --git a/arch/z8001/table.c b/arch/z8001/table.c index 61411b3ce..2df127a89 100644 --- a/arch/z8001/table.c +++ b/arch/z8001/table.c @@ -319,36 +319,55 @@ struct optab table[] = { /* inc word by 1 */ { PLUS, FOREFF|INAREG|FORCC, - SAREG|SOREG|SNAME, TWORD, - SONE, TANY, + 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|SOREG|SNAME, TWORD, - SMONE, TANY, + 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", }, -/* add word reg + reg */ +/* add word reg + reg/name/const (add src is R/IM/IR/DA/X - no BA) */ { PLUS, INAREG|FOREFF|FORCC, SAREG, TWORD, - SAREG|SNAME|SOREG|SCON, TWORD, + SAREG|SNAME|SCON, TWORD, 0, RLEFT|RESCC, " add AL,AR\n", }, -/* add word mem + reg/const (for side effects) */ -{ PLUS, FOREFF|FORCC, - SNAME|SOREG, TWORD, - SAREG|SCON, TWORD, +/* add word reg + encodable OREG */ +{ PLUS, INAREG|FOREFF|FORCC, + SAREG, TWORD, + SNBA, TWORD, 0, RLEFT|RESCC, " add AL,AR\n", }, /* add pair + pair (long/ptr); pointer offsets are widened to a pair */ { PLUS, INBREG|FOREFF, SBREG, TLONG|TULONG|TPOINT, - SBREG|SOREG|SNAME|SCON, 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", }, @@ -371,36 +390,54 @@ struct optab table[] = { /* dec word by 1 */ { MINUS, FOREFF|INAREG|FORCC, - SAREG|SOREG|SNAME, TWORD, - SONE, TANY, + 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|SOREG|SNAME, TWORD, - SMONE, TANY, + 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", }, -/* subtract word reg - reg */ +/* subtract word reg - reg/name/const (sub src is R/IM/IR/DA/X - no BA) */ { MINUS, INAREG|FOREFF|FORCC, SAREG, TWORD, - SAREG|SNAME|SOREG|SCON, TWORD, + SAREG|SNAME|SCON, TWORD, 0, RLEFT|RESCC, " sub AL,AR\n", }, -/* subtract word mem (side effect) */ -{ MINUS, FOREFF|FORCC, - SNAME|SOREG, TWORD, - SAREG|SCON, TWORD, +/* subtract word reg - encodable OREG */ +{ MINUS, INAREG|FOREFF|FORCC, + SAREG, TWORD, + SNBA, TWORD, 0, RLEFT|RESCC, " sub AL,AR\n", }, /* subtract pair (long/ptr); pointer offsets are widened to a pair */ { MINUS, INBREG|FOREFF, SBREG, TLONG|TULONG|TPOINT, - SBREG|SOREG|SNAME|SCON, 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", }, @@ -428,7 +465,7 @@ struct optab table[] = { */ { MUL, INAREG, SAREG, TWORD, - SAREG|SNAME|SOREG|SCON, TWORD, + SAREG|SNAME|SCON, TWORD, NEEDS(NLEFT(R1), NEVER(R0), NRES(R1), NORIGHT(R0), NORIGHT(R1)), RLEFT, " mult rr0,AR\n", }, @@ -440,7 +477,7 @@ struct optab table[] = { */ { MUL, INBREG, SBREG, TLONG|TULONG, - SBREG|SNAME|SOREG|SCON, TLONG|TULONG, + SBREG|SNAME|SCON, TLONG|TULONG, NEEDS(NLEFT(RR2), NEVER(RR0), NRES(RR2), NORIGHT(RR0), NORIGHT(RR2)), RLEFT, " multl rq0,AR\n", }, @@ -457,14 +494,14 @@ struct optab table[] = { */ { DIV, INAREG, SAREG, TINT, - SAREG|SNAME|SOREG|SCON, 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|SOREG|SCON, TUNSIGNED, + SAREG|SNAME|SCON, TUNSIGNED, NEEDS(NLEFT(R1), NEVER(R0), NRES(R1), NORIGHT(R0), NORIGHT(R1)), RLEFT, " clr r0\n div r0,AR\n", }, @@ -475,14 +512,14 @@ struct optab table[] = { */ { DIV, INBREG, SBREG, TLONG, - SBREG|SNAME|SOREG|SCON, 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|SOREG|SCON, 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", }, @@ -493,13 +530,13 @@ struct optab table[] = { */ { MOD, INAREG, SAREG, TINT, - SAREG|SNAME|SOREG|SCON, 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|SOREG|SCON, 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", }, @@ -509,13 +546,13 @@ struct optab table[] = { */ { MOD, INBREG, SBREG, TLONG, - SBREG|SNAME|SOREG|SCON, 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|SOREG|SCON, 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", }, @@ -523,67 +560,129 @@ struct optab table[] = { * Shifts. */ -/* logical shift left word */ +/* logical shift left word, constant count */ { LS, INAREG|FOREFF, SAREG, TWORD, - SAREG|SCON, TWORD, + SCON, TWORD, 0, RLEFT, " sll AL,AR\n", }, -/* arithmetic shift right (signed) */ +/* arithmetic shift right (signed), constant count */ { RS, INAREG|FOREFF, SAREG, TINT|TSHORT, - SAREG|SCON, TWORD, + SCON, TWORD, 0, RLEFT, " sra AL,AR\n", }, -/* logical shift right (unsigned) */ +/* logical shift right (unsigned), constant count */ { RS, INAREG|FOREFF, SAREG, TUNSIGNED|TUSHORT, - SAREG|SCON, TWORD, + SCON, TWORD, 0, RLEFT, " srl AL,AR\n", }, -/* shift left long pair */ +/* shift left long pair, constant count */ { LS, INBREG|FOREFF, SBREG, TLONG|TULONG, - SAREG|SCON, TWORD, + SCON, TWORD, 0, RLEFT, " slll AL,AR\n", }, -/* arithmetic shift right long pair (signed) */ +/* arithmetic shift right long pair (signed), constant count */ { RS, INBREG|FOREFF, SBREG, TLONG, - SAREG|SCON, TWORD, + SCON, TWORD, 0, RLEFT, " sral AL,AR\n", }, -/* logical shift right long pair (unsigned) */ +/* logical shift right long pair (unsigned), constant count */ { RS, INBREG|FOREFF, SBREG, TULONG, - SAREG|SCON, TWORD, + 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. */ + { AND, INAREG|FOREFF|FORCC, SAREG, TWORD, - SAREG|SNAME|SOREG|SCON, TWORD, + SAREG|SNAME|SCON, TWORD, 0, RLEFT|RESCC, " and AL,AR\n", }, -{ AND, FOREFF|FORCC, - SNAME|SOREG, TWORD, - SAREG|SCON, TWORD, +{ AND, INAREG|FOREFF|FORCC, + SAREG, TWORD, + SNBA, TWORD, 0, RLEFT|RESCC, " and AL,AR\n", }, { AND, INBREG|FOREFF, SBREG, TLONG|TULONG, - SBREG|SNAME|SOREG|SCON, TLONG|TULONG, + SBREG|SNAME|SCON, TLONG|TULONG, + 0, RLEFT, + "ZL", }, + +{ AND, INBREG|FOREFF, + SBREG, TLONG|TULONG, + SFRAME, TLONG|TULONG, 0, RLEFT, "ZL", }, @@ -593,19 +692,25 @@ struct optab table[] = { { OR, INAREG|FOREFF|FORCC, SAREG, TWORD, - SAREG|SNAME|SOREG|SCON, TWORD, + SAREG|SNAME|SCON, TWORD, 0, RLEFT|RESCC, " or AL,AR\n", }, -{ OR, FOREFF|FORCC, - SNAME|SOREG, TWORD, - SAREG|SCON, TWORD, +{ OR, INAREG|FOREFF|FORCC, + SAREG, TWORD, + SNBA, TWORD, 0, RLEFT|RESCC, " or AL,AR\n", }, { OR, INBREG|FOREFF, SBREG, TLONG|TULONG, - SBREG|SNAME|SOREG|SCON, TLONG|TULONG, + SBREG|SNAME|SCON, TLONG|TULONG, + 0, RLEFT, + "ZL", }, + +{ OR, INBREG|FOREFF, + SBREG, TLONG|TULONG, + SFRAME, TLONG|TULONG, 0, RLEFT, "ZL", }, @@ -615,19 +720,25 @@ struct optab table[] = { { ER, INAREG|FOREFF|FORCC, SAREG, TWORD, - SAREG|SNAME|SOREG|SCON, TWORD, + SAREG|SNAME|SCON, TWORD, 0, RLEFT|RESCC, " xor AL,AR\n", }, -{ ER, FOREFF|FORCC, - SNAME|SOREG, TWORD, - SAREG|SCON, TWORD, +{ ER, INAREG|FOREFF|FORCC, + SAREG, TWORD, + SNBA, TWORD, 0, RLEFT|RESCC, " xor AL,AR\n", }, { ER, INBREG|FOREFF, SBREG, TLONG|TULONG, - SBREG|SNAME|SOREG|SCON, TLONG|TULONG, + SBREG|SNAME|SCON, TLONG|TULONG, + 0, RLEFT, + "ZL", }, + +{ ER, INBREG|FOREFF, + SBREG, TLONG|TULONG, + SFRAME, TLONG|TULONG, 0, RLEFT, "ZL", }, @@ -642,20 +753,33 @@ struct optab table[] = { 0, RDEST, " clr AL\n", }, -/* word mem <- zero */ +/* word mem <- zero (clr dst is R/IR/DA/X - no BA) */ { ASSIGN, FOREFF, - SNAME|SOREG, TWORD, + SNAME, TWORD, SZERO, TANY, 0, 0, " clr AL\n", }, -/* pair <- zero (long) */ +{ ASSIGN, FOREFF, + SNBA, TWORD, + SZERO, TANY, + 0, 0, + " clr AL\n", }, + +/* pair <- zero (long); ZQ clears both halves (off and off+2), so the + * memory form is name/frame only */ { ASSIGN, FOREFF|INBREG, - SBREG|SNAME|SOREG, TLONG|TULONG|TPOINT, - SZERO, TANY, + SBREG|SNAME, TLONG|TULONG|TPOINT, + SZERO, TANY, 0, RDEST, "ZQ", }, +{ ASSIGN, FOREFF, + SFRAME, TLONG|TULONG|TPOINT, + SZERO, TANY, + 0, 0, + "ZQ", }, + /* word reg <- reg */ { ASSIGN, FOREFF|INAREG|FORCC, SAREG, TWORD, @@ -670,9 +794,16 @@ struct optab table[] = { 0, RESCC, " ld AL,AR\n", }, -/* word mem <- const */ +/* 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|SOREG, TWORD, + SNAME, TWORD, + SCON, TANY, + 0, 0, + " ld AL,AR\n", }, + +{ ASSIGN, FOREFF, + SNBA, TWORD, SCON, TANY, 0, 0, " ld AL,AR\n", }, @@ -691,12 +822,10 @@ struct optab table[] = { 0, 0, " ldl AL,AR\n", }, -/* pair mem <- const long */ -{ ASSIGN, FOREFF, - SNAME|SOREG, TLONG|TULONG, - SCON, TANY, - 0, 0, - " ldl AL,AR\n", }, +/* 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/char reg <- reg or mem (dest reg -> low byte; src reg -> low byte) */ { ASSIGN, FOREFF|INAREG|FORCC, @@ -712,9 +841,15 @@ struct optab table[] = { 0, RESCC, " ldb AL,ZJ\n", }, -/* byte mem <- const */ +/* byte mem <- const (ldb dst,#imm takes IR/DA/X - no BA) */ { ASSIGN, FOREFF, - SNAME|SOREG, TCHAR|TUCHAR, + SNAME, TCHAR|TUCHAR, + SCON, TANY, + 0, 0, + " ldb AL,AR\n", }, + +{ ASSIGN, FOREFF, + SNBA, TCHAR|TUCHAR, SCON, TANY, 0, 0, " ldb AL,AR\n", }, @@ -775,40 +910,98 @@ struct optab table[] = { * (e.g. "0 < 0" wrongly taken). cp/cpl set S, V, C and Z correctly for all * signed and unsigned conditions. */ +/* + * 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". + */ + +/* compare word vs zero */ +{ OPLOG, FORCC, + SAREG|SNAME, TWORD, + SZERO, TANY, + 0, RESCC, + " cp AL,$0\n", }, + { OPLOG, FORCC, - SAREG|SNAME|SOREG, TWORD, + SNBA, TWORD, SZERO, TANY, 0, RESCC, " cp AL,$0\n", }, -/* compare pair vs zero */ +/* compare pair vs zero: register only (no cpl mem,#imm form exists) */ { OPLOG, FORCC, - SBREG|SNAME|SOREG, TLONG|TULONG|TPOINT, + SBREG, TLONG|TULONG|TPOINT, SZERO, TANY, 0, RESCC, " cpl AL,$0\n", }, -/* compare word vs word */ +/* compare word reg vs reg/name/const */ +{ OPLOG, FORCC, + SAREG, TWORD, + SAREG|SNAME|SCON, TWORD, + 0, RESCC, + " cp AL,AR\n", }, + { OPLOG, FORCC, - SAREG|SNAME|SOREG, TWORD, - SAREG|SCON, TWORD, + SAREG, TWORD, + SNBA, TWORD, 0, RESCC, " cp AL,AR\n", }, -/* compare pair vs pair (long/ptr) */ +/* compare word mem vs const (immediate-compare form) */ { OPLOG, FORCC, - SBREG|SNAME|SOREG, TLONG|TULONG|TPOINT, - SBREG|SCON, TLONG|TULONG|TPOINT, + 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 (reg operands -> low byte) */ { OPLOG, FORCC, - SAREG|SNAME|SOREG, TCHAR|TUCHAR, - SAREG|SCON, TCHAR|TUCHAR, + SAREG, TCHAR|TUCHAR, + SAREG|SNAME|SCON, TCHAR|TUCHAR, 0, RESCC, " cpb ZG,ZJ\n", }, +{ OPLOG, FORCC, + SAREG, TCHAR|TUCHAR, + SNBA, TCHAR|TUCHAR, + 0, RESCC, + " cpb ZG,ZJ\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", }, + /* * GOTO - unconditional jump. */ From 9770c8d33e9594e2ccab0ea969fc7de0133302a6 Mon Sep 17 00:00:00 2001 From: Michal Pleban Date: Fri, 3 Jul 2026 23:00:24 +0200 Subject: [PATCH 04/67] z8001: struct return by value, byte-register constraints, bitfield fixes All verified on real Coherent hardware (Commodore 900): strret 23/23, chartest 8/8, edgetest 40/40. Struct/union return by value uses a hidden-pointer ABI (invented here; the native Coherent K&R compiler has no struct-by-value): - The caller reserves a buffer in its frame for EACH struct-returning call (offsets assigned in myreader() and carried on the call node via the previously unused ATTR_P2_TARGET pass2-attribute hook) and pushes its address last, so it lands in the first-argument slot. Distinct per-call buffers are required: pass 2 pre-evaluates call-containing arguments into pointer temps before pushing anything, so with f(g(), h()) both results are live at once. - The callee (bfcode) shifts the declared argument offsets one pointer up and saves the hidden pointer in a temp; clocal(FORCE) returns the value's address in rr0; efcode copies the value through the hidden pointer and returns that pointer (the caller's live buffer) in rr0. - Call results can coalesce with the precolored rr0, bypassing the clregs mask, and rr0 cannot be a memory base in any mode: NEVER(RR0) on all pair-result call rules forces the result out of rr0 right after every call. Removed stale STCALL rules that predate the ABI (they pushed no hidden pointer at all). Byte-register constraints: byte instructions can only name the halves of r0-r7, so BYTEL/BYTER/BYTET needs (NOLEFT/NORIGHT/NEVER of r8-r12) confine char values at every ldb/cpb site; under pressure they now spill instead of hitting the blput() comperr. Unsigned char loads are rewritten read-first (ldb then "and $0xff") - the old clr-first form could clobber its own address base. Bitfields: clocal's same-size SCONV folds no longer retype a FLD child. A FLD's signedness picks sra vs srl in the generic rmfldops lowering, so folding stref's SCONV(INT) wrapper made unsigned fields read back sign-extended. Also: memory-destination assignments gained INAREG/INBREG+RDEST forms (e.g. "return ++staticvar" previously could not match), and member access directly on a call result (f().y) restores the struct size descriptor that pprop loses, in myp2tree. Co-Authored-By: Claude Fable 5 --- arch/z8001/code.c | 89 ++++++++++++++++++++--------- arch/z8001/local.c | 38 ++++++++++++- arch/z8001/local2.c | 101 +++++++++++++++++++++++++++++---- arch/z8001/macdefs.h | 16 +++++- arch/z8001/table.c | 130 ++++++++++++++++++++++++++++++------------- 5 files changed, 299 insertions(+), 75 deletions(-) diff --git a/arch/z8001/code.c b/arch/z8001/code.c index 4eefcd828..5a57ef540 100644 --- a/arch/z8001/code.c +++ b/arch/z8001/code.c @@ -105,51 +105,90 @@ defloc(struct symtab *sp) } /* - * End-of-function code. - * For struct/union return, Coherent uses a hidden first argument: - * the caller passes a pointer to the result area, and the callee - * copies the return value there, then returns the pointer in rr0. - * PCC handles the caller side automatically; here we just need to - * copy the local struct to the hidden pointer and load rr0 with it. + * 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)); + } - /* - * The hidden pointer arrives as the first argument. - * Build: *hidden_ptr = return_value; rr0 = hidden_ptr. - */ - q = block(REG, NIL, NIL, INCREF(cftnsp->stype - FTN), - cftnsp->sdf, cftnsp->sap); - slval(q, 0); - regno(q) = RR0; - - p = buildtree(UMUL, tcopy(q), NIL); - p = buildtree(ASSIGN, p, - block(REG, NIL, NIL, cftnsp->stype - FTN, - cftnsp->sdf, cftnsp->sap)); - ecomp(p); - - /* return the pointer in rr0 (already there from the hidden arg) */ + /* 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 there - * is nothing to do here unless we are optimising args into temps. + * 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; + 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)); + } + if (xtemps == 0) return; diff --git a/arch/z8001/local.c b/arch/z8001/local.c index 5fb34f7b9..a46fe9230 100644 --- a/arch/z8001/local.c +++ b/arch/z8001/local.c @@ -149,6 +149,17 @@ clocal(NODE *p) 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 || @@ -170,8 +181,20 @@ clocal(NODE *p) case FORCE: /* * FORCE ensures the return value is in the correct register. - * On Z8001: int/short in R1, long/ptr in RR0. + * 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); @@ -250,6 +273,19 @@ 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; diff --git a/arch/z8001/local2.c b/arch/z8001/local2.c index 450c1fa90..526e5b492 100644 --- a/arch/z8001/local2.c +++ b/arch/z8001/local2.c @@ -437,16 +437,36 @@ zzzcode(NODE *p, int c) case 'S': /* struct assignment: ldirb (dest),(src),count */ { struct attr *ap = attr_find(p->n_ap, ATTR_P2STRUCT); + NODE *l = p->n_left; int sz = ap->iarg(0); /* 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. */ - expand(p, FOREFF, "\tlda\tA2,AL\n"); - printf("\tand\t"); - prword(getlr(p, '2'), 0); - printf(",$32512\n"); + * 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); @@ -454,6 +474,22 @@ zzzcode(NODE *p, int c) } 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 'T': /* struct argument passed by value. * * Reserve the argument slot at the top of stack with @@ -806,11 +842,14 @@ lastcall(NODE *p) if (p->n_op != CALL && p->n_op != FORTCALL && p->n_op != STCALL && p->n_op != UCALL && p->n_op != USTCALL) return; - if (p->n_right == NIL) - return; /* no arguments */ - for (p = p->n_right; p->n_op == CM; p = p->n_left) - size += argsiz(p->n_right); - size += argsiz(p); + /* 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; } @@ -873,9 +912,51 @@ myoptim(struct interpass *ip) { } +/* + * 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); +} + void myreader(struct interpass *ip) { + struct interpass *ip2; + int off = p2autooff; + + DLIST_FOREACH(ip2, ip, qelem) { + if (ip2->type != IP_NODE) + continue; + walkf(ip2->ip_node, findstcall, &off); + } + if (off > p2autooff) { + p2autooff = off; + if (p2autooff > p2maxautooff) + p2maxautooff = p2autooff; + } } void diff --git a/arch/z8001/macdefs.h b/arch/z8001/macdefs.h index 7011bd527..6e2359670 100644 --- a/arch/z8001/macdefs.h +++ b/arch/z8001/macdefs.h @@ -146,10 +146,14 @@ typedef long long OFFSZ; * long/float/ptr -> 2 (register pair) * double/longlong -> 4 (register quad) */ +/* 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) ? 2 : 1) + (t) == FLOAT || ISPTR(t) || \ + (t) == STRTY || (t) == UNIONTY ? 2 : 1) /* * Register definitions. @@ -276,6 +280,16 @@ int COLORMAP(int c, int *r); * 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) */ diff --git a/arch/z8001/table.c b/arch/z8001/table.c index 2df127a89..42c8e33b8 100644 --- a/arch/z8001/table.c +++ b/arch/z8001/table.c @@ -60,6 +60,21 @@ #define XSL(c) NEEDS(NREG(c, 1), NSL(c)) #define XSR(c) NEEDS(NREG(c, 1), NSR(c)) +/* + * Byte-register constraints. Z8001 byte instructions can only name the + * halves of r0-r7 (rh0-rh7/rl0-rl7); r8-r12 have no byte-addressable + * halves. Word instructions (extsb, and, push, ...) operate on any + * register, so a char value must sit in r0-r7 only at instructions that + * PRINT a byte register name (ldb/cpb via the ZG/ZH/ZJ escapes). + * BYTEL/BYTER constrain the left/right operand's live range at such a + * use; BYTET keeps the rule's own temp (a ZH destination) out of + * r8-r12. Values under pressure spill instead of misallocating; + * blput()'s comperr remains as a backstop. + */ +#define BYTEL NOLEFT(R8), NOLEFT(R9), NOLEFT(R10), NOLEFT(R11), NOLEFT(R12) +#define BYTER NORIGHT(R8), NORIGHT(R9), NORIGHT(R10), NORIGHT(R11), NORIGHT(R12) +#define BYTET NEVER(R8), NEVER(R9), NEVER(R10), NEVER(R11), NEVER(R12) + struct optab table[] = { /* First entry must be an empty entry */ @@ -168,15 +183,15 @@ struct optab table[] = { { SCONV, INAREG, SOREG|SNAME, TCHAR, SANY, TINT|TUNSIGNED, - NAREG, RESC1, + NEEDS(NREG(A, 1), BYTET), RESC1, " ldb ZH,AL\n extsb A1\n", }, /* unsigned char from memory -> (u)int */ { SCONV, INAREG, SOREG|SNAME, TUCHAR, SANY, TINT|TUNSIGNED, - NAREG, RESC1, - " clr A1\n ldb ZH,AL\n", }, + NEEDS(NREG(A, 1), BYTET), RESC1, + " ldb ZH,AL\n and A1,$0xff\n", }, /* char -> long */ { SCONV, INBREG, @@ -240,28 +255,60 @@ struct optab table[] = { { CALL, INBREG, SCON, TANY, SANY, TLONG|TULONG|TPOINT, - XSL(B), RESC1, + NEEDS(NREG(B, 1), NSL(B), NEVER(RR0)), RESC1, " calr CL\nZB", }, { UCALL, INBREG, SCON, TANY, SANY, TLONG|TULONG|TPOINT, - XSL(B), RESC1, + NEEDS(NREG(B, 1), NSL(B), NEVER(RR0)), RESC1, " calr CL\n", }, /* Indirect call (function pointer in pair), long/ptr result */ { CALL, INBREG, SBREG, TANY, SANY, TLONG|TULONG|TPOINT, - XSL(B), RESC1, + NEEDS(NREG(B, 1), NSL(B), NEVER(RR0)), RESC1, " call (AL)\nZB", }, { UCALL, INBREG, SBREG, TANY, SANY, TLONG|TULONG|TPOINT, - XSL(B), RESC1, + NEEDS(NREG(B, 1), NSL(B), NEVER(RR0)), 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, @@ -288,30 +335,32 @@ struct optab table[] = { 0, 0, " call (AL)\n", }, -/* Struct call (result by hidden pointer) */ -{ STCALL, INAREG, +/* 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, - XSL(A), RESC1, - " calr CL\nZB", }, + 0, 0, + "ZR calr CL\nZB", }, -{ USTCALL, INAREG, +{ USTCALL, FOREFF, SCON, TANY, SANY, TANY, - XSL(A), RESC1, - " calr CL\n", }, + 0, 0, + "ZR calr CL\nZB", }, { STCALL, FOREFF, - SCON, TANY, + SBREG, TANY, SANY, TANY, 0, 0, - " calr CL\nZB", }, + "ZR call (AL)\nZB", }, { USTCALL, FOREFF, - SCON, TANY, + SBREG, TANY, SANY, TANY, 0, 0, - " calr CL\n", }, + "ZR call (AL)\nZB", }, /* * PLUS - integer addition. @@ -787,11 +836,11 @@ struct optab table[] = { 0, RDEST|RESCC, " ld AL,AR\n", }, -/* word mem <- reg */ -{ ASSIGN, FOREFF|FORCC, +/* word mem <- reg (INAREG: the stored value stays available in AR) */ +{ ASSIGN, FOREFF|INAREG|FORCC, SNAME|SOREG, TWORD, SAREG, TWORD, - 0, RESCC, + 0, RDEST|RESCC, " ld AL,AR\n", }, /* word mem <- const (ld dst,#imm takes IR/DA/X - no BA; a BA dest falls @@ -816,10 +865,10 @@ struct optab table[] = { " ldl AL,AR\n", }, /* pair mem <- pair reg (long/ptr) */ -{ ASSIGN, FOREFF, +{ ASSIGN, FOREFF|INBREG, SNAME|SOREG, TLONG|TULONG|TPOINT, SBREG, TLONG|TULONG|TPOINT, - 0, 0, + 0, RDEST, " ldl AL,AR\n", }, /* pair mem <- const long: there is NO direct rule on purpose. The @@ -831,14 +880,14 @@ struct optab table[] = { { ASSIGN, FOREFF|INAREG|FORCC, SAREG, TCHAR|TUCHAR, SAREG|SNAME|SOREG|SCON, TCHAR|TUCHAR, - 0, RDEST|RESCC, + NEEDS(BYTEL, BYTER), RDEST|RESCC, " ldb ZG,ZJ\n", }, /* byte mem <- reg (src reg -> low byte) */ -{ ASSIGN, FOREFF|FORCC, +{ ASSIGN, FOREFF|INAREG|FORCC, SNAME|SOREG, TCHAR|TUCHAR, SAREG, TCHAR|TUCHAR, - 0, RESCC, + NEEDS(BYTER), RDEST|RESCC, " ldb AL,ZJ\n", }, /* byte mem <- const (ldb dst,#imm takes IR/DA/X - no BA) */ @@ -855,11 +904,15 @@ struct optab table[] = { " ldb AL,AR\n", }, /* struct assignment: block-copy via ldirb. A1 = dest-address pair, - * A2 = byte count; the source pointer (right) is a 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)), RDEST, + NEEDS(NREG(A, 1), NREG(B, 1), NORIGHT(RR0)), RDEST, "ZS", }, /* @@ -887,15 +940,15 @@ struct optab table[] = { { UMUL, INAREG, SANY, TANY, SOREG, TCHAR, - XSL(A), RESC1, + NEEDS(NREG(A, 1), NSL(A), BYTET), RESC1, " ldb ZH,AL\n extsb A1\n", }, /* load unsigned char through pair register */ { UMUL, INAREG, SANY, TANY, SOREG, TUCHAR, - XSL(A), RESC1, - " clr A1\n ldb ZH,AL\n", }, + NEEDS(NREG(A, 1), NSL(A), BYTET), RESC1, + " ldb ZH,AL\n and A1,$0xff\n", }, /* * Logical/comparison operators. @@ -980,13 +1033,13 @@ struct optab table[] = { { OPLOG, FORCC, SAREG, TCHAR|TUCHAR, SAREG|SNAME|SCON, TCHAR|TUCHAR, - 0, RESCC, + NEEDS(BYTEL, BYTER), RESCC, " cpb ZG,ZJ\n", }, { OPLOG, FORCC, SAREG, TCHAR|TUCHAR, SNBA, TCHAR|TUCHAR, - 0, RESCC, + NEEDS(BYTEL), RESCC, " cpb ZG,ZJ\n", }, /* compare char mem vs const (immediate-compare form) */ @@ -1048,15 +1101,15 @@ struct optab table[] = { { OPLTYPE, INAREG, SANY, TANY, SOREG|SNAME, TCHAR, - NAREG, RESC1, + NEEDS(NREG(A, 1), BYTET), RESC1, " ldb ZH,AL\n extsb A1\n", }, /* load unsigned char from mem into word register */ { OPLTYPE, INAREG, SANY, TANY, SOREG|SNAME, TUCHAR, - NAREG, RESC1, - " clr A1\n ldb ZH,AL\n", }, + NEEDS(NREG(A, 1), BYTET), RESC1, + " ldb ZH,AL\n and A1,$0xff\n", }, /* load address (lda) of SOREG/SNAME into pair register */ { OPLTYPE, INBREG, @@ -1151,11 +1204,12 @@ struct optab table[] = { " 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. */ + * 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)), 0, + NEEDS(NREG(A, 1), NREG(B, 1), NOLEFT(RR0)), 0, "ZT", }, /* From 3abf79be26501ed9725b5ff0ecfa4cf26c4b84c5 Mon Sep 17 00:00:00 2001 From: Michal Pleban Date: Fri, 3 Jul 2026 23:00:25 +0200 Subject: [PATCH 05/67] cxxcom: fix out-of-bounds read in builtin_nanx The NaN patterns (nLDOUBLE et al.) hold 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 - so copying sizeof(long double) reads past the array. Bound the copy like the VALX macro already does, via a zero-filled temporary. Found via -Warray-bounds while cross-building for z8001-coherent. Co-Authored-By: Claude Fable 5 --- cc/cxxcom/builtins.c | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) 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; From bc6b56265e0e101aa5a88d3b6a36d755af32968e Mon Sep 17 00:00:00 2001 From: Michal Pleban Date: Sat, 4 Jul 2026 02:49:06 +0200 Subject: [PATCH 06/67] z8001: software floating point (float/double, native Coherent FP runtime) The Z8001 in the Commodore 900 has no FPU; lower all floating point to the libc soft-FP runtime, following the native compiler's convention exactly (discovered from libc/crt/{dadd,dcmp,dmul,ddiv,dtoi,itod,dtof, ftod}.s and the compiler-emitted call sites in factor.s/modf.s): - a double travels in rq0 (r0 = sign+exponent, big-endian) and is pushed "pushl rr2; pushl rr0" (8 bytes, caller pops) - dradd/drsub/drmul/drdiv(da,db): both by value, result in rq0 - drcmp(da,db): three-way result in r1, then compared against 0 - diflt/duflt/dlflt/dvflt: int/unsigned/long/ulong -> double - ifix/ufix -> r1, lfix/vfix -> rr0: double -> integer - dfpack/fdpack: float <-> double, register-based (rr0/rq0) - negation inline: "xor ,$-32768" (sign-bit flip) - float is raw bits in a pair; float arithmetic computed in double Implementation: - new register class C: quads rq0/rq4/rq8 (i386 pairs-of-pairs pattern); RETREG(double)=rq0, 3-class COLORMAP, quad rmove, prologue saves for quad-clobbered callee-saved words - fixfloatops() in myreader() (m68k fixcalls pattern) rewrites FP arithmetic, comparisons, and int<->fp SCONVs into helper CALLs; float ops convert operands to double and round the result back - table: dfpack/fdpack SCONV rules (NLEFT/NRES pin rr0/rq0), quad CALL/ASSIGN/OPLTYPE/UMUL/FUNARG/UMINUS rules; TFLOAT added to the pair rules; zzzcode ZD/ZE/ZP/ZW + quadmem() (ldm for frame/name memory - ldm has no BA mode - and two ldl for pair bases, loading the base pair's own half last since OREG bases have no regw) - ninval() emits FP constants as big-endian .word pairs (the generic inval assumes SZINT-sized soft_toush chunks; they are uint32_t) - emit ".globl _dtoa_" from ejobcode() when a unit uses FP so the linker pulls libc's real _dtefg printf formatter instead of the "No floating point!" dummy in sdtoa.o Fixes found along the way: - myp2tree's FCON symtab left the real sap field as arena garbage (the "#define sap sss" compat macro hijacked the initializer); locctr's attr_find then chased a wild pointer. memset the struct. - SCONV widening rules only listed same-signedness results; e.g. "u == 40000" (a long compare on 16-bit int) had no unsigned->long rule. Result masks broadened to TLNG on all six. - PCONV word->pointer was backwards ("ld A1,AL; clr U1" put the value in the segment word and cleared the offset); zero-extend templates printed the pair name in clr (worked only because as encodes "clr rr2" as r2). Both now name the word explicitly (ZM). Hardware-verified on the Commodore 900: ztests/floattest.c, all 65 tests pass plus printf %f. Co-Authored-By: Claude Fable 5 --- arch/z8001/code.c | 8 + arch/z8001/local.c | 53 +++++- arch/z8001/local2.c | 403 +++++++++++++++++++++++++++++++++++++++++-- arch/z8001/macdefs.h | 74 +++++--- arch/z8001/table.c | 215 ++++++++++++++++++----- 5 files changed, 670 insertions(+), 83 deletions(-) diff --git a/arch/z8001/code.c b/arch/z8001/code.c index 5a57ef540..dd34b7452 100644 --- a/arch/z8001/code.c +++ b/arch/z8001/code.c @@ -207,10 +207,18 @@ bfcode(struct symtab **sp, int cnt) /* * Called just before compiler exits. + * If the unit used floating point, emit an undefined reference to + * _dtoa_ (defined in libc's crt/dtefg.o next to the real _dtefg + * printf formatter) so the linker picks the real FP formatter over + * the "No floating point!" dummy in gen/sdtoa.o (see zfpused). */ void ejobcode(int flag) { + extern int zfpused; + + if (zfpused) + printf("\t.globl\t_dtoa_\n"); } /* diff --git a/arch/z8001/local.c b/arch/z8001/local.c index a46fe9230..965d73598 100644 --- a/arch/z8001/local.c +++ b/arch/z8001/local.c @@ -57,6 +57,16 @@ * - Locals: negative OREG offsets from R13 (AUTOINIT=0) * - Params: positive OREG offsets from R13 (ARGINIT=32 bits = 4 bytes) */ +/* + * Set when the compilation unit uses any floating point. Coherent's + * libc has two definitions of the printf FP formatter _dtefg: the real + * one in crt/dtefg.o (which also defines _dtoa/ecvt/fcvt) and a dummy + * in gen/sdtoa.o that aborts with "No floating point!" so non-FP + * programs stay small. ejobcode() emits an undefined reference to + * _dtoa_ so the linker pulls the real module in first. + */ +int zfpused; + NODE * clocal(NODE *p) { @@ -65,6 +75,10 @@ clocal(NODE *p) int o; TWORD m; + m = p->n_type; + if (m == FLOAT || m == DOUBLE || m == LDOUBLE) + zfpused = 1; + switch (o = p->n_op) { case NAME: @@ -237,7 +251,9 @@ exname(char *p) * * On Z8001: char, short, int fit in a word register (SAREG). * long, float, and pointers fit in a pair register (SBREG). - * double and long long require 4 word registers — not supported. + * 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) @@ -257,11 +273,33 @@ cisreg(TWORD t) /* * 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) { - return 0; /* let generic inval() handle all types */ +#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 } /* @@ -291,14 +329,19 @@ myp2tree(NODE *p) 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->sap = 0; sp->slevel = 1; /* fake numeric label */ sp->soffset = getlab(); - sp->sflags = 0; sp->stype = p->n_type; sp->squal = (CON >> TSHIFT); - sp->sname = NULL; locctr(DATA, sp); defloc(sp); diff --git a/arch/z8001/local2.c b/arch/z8001/local2.c index 526e5b492..4412c7c16 100644 --- a/arch/z8001/local2.c +++ b/arch/z8001/local2.c @@ -68,16 +68,36 @@ void mygenregs(struct interpass *ip); /* * Register names indexed by PCC register number. - * r0-r15 are 16-bit word registers; rr0,rr2,...,rr10 are 32-bit pairs. - * Indices: 0-15 = r0-r15, 16=rr0, 17=rr2, 18=rr4, 19=rr6, 20=rr8, 21=rr10. + * r0-r15 are 16-bit word registers; rr0,rr2,...,rr10 are 32-bit pairs; + * rq0,rq4,rq8 are 64-bit quads (doubles). + * Indices: 0-15 = r0-r15, 16=rr0 .. 21=rr10, 22=rq0, 23=rq4, 24=rq8. */ char *rnames[] = { "r0", "r1", "r2", "r3", "r4", "r5", "r6", "r7", "r8", "r9", "r10", "r11", "r12", "r13", "r14", "r15", - "rr0", "rr2", "rr4", "rr6", "rr8", "rr10" + "rr0", "rr2", "rr4", "rr6", "rr8", "rr10", + "rq0", "rq4", "rq8" }; +/* + * 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)]; +} + void deflab(int label) { @@ -127,6 +147,12 @@ firstsavereg(void) 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; return firstsave; } @@ -319,6 +345,81 @@ prword(NODE *p, int lo) } } +/* + * 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. * @@ -331,6 +432,10 @@ prword(NODE *p, int lo) * ZM high (segment) word of result pair, for the post-lda segment mask * 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 */ void zzzcode(NODE *p, int c) @@ -490,6 +595,78 @@ zzzcode(NODE *p, int c) } break; + case 'D': /* double assignment: left <- right, one side a quad */ + { + NODE *l = p->n_left, *r = p->n_right; + + 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 */ + { + NODE *l = getlr(p, 'L'); + int q = getlr(p, '1')->n_rval; + + 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. */ + { + NODE *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") */ + { + NODE *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 @@ -760,7 +937,11 @@ rmove(int s, int d, TWORD t) { if (s == d) return; - if (szty(t) == 2) { + 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 { @@ -779,7 +960,9 @@ rmove(int s, int d, TWORD t) * 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 (rr0,rr2,rr4,rr6,rr8,rr10); each pair overlaps two word registers. + * 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. Worst-case (conservative) blocking counts. */ int COLORMAP(int c, int *r) @@ -788,26 +971,32 @@ COLORMAP(int c, int *r) switch (c) { case CLASSA: - /* each interfering pair blocks two word registers */ - num = r[CLASSA] + 2 * r[CLASSB]; + /* a pair blocks 2 words, a quad blocks 4 */ + num = r[CLASSA] + 2 * r[CLASSB] + 4 * r[CLASSC]; return num < 13; case CLASSB: - /* 5 allocatable pairs (rr2,rr4,rr6,rr8,rr10; rr0 reserved) */ - num = r[CLASSB] + r[CLASSA]; + /* 5 allocatable pairs (rr2..rr10; rr0 reserved); + * a word or pair neighbour blocks 1 pair, a quad blocks 2 */ + num = r[CLASSB] + r[CLASSA] + 2 * r[CLASSC]; return num < 5; + case CLASSC: + /* any neighbour blocks at most 1 of the 3 quads */ + num = r[CLASSA] + r[CLASSB] + r[CLASSC]; + return num < 3; } return 0; } /* * Return the register class suitable for a value of type t. - * Consistent with PCLASS in macdefs.h: 32-bit values (long/ptr/float) - * use class B (pairs), everything else uses class A (words). + * Consistent with PCLASS in macdefs.h: 64-bit values (double) use class + * C (quads), 32-bit values (long/ptr/float) class B (pairs), everything + * else class A (words). */ int gclass(TWORD t) { - return szty(t) == 2 ? CLASSB : CLASSA; + return szty(t) == 4 ? CLASSC : szty(t) == 2 ? CLASSB : CLASSA; } /* @@ -912,6 +1101,195 @@ myoptim(struct interpass *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 @@ -950,6 +1328,7 @@ myreader(struct interpass *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) { diff --git a/arch/z8001/macdefs.h b/arch/z8001/macdefs.h index 6e2359670..6bd3d0aca 100644 --- a/arch/z8001/macdefs.h +++ b/arch/z8001/macdefs.h @@ -145,6 +145,11 @@ typedef long long OFFSZ; * 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 @@ -165,6 +170,8 @@ typedef long long OFFSZ; * 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) */ #define R0 0 /* scratch, low half of rr0, return word (int) */ #define R1 1 /* scratch, high half of rr0, return value (int) */ @@ -190,7 +197,11 @@ typedef long long OFFSZ; #define RR8 20 /* r8:r9 - callee-saved pair */ #define RR10 21 /* r10:r11 - callee-saved pair */ -#define MAXREGS 22 +#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 MAXREGS 25 /* * RSTATUS: register class membership and caller/callee-saved flags. @@ -219,43 +230,51 @@ typedef long long OFFSZ; SBREG|TEMPREG, /* 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 */ + SBREG, /* rr10 - preserved via its words r10/r11 */ \ + SCREG|TEMPREG, /* rq0 - caller-saved, double RETREG */ \ + SCREG|TEMPREG, /* rq4 - r6/r7 half saved by prologue when used */ \ + SCREG, /* rq8 - preserved via its words r8-r11 */ /* * ROVERLAP: which other registers each register aliases. - * A pair RRn aliases the two word registers Rn and R(n+1). + * A pair RRn aliases the two word registers Rn and R(n+1); a quad RQn + * aliases four words and two pairs. */ #define ROVERLAP \ - { RR0, -1 }, /* r0 */ \ - { RR0, -1 }, /* r1 */ \ - { RR2, -1 }, /* r2 */ \ - { RR2, -1 }, /* r3 */ \ - { RR4, -1 }, /* r4 */ \ - { RR4, -1 }, /* r5 */ \ - { RR6, -1 }, /* r6 */ \ - { RR6, -1 }, /* r7 */ \ - { RR8, -1 }, /* r8 */ \ - { RR8, -1 }, /* r9 */ \ - { RR10, -1 }, /* r10 */ \ - { RR10, -1 }, /* r11 */ \ + { RR0, RQ0, -1 }, /* r0 */ \ + { RR0, RQ0, -1 }, /* r1 */ \ + { RR2, RQ0, -1 }, /* r2 */ \ + { RR2, RQ0, -1 }, /* r3 */ \ + { RR4, RQ4, -1 }, /* r4 */ \ + { RR4, RQ4, -1 }, /* r5 */ \ + { RR6, RQ4, -1 }, /* r6 */ \ + { 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, -1 }, /* rr0 */ \ - { R2, R3, -1 }, /* rr2 */ \ - { R4, R5, -1 }, /* rr4 */ \ - { R6, R7, -1 }, /* rr6 */ \ - { R8, R9, -1 }, /* rr8 */ \ - { R10, R11, -1 }, /* rr10 */ + { R0, R1, RQ0, -1 }, /* rr0 */ \ + { R2, R3, RQ0, -1 }, /* rr2 */ \ + { R4, R5, RQ4, -1 }, /* rr4 */ \ + { R6, R7, RQ4, -1 }, /* rr6 */ \ + { R8, R9, RQ8, -1 }, /* rr8 */ \ + { R10, R11, RQ8, -1 }, /* rr10 */ \ + { R0, R1, R2, R3, RR0, RR2, -1 }, /* rq0 */ \ + { R4, R5, R6, R7, RR4, RR6, -1 }, /* rq4 */ \ + { R8, R9, R10, R11, RR8, RR10, -1 }, /* rq8 */ /* Return the register class for a node's type */ -#define PCLASS(p) (szty((p)->n_type) == 2 ? SBREG : SAREG) +#define PCLASS(p) (szty((p)->n_type) == 4 ? SCREG : \ + szty((p)->n_type) == 2 ? SBREG : SAREG) -#define NUMCLASS 2 /* two register classes: A (word) and B (pair) */ +#define NUMCLASS 3 /* classes: A (word), B (pair), C (quad) */ int COLORMAP(int c, int *r); -#define GCLASS(x) ((x) < 16 ? CLASSA : CLASSB) +#define GCLASS(x) ((x) < 16 ? CLASSA : (x) < 22 ? CLASSB : CLASSC) #define DECRA(x,y) (((x) >> ((y)*5)) & 31) /* decode register from n_reg */ #define ENCRD(x) (x) /* encode dest register */ #define ENCRA1(x) ((x) << 5) @@ -263,11 +282,14 @@ int COLORMAP(int c, int *r); #define ENCRA(x,y) ((x) << (5+(y)*5)) /* encode source register */ /* - * Return register for each type: + * 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): * int/short/char -> r1 * long/ptr/float -> rr0 (r0:r1) + * double -> rq0 (r0-r3) */ -#define RETREG(x) (szty(x) == 2 ? RR0 : R1) +#define RETREG(x) (szty(x) == 4 ? RQ0 : szty(x) == 2 ? RR0 : R1) #define FPREG R13 /* frame pointer */ #define STKREG R15 /* stack pointer (low word of rr14) */ diff --git a/arch/z8001/table.c b/arch/z8001/table.c index 42c8e33b8..4610bb30b 100644 --- a/arch/z8001/table.c +++ b/arch/z8001/table.c @@ -30,10 +30,11 @@ * 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) * * Key calling convention facts: * - Pure stack, right-to-left push, caller cleans - * - int result in r1; long/ptr result in rr0 (r0:r1) + * - 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 * @@ -53,6 +54,8 @@ /* 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)) @@ -98,12 +101,16 @@ struct optab table[] = { XSL(B), RESC1, " ldl A1,AL\n", }, -/* word reg to pointer pair (zero-extend) */ +/* 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 A1,AL\n clr U1\n", }, + " ld U1,AL\n clr ZM\n", }, /* * SCONVs - scalar type conversions. @@ -130,33 +137,34 @@ struct optab table[] = { 0, RLEFT, "", }, -/* int -> long: sign-extend word into pair */ +/* 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, TLONG, + SANY, TLNG, XSL(B), RESC1, " ld U1,AL\n exts A1\n", }, -/* unsigned int -> unsigned long: zero-extend word into pair */ +/* unsigned int -> (u)long: zero-extend word into pair */ { SCONV, INBREG, SAREG, TUNSIGNED, - SANY, TULONG, + SANY, TLNG, XSL(B), RESC1, - " ld U1,AL\n clr A1\n", }, + " ld U1,AL\n clr ZM\n", }, -/* int/short from memory -> long */ +/* int/short from memory -> (u)long */ { SCONV, INBREG, SOREG|SNAME, TINT|TSHORT, - SANY, TLONG, + SANY, TLNG, NBREG, RESC1, " ld U1,AL\n exts A1\n", }, -/* unsigned/ushort from memory -> unsigned long */ +/* unsigned/ushort from memory -> (u)long */ { SCONV, INBREG, SOREG|SNAME, TUNSIGNED|TUSHORT, - SANY, TULONG, + SANY, TLNG, NBREG, RESC1, - " ld U1,AL\n clr A1\n", }, + " ld U1,AL\n clr ZM\n", }, /* long/ptr pair -> int: take low word of pair */ { SCONV, INAREG, @@ -193,19 +201,19 @@ struct optab table[] = { NEEDS(NREG(A, 1), BYTET), RESC1, " ldb ZH,AL\n and A1,$0xff\n", }, -/* char -> long */ +/* char -> (u)long */ { SCONV, INBREG, SAREG, TCHAR, - SANY, TLONG, + SANY, TLNG, XSL(B), RESC1, " extsb AL\n ld U1,AL\n exts A1\n", }, -/* unsigned char -> unsigned long */ +/* unsigned char -> (u)long */ { SCONV, INBREG, SAREG, TUCHAR, - SANY, TULONG, + SANY, TLNG, XSL(B), RESC1, - " and AL,$0xff\n ld U1,AL\n clr A1\n", }, + " and AL,$0xff\n ld U1,AL\n clr ZM\n", }, /* int -> char: keep low byte (already in register) */ { SCONV, INAREG, @@ -214,6 +222,43 @@ struct optab table[] = { 0, RLEFT, "", }, +/* + * 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. * @@ -251,32 +296,58 @@ struct optab table[] = { XSL(A), RESC1, " call (AL)\n", }, -/* Named call, result is long/ptr in SBREG */ +/* Named call, result is long/ptr/float in SBREG */ { CALL, INBREG, SCON, TANY, - SANY, TLONG|TULONG|TPOINT, + 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, + 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 result */ +/* Indirect call (function pointer in pair), long/ptr/float result */ { CALL, INBREG, SBREG, TANY, - SANY, TLONG|TULONG|TPOINT, + 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, + 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 @@ -857,20 +928,35 @@ struct optab table[] = { 0, 0, " ld AL,AR\n", }, -/* pair reg <- pair reg or mem (long/ptr) */ +/* pair reg <- pair reg or mem (long/ptr/float) */ { ASSIGN, FOREFF|INBREG, - SBREG, TLONG|TULONG|TPOINT, - SBREG|SNAME|SOREG|SCON, TLONG|TULONG|TPOINT, + SBREG, TLONG|TULONG|TPOINT|TFLT, + SBREG|SNAME|SOREG|SCON, TLONG|TULONG|TPOINT|TFLT, 0, RDEST, " ldl AL,AR\n", }, -/* pair mem <- pair reg (long/ptr) */ +/* pair mem <- pair reg (long/ptr/float) */ { ASSIGN, FOREFF|INBREG, - SNAME|SOREG, TLONG|TULONG|TPOINT, - SBREG, TLONG|TULONG|TPOINT, + 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 @@ -929,13 +1015,23 @@ struct optab table[] = { XSL(A), RESC1, " ld A1,AL\n", }, -/* load long/ptr through pair register */ +/* load long/ptr/float through pair register */ { UMUL, INBREG, SANY, TANY, - SOREG, TLONG|TULONG|TPOINT, + 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 signed char through pair register */ { UMUL, INAREG, SANY, TANY, @@ -1083,13 +1179,22 @@ struct optab table[] = { NAREG, RESC1, " ld A1,AL\n", }, -/* load long/ptr constant/name/oreg into pair register */ +/* load long/ptr/float constant/name/oreg into pair register */ { OPLTYPE, INBREG, SANY, TANY, - SBREG|SCON|SOREG|SNAME, TLONG|TULONG|TPOINT, + 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", }, + /* char already in a register, or a char constant: copy as a word */ { OPLTYPE, INAREG, SANY, TANY, @@ -1134,6 +1239,20 @@ struct optab table[] = { 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. */ @@ -1175,20 +1294,36 @@ struct optab table[] = { NAREG, RNULL, " ld A1,AL\n push (rr14),A1\n", }, -/* push long/ptr pair register */ +/* push long/ptr/float pair register */ { FUNARG, FOREFF, - SBREG, TLONG|TULONG|TPOINT, - SANY, TLONG|TULONG|TPOINT, + SBREG, TLONG|TULONG|TPOINT|TFLT, + SANY, TLONG|TULONG|TPOINT|TFLT, 0, RNULL, " pushl (rr14),AL\n", }, -/* push long/ptr from memory or constant */ +/* push long/ptr/float from memory or constant */ { FUNARG, FOREFF, - SCON|SNAME|SOREG, TLONG|TULONG|TPOINT, - SANY, TLONG|TULONG|TPOINT, + SCON|SNAME|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", }, + +{ FUNARG, FOREFF, + SNAME|SFRAME, TDBL, + SANY, TDBL, + 0, RNULL, + "ZP", }, + /* push char (promoted to word) */ { FUNARG, FOREFF, SAREG, TCHAR|TUCHAR, From ed01a297d56c318de6aabf2ec540788621d23967 Mon Sep 17 00:00:00 2001 From: Michal Pleban Date: Sat, 4 Jul 2026 12:57:47 +0200 Subject: [PATCH 07/67] z8001: make &scalar-local work; FP-printf selection is link-time policy Taking the address of a scalar auto or parameter only compiled under -xtemps (where locals become TEMPs, and &TEMP is legal), and never for doubles (cisreg(DOUBLE)=0 keeps them out of temps): clocal rewrote scalar locals straight into OREGs, and buildtree's ADDROF accepts only UMUL/TEMP/NAME ("unacceptable operand of &"). Exposed by scanf-style code: sscanf("%lf", &d). Fix per the arch/i86 idiom: clocal now builds the *(r13+off) structure reference for ALL autos and parameters, scalars included. &x then cancels ADDROF(UMUL) in buildtree, and pass 2's canon folds ordinary accesses back into the same frame OREGs as before - the generated assembly for the entire existing test suite is byte-identical. The big-endian char-parameter byte correction (value in the low byte of the pushed word slot) moves from per-access in clocal to a one-time soffset adjustment in bfcode, so &charparam also yields the true byte address. Also revert the automatic ".globl _dtoa_" emission from ejobcode: pulling libc's real _dtefg printf formatter is link-time policy (the native "cc -f" flag), not the code generator's business - emitting it per FP-using unit would drag the formatter into programs that never print and would tie generated code to one libc's member layout. Link FP-printing programs with "ld -u _dtoa_" (or -Wl,-u,_dtoa_ via the driver). Hardware-verified on the Commodore 900: floattest 69/69 (adds sscanf %lf/%f/%d interop and &double/&float/&int arguments), printf %f via the -u link flag. Co-Authored-By: Claude Fable 5 --- arch/z8001/code.c | 30 ++++++++++++++++------- arch/z8001/local.c | 60 ++++++++++++---------------------------------- 2 files changed, 37 insertions(+), 53 deletions(-) diff --git a/arch/z8001/code.c b/arch/z8001/code.c index dd34b7452..5c693860b 100644 --- a/arch/z8001/code.c +++ b/arch/z8001/code.c @@ -189,6 +189,20 @@ bfcode(struct symtab **sp, int cnt) 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; @@ -207,18 +221,18 @@ bfcode(struct symtab **sp, int cnt) /* * Called just before compiler exits. - * If the unit used floating point, emit an undefined reference to - * _dtoa_ (defined in libc's crt/dtefg.o next to the real _dtefg - * printf formatter) so the linker picks the real FP formatter over - * the "No floating point!" dummy in gen/sdtoa.o (see zfpused). + * + * 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. */ void ejobcode(int flag) { - extern int zfpused; - - if (zfpused) - printf("\t.globl\t_dtoa_\n"); } /* diff --git a/arch/z8001/local.c b/arch/z8001/local.c index 965d73598..20f40e27f 100644 --- a/arch/z8001/local.c +++ b/arch/z8001/local.c @@ -57,16 +57,6 @@ * - Locals: negative OREG offsets from R13 (AUTOINIT=0) * - Params: positive OREG offsets from R13 (ARGINIT=32 bits = 4 bytes) */ -/* - * Set when the compilation unit uses any floating point. Coherent's - * libc has two definitions of the printf FP formatter _dtefg: the real - * one in crt/dtefg.o (which also defines _dtoa/ecvt/fcvt) and a dummy - * in gen/sdtoa.o that aborts with "No floating point!" so non-FP - * programs stay small. ejobcode() emits an undefined reference to - * _dtoa_ so the linker pulls the real module in first. - */ -int zfpused; - NODE * clocal(NODE *p) { @@ -75,10 +65,6 @@ clocal(NODE *p) int o; TWORD m; - m = p->n_type; - if (m == FLOAT || m == DOUBLE || m == LDOUBLE) - zfpused = 1; - switch (o = p->n_op) { case NAME: @@ -90,39 +76,23 @@ clocal(NODE *p) case REGISTER: case PARAM: /* - * An aggregate local or parameter (struct, union, or - * array) must stay an addressable object so that member - * access (p.x), indexing (a[i]), array decay and &p all - * work: build a frame structure reference *(r13 + off). - * &(*(r13+off)) then cancels to the lda form rather than - * hitting ADDROF(OREG). Same idiom as arch/i86. - */ - if (ISARY(p->n_type) || - p->n_type == STRTY || p->n_type == UNIONTY) { - 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; - } - /* - * Scalar auto/param: frame-relative OREG. soffset is - * in bits; divide by SZCHAR to get bytes. Parameters - * arrive above the frame pointer (ARGINIT = 4 bytes). + * 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). * - * Each argument occupies at least a 16-bit slot (the - * caller promotes char to a pushed word). On big-endian - * a char value lives in the low-order byte of that slot, - * so a char parameter's access offset is slot start + 1. + * 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. */ - p->n_op = OREG; - o = q->soffset / SZCHAR; - if (q->sclass == PARAM && - (p->n_type == CHAR || p->n_type == UCHAR || - p->n_type == BOOL)) - o += (SZINT - SZCHAR) / SZCHAR; - slval(p, o); - p->n_rval = FPREG; + 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: From 34982cca43b508b4d6a8060a87adb8d0d52bdab2 Mon Sep 17 00:00:00 2001 From: Michal Pleban Date: Sun, 5 Jul 2026 19:53:12 +0200 Subject: [PATCH 08/67] ccom: K&R parameter fixes and target-selectable sizeof/ptrdiff types Two front-end issues exposed by compiling real K&R sources (the Coherent userland) with the z8001 backend: - olddecl() copies stype/sdf/sap from an old-style parameter's real declaration but not the struct descriptor (sss), so any K&R struct or struct-pointer parameter kept ss == NULL and died with "compiler error: strmemb" on first member access (or in bfcode's -xtemps parameter copy via suemeq). Prototype-style definitions take defid2's enter path, which does copy it. - sizeof, offsetof, and pointer-difference results were hardwired to INTPTR, which is LONG on targets whose pointers are wider than int. A K&R libc declares sizes and counts as plain int/unsigned (qsort "unsigned nel, width", malloc "unsigned size"), so passing a widened sizeof to an unprototyped function misaligns the callee's argument walk. New SIZET and PTRDIFFT macros default to INTPTR (no change for existing targets) and may be overridden in macdefs.h; pointer subtraction still computes pointer-sized and only narrows the final result. Co-Authored-By: Claude Fable 5 --- cc/ccom/cgram.y | 3 ++- cc/ccom/pass1.h | 14 ++++++++++++++ cc/ccom/trees.c | 11 +++++++++-- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/cc/ccom/cgram.y b/cc/ccom/cgram.y index d297facaa..90bfc3d4f 100644 --- a/cc/ccom/cgram.y +++ b/cc/ccom/cgram.y @@ -1234,7 +1234,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 +1925,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/pass1.h b/cc/ccom/pass1.h index a80cd024c..83dedae40 100644 --- a/cc/ccom/pass1.h +++ b/cc/ccom/pass1.h @@ -702,6 +702,20 @@ 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 + /* 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..cd6f389cb 100644 --- a/cc/ccom/trees.c +++ b/cc/ccom/trees.c @@ -1460,6 +1460,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 ); } @@ -1980,8 +1987,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; } From 4a64c14ecc633a20ebe652c1384d58add89a2ab9 Mon Sep 17 00:00:00 2001 From: Michal Pleban Date: Sun, 5 Jul 2026 19:53:38 +0200 Subject: [PATCH 09/67] z8001: fixes from compiling real programs (echo, wc, ls run correct) Compiling the Coherent userland sources (echo.c, wc.c, ls.c) against the native compiler's golden .s output found five backend bugs the synthetic suite never touched; with them fixed, all three programs produce output identical to the native binaries on the C900 (ls -l matches byte for byte, wc matches on files and piped stdin), and the seven synthetic suites still pass. - The &frame-object rules (ZF) used SANY for the left operand, which also direct-matches NAME/OREG pointers and stole global-pointer arithmetic (char *gp; gp++) from the addl/subl rules, emitting frame-lda garbage. New SR13 special shape matches only REG r13. - sizeof/ptrdiff: SIZET=UNSIGNED, PTRDIFFT=INT (see the ccom commit); the K&R libc reads sizes as 16-bit words, so qsort/malloc/read calls pushed 4-byte longs where the callee walks 2-byte ints. - Statics were emitted as .comm, but Coherent commons are linker global: two modules with same-numbered L static labels would be merged into shared storage. defzero now reserves .bssd storage under the label (".even; L: .blkb size") like the native compiler; only real commons keep .comm. - TEMPREG is now carried by r0-r5 only. Listing rr0-rr4/rq0/rq4 in tempregs[] was redundant (AssignColors excludes overlapped colors via aliasmap from the r0-r5 edges) and the rq4 entry made every call exclude callee-saved r6/r7/rr6 from values live across it, so register locals in wc-sized functions all spilled to the frame. - The long zero-assign rule offered INBREG+RDEST with an SNAME destination, but its ZQ template clears memory and leaves no register holding the value, so chained assignments (chars = words = lines = 0) reclaimed an uninitialized pair. Split: SBREG keeps INBREG+RDEST, memory dest is FOREFF-only. - Frame equates are now numeric ("L=0|total", E_ASEG) instead of symbolic ("L=SS|total", E_SEG): the shipped Coherent as drops the bit-15 long-form flag when emitting E_SEG addresses with offsets outside 0..255 (outsof() sets it on the wrong expr), so the CPU decodes a short form and executes the offset word as an instruction. Any frame over ~250 bytes read garbage argc/argv through the broken form - the echo(1) segfault. MWC's factory binaries were built with an assembler without this bug; a numeric segment takes the correct E_ASEG path and encodes identically at runtime (crts0.s pins SS = 0). Co-Authored-By: Claude Fable 5 --- arch/z8001/local.c | 22 ++++++++++++++++++++++ arch/z8001/local2.c | 29 ++++++++++++++++++++++++----- arch/z8001/macdefs.h | 42 +++++++++++++++++++++++++++++++++++++----- arch/z8001/table.c | 26 ++++++++++++++++++++------ 4 files changed, 103 insertions(+), 16 deletions(-) diff --git a/arch/z8001/local.c b/arch/z8001/local.c index 20f40e27f..577ff9db0 100644 --- a/arch/z8001/local.c +++ b/arch/z8001/local.c @@ -426,6 +426,28 @@ defzero(struct symtab *sp) 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); diff --git a/arch/z8001/local2.c b/arch/z8001/local2.c index 4412c7c16..d91aff294 100644 --- a/arch/z8001/local2.c +++ b/arch/z8001/local2.c @@ -170,13 +170,28 @@ prologue(struct interpass_prolog *ipp) /* * Frame-base equate for stack-segment addressing (see framelab above). - * L = SS|total; slots are then 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 SS segment. total >= 2 always - * (R13 is always saved), so SS|total is never SS|0. + * L = 0|total; slots are then addressed "L+off(r13)" with r13 at + * the frame bottom, so EA = seg0:(total + off + r13_bottom) recovers the + * absolute frame offset and supplies the stack segment. total >= 2 + * always (R13 is always saved). + * + * The native compiler wrote "L=SS|total" with SS an external symbol + * (crts0.s pins it: "SS = 0x0000"), but that form is POISON with the + * shipped Coherent "as": a symbolic segment expression is E_SEG, and + * outsof()'s E_SEG long-form path (needed whenever total+off > 255, + * i.e. any frame bigger than ~250 bytes) drops the bit-15 long-form + * flag from the first address word (machine.c:1011 sets it on the + * caller's expr, then emits the zeroed copy). The CPU then decodes + * the short form, eats one word too few, and executes the offset word + * as an instruction - echo.c crashed exactly this way, and even the + * native echo.s reassembled with the on-disk as loses its argv (MWC's + * factory binaries were built with an assembler that didn't have the + * bug). A NUMERIC segment ("0|total") is E_ASEG instead, whose short + * AND long emissions are both correct, and the segment truly is 0 on + * this ABI. */ framelab = getlab2(); - printf(LABFMT "=SS|%d\n", framelab, total); + printf(LABFMT "=0|%d\n", framelab, total); /* Step 1: allocate entire frame */ if (total > 0) { @@ -1065,6 +1080,10 @@ special(NODE *p, int shape) 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; } return SRNOPE; } diff --git a/arch/z8001/macdefs.h b/arch/z8001/macdefs.h index 6bd3d0aca..a192fa317 100644 --- a/arch/z8001/macdefs.h +++ b/arch/z8001/macdefs.h @@ -224,16 +224,31 @@ typedef long long OFFSZ; 0, /* r13 - frame pointer */ \ 0, /* r14 - SP high */ \ 0, /* r15 - SP low */ \ - SBREG|TEMPREG, /* rr0 - valid color (RETREG) but cleared from clregs + SBREG, /* rr0 - valid color (RETREG) but cleared from clregs in bjobcode so never allocated (RR0 can't be a base) */ \ - SBREG|TEMPREG, /* rr2 */ \ - SBREG|TEMPREG, /* rr4 */ \ + 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|TEMPREG, /* rq0 - caller-saved, double RETREG */ \ - SCREG|TEMPREG, /* rq4 - r6/r7 half saved by prologue when used */ \ + SCREG, /* rq0 - caller-saved, double RETREG */ \ + SCREG, /* rq4 - r6/r7 half saved by prologue when used */ \ SCREG, /* rq8 - preserved via its words r8-r11 */ +/* + * 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. @@ -319,6 +334,23 @@ int COLORMAP(int c, int *r); 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) */ + +/* + * 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 /* Soft-float: big-endian IEEE binary32 and binary64 */ #define USE_IEEEFP_32 diff --git a/arch/z8001/table.c b/arch/z8001/table.c index 4610bb30b..5a7793e06 100644 --- a/arch/z8001/table.c +++ b/arch/z8001/table.c @@ -497,9 +497,12 @@ struct optab table[] = { * 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, - SANY, TPOINT, + SR13, TPOINT, SCON, TANY, NBREG, RESC1, "ZF", }, @@ -565,10 +568,11 @@ struct optab table[] = { * 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. + * 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, - SANY, TPOINT, + SR13, TPOINT, SCON, TANY, NBREG, RESC1, "ZF", }, @@ -886,14 +890,24 @@ struct optab table[] = { 0, 0, " clr AL\n", }, -/* pair <- zero (long); ZQ clears both halves (off and off+2), so the - * memory form is name/frame only */ +/* pair reg <- zero (long); RDEST valid: the cleared pair IS the value */ { ASSIGN, FOREFF|INBREG, - SBREG|SNAME, TLONG|TULONG|TPOINT, + SBREG, TLONG|TULONG|TPOINT, SZERO, TANY, 0, RDEST, "ZQ", }, +/* 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, From 26f693f4935d0f97a68ac846d300e6d029790825 Mon Sep 17 00:00:00 2001 From: Michal Pleban Date: Sun, 5 Jul 2026 22:05:15 +0200 Subject: [PATCH 10/67] z8001: byte register class D (rl0-rl7) replaces byte-insn constraints Z8001 byte instructions can only name the halves of r0-r7. This was enforced by NOLEFT/NORIGHT/NEVER constraints on the byte rules; NEVER is clobber semantics (addalledges), so every char access gave r8-r12 interference edges to ALL live values, starving cross-call values out of rr8/rr10 in char-heavy code (every register local in wc spilled). Make chars their own register class D instead (the arch/i86 model): rl0-rl7, the low bytes of r0-r7, indices 25-32. The hardware restriction is now structural (ROVERLAP/aliasmap wiring), and the constraints are gone. Only low-byte registers exist - a char in a word register lives in its low byte everywhere in this ABI. Conversions exploit word ops (ld/extsb/and/neg/push) being legal on any register: they operate on the byte register's CONTAINING word. New zzzcode escapes ZG/ZH print it, ZK/ZI emit the conversion moves and elide them when NSL sharing makes source and destination coincide, fusing char->int back to the native "ldb rlN,mem; extsb rN" idiom. No fixed registers, no NEEDS constraints in char handling. Details: PCLASS/gclass(char)=class D; RETREG(CHAR)=rl1 with INDREG call rules (shape matching is by PCLASS of node type); DECRA/ENCRA widened to 6-bit fields for MAXREGS 33; byte regs carry no TEMPREG (call-site r0-r5 edges reach rl0-rl5 via aliasmap); firstsavereg treats rl6/rl7 as r6/r7; rmove emits ldb for class D; COLORMAP gains the D terms; UMUL/OPLTYPE char loads are a bare ldb with promotion as a separate (usually fused) SCONV, preserving read-before-write on aliased bases. Verified in the C900 emulator: all 7 test suites pass (262 tests); echo, wc and ls output byte-identical to the native binaries. wc main's frame accesses drop 44 -> 2 (argc r9, argv rr10, fp rr6 across calls, matching native allocation); entry/exit dead moves decreased. Co-Authored-By: Claude Fable 5 --- arch/z8001/local2.c | 148 +++++++++++++++++--------- arch/z8001/macdefs.h | 105 ++++++++++++++----- arch/z8001/table.c | 242 ++++++++++++++++++++++++------------------- 3 files changed, 317 insertions(+), 178 deletions(-) diff --git a/arch/z8001/local2.c b/arch/z8001/local2.c index d91aff294..09214f787 100644 --- a/arch/z8001/local2.c +++ b/arch/z8001/local2.c @@ -69,17 +69,32 @@ void mygenregs(struct interpass *ip); /* * 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). - * Indices: 0-15 = r0-r15, 16=rr0 .. 21=rr10, 22=rq0, 23=rq4, 24=rq8. + * 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" + "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 @@ -153,6 +168,11 @@ firstsavereg(void) 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; } @@ -302,25 +322,17 @@ tlen(NODE *p) } /* - * blput: print an operand for a BYTE instruction. - * - * The Z8001 only has byte registers for r0..r7: in a byte instruction the - * register field 8+N names the LOW byte of word register rN ("rlN"), while - * field N names the HIGH byte ("rhN"). A char value held in word register rN - * lives in its low byte, so byte ops (ldb/andb/cpb/...) must name it "rlN" -- - * printing the plain word name "rN" would (mis)select the high byte rhN. - * Non-register operands (memory, constants) print exactly as adrput. + * 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 -blput(NODE *p) +bwput(NODE *p) { - if (p->n_op == REG) { - if (p->n_rval > R7) - comperr("blput: byte value in non-byte-addressable " - "register r%d (needs r0-r7)", p->n_rval); - printf("rl%d", p->n_rval); - } else - adrput(stdout, 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)]); } /* @@ -445,6 +457,12 @@ quadmem(NODE *mem, int q, int store) * 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) + * 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 @@ -455,18 +473,44 @@ quadmem(NODE *mem, int q, int store) void zzzcode(NODE *p, int c) { + NODE *l; int n; char *op; switch (c) { - case 'H': /* byte-register form of the result operand (A1) */ - blput(getlr(p, '1')); + 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 'G': /* byte-register form of the left operand (AL) */ - blput(getlr(p, 'L')); + + 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 'J': /* byte-register form of the right operand (AR) */ - blput(getlr(p, 'R')); + + 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 */ @@ -557,9 +601,10 @@ zzzcode(NODE *p, int c) case 'S': /* struct assignment: ldirb (dest),(src),count */ { struct attr *ap = attr_find(p->n_ap, ATTR_P2STRUCT); - NODE *l = p->n_left; 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 @@ -612,8 +657,9 @@ zzzcode(NODE *p, int c) case 'D': /* double assignment: left <- right, one side a quad */ { - NODE *l = p->n_left, *r = p->n_right; + 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)); @@ -630,9 +676,9 @@ zzzcode(NODE *p, int c) case 'E': /* double load: AL (leaf or quad reg) -> quad A1 */ { - NODE *l = getlr(p, 'L'); 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)); @@ -650,9 +696,7 @@ zzzcode(NODE *p, int c) * 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. */ - { - NODE *l = getlr(p, 'L'); - + 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)); @@ -663,23 +707,19 @@ zzzcode(NODE *p, int c) 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") */ - { - NODE *l = getlr(p, 'L'); - + 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. @@ -952,7 +992,12 @@ rmove(int s, int d, TWORD t) { if (s == d) return; - if (szty(t) == 4) { + 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)); @@ -977,7 +1022,9 @@ rmove(int s, int d, TWORD t) * 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. Worst-case (conservative) blocking counts. + * 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) @@ -986,31 +1033,38 @@ COLORMAP(int c, int *r) switch (c) { case CLASSA: - /* a pair blocks 2 words, a quad blocks 4 */ - num = r[CLASSA] + 2 * r[CLASSB] + 4 * r[CLASSC]; + /* 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 or pair neighbour blocks 1 pair, a quad blocks 2 */ - num = r[CLASSB] + r[CLASSA] + 2 * r[CLASSC]; + /* 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]; + 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; } /* * Return the register class suitable for a value of type t. - * Consistent with PCLASS in macdefs.h: 64-bit values (double) use class - * C (quads), 32-bit values (long/ptr/float) class B (pairs), everything - * else class A (words). + * 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; } diff --git a/arch/z8001/macdefs.h b/arch/z8001/macdefs.h index a192fa317..581e53598 100644 --- a/arch/z8001/macdefs.h +++ b/arch/z8001/macdefs.h @@ -172,6 +172,20 @@ typedef long long OFFSZ; * 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) */ @@ -201,7 +215,16 @@ typedef long long OFFSZ; #define RQ4 23 /* r4-r7 - scratch quad (r6/r7 saved when used) */ #define RQ8 24 /* r8-r11 - callee-saved quad */ -#define MAXREGS 25 +#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. @@ -233,8 +256,20 @@ typedef long long OFFSZ; 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 */ + 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 @@ -253,17 +288,18 @@ typedef long long OFFSZ; /* * 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. + * 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 \ - { RR0, RQ0, -1 }, /* r0 */ \ - { RR0, RQ0, -1 }, /* r1 */ \ - { RR2, RQ0, -1 }, /* r2 */ \ - { RR2, RQ0, -1 }, /* r3 */ \ - { RR4, RQ4, -1 }, /* r4 */ \ - { RR4, RQ4, -1 }, /* r5 */ \ - { RR6, RQ4, -1 }, /* r6 */ \ - { RR6, RQ4, -1 }, /* r7 */ \ + { 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 */ \ @@ -272,39 +308,54 @@ typedef long long OFFSZ; { -1 }, /* r13 - reserved */ \ { -1 }, /* r14 - reserved */ \ { -1 }, /* r15 - reserved */ \ - { R0, R1, RQ0, -1 }, /* rr0 */ \ - { R2, R3, RQ0, -1 }, /* rr2 */ \ - { R4, R5, RQ4, -1 }, /* rr4 */ \ - { R6, R7, RQ4, -1 }, /* rr6 */ \ + { 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, RR0, RR2, -1 }, /* rq0 */ \ - { R4, R5, R6, R7, RR4, RR6, -1 }, /* rq4 */ \ - { R8, R9, R10, R11, RR8, RR10, -1 }, /* rq8 */ + { 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) (szty((p)->n_type) == 4 ? SCREG : \ +#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 3 /* classes: A (word), B (pair), C (quad) */ +#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 : CLASSC) -#define DECRA(x,y) (((x) >> ((y)*5)) & 31) /* decode register from n_reg */ +#define GCLASS(x) ((x) < 16 ? CLASSA : (x) < 22 ? CLASSB : \ + (x) < 25 ? CLASSC : CLASSD) +/* 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) << 5) -#define ENCRA2(x) ((x) << 10) -#define ENCRA(x,y) ((x) << (5+(y)*5)) /* encode source 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): - * int/short/char -> r1 + * 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) (szty(x) == 4 ? RQ0 : szty(x) == 2 ? RR0 : R1) +#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) */ diff --git a/arch/z8001/table.c b/arch/z8001/table.c index 5a7793e06..fb0b23d27 100644 --- a/arch/z8001/table.c +++ b/arch/z8001/table.c @@ -31,6 +31,7 @@ * 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 @@ -60,23 +61,25 @@ /* 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)) /* - * Byte-register constraints. Z8001 byte instructions can only name the - * halves of r0-r7 (rh0-rh7/rl0-rl7); r8-r12 have no byte-addressable - * halves. Word instructions (extsb, and, push, ...) operate on any - * register, so a char value must sit in r0-r7 only at instructions that - * PRINT a byte register name (ldb/cpb via the ZG/ZH/ZJ escapes). - * BYTEL/BYTER constrain the left/right operand's live range at such a - * use; BYTET keeps the rule's own temp (a ZH destination) out of - * r8-r12. Values under pressure spill instead of misallocating; - * blput()'s comperr remains as a backstop. + * 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. */ -#define BYTEL NOLEFT(R8), NOLEFT(R9), NOLEFT(R10), NOLEFT(R11), NOLEFT(R12) -#define BYTER NORIGHT(R8), NORIGHT(R9), NORIGHT(R10), NORIGHT(R11), NORIGHT(R12) -#define BYTET NEVER(R8), NEVER(R9), NEVER(R10), NEVER(R11), NEVER(R12) struct optab table[] = { @@ -173,54 +176,64 @@ struct optab table[] = { XSL(A), RESC1, " ld A1,UL\n", }, -/* char in reg -> int (sign-extend) */ -{ SCONV, INAREG, - SAREG, TCHAR, - SANY, TINT|TUNSIGNED, - 0, RLEFT, - " extsb AL\n", }, - -/* unsigned char in reg -> (u)int (zero-extend) */ -{ SCONV, INAREG, - SAREG, TUCHAR, - SANY, TINT|TUNSIGNED, +/* char <-> char: same byte register either way */ +{ SCONV, INDREG, + SDREG, TCHAR|TUCHAR, + SANY, TCHAR|TUCHAR, 0, RLEFT, - " and AL,$0xff\n", }, + "", }, -/* char from memory -> int */ +/* 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, - SOREG|SNAME, TCHAR, - SANY, TINT|TUNSIGNED, - NEEDS(NREG(A, 1), BYTET), RESC1, - " ldb ZH,AL\n extsb A1\n", }, + SDREG, TCHAR, + SANY, TWORD, + NEEDS(NREG(A, 1), NSL(A)), RESC1, + "ZK extsb A1\n", }, -/* unsigned char from memory -> (u)int */ +/* unsigned char (byte reg) -> (u)int: zero-extend via word and */ { SCONV, INAREG, - SOREG|SNAME, TUCHAR, - SANY, TINT|TUNSIGNED, - NEEDS(NREG(A, 1), BYTET), RESC1, - " ldb ZH,AL\n and A1,$0xff\n", }, + SDREG, TUCHAR, + SANY, TWORD, + NEEDS(NREG(A, 1), NSL(A)), RESC1, + "ZK and A1,$0xff\n", }, -/* char -> (u)long */ +/* 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, - SAREG, TCHAR, + SDREG, TCHAR, SANY, TLNG, - XSL(B), RESC1, - " extsb AL\n ld U1,AL\n exts A1\n", }, + NBREG, RESC1, + " ld U1,ZG\n extsb U1\n exts A1\n", }, /* unsigned char -> (u)long */ { SCONV, INBREG, - SAREG, TUCHAR, + SDREG, TUCHAR, SANY, TLNG, - XSL(B), RESC1, - " and AL,$0xff\n ld U1,AL\n clr ZM\n", }, + NBREG, RESC1, + " ld U1,ZG\n and U1,$0xff\n clr ZM\n", }, -/* int -> char: keep low byte (already in register) */ -{ SCONV, INAREG, +/* 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, - 0, RLEFT, - "", }, + 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 @@ -270,32 +283,58 @@ struct optab table[] = { * ZB expands to the stack adjustment after the call (via zzzcode 'B'). */ -/* Named call, result is word (int/short/char/ptr-lo) in SAREG */ +/* Named call, result is word (int/short) in SAREG */ { CALL, INAREG, SCON, TANY, - SANY, TWORD|TCHAR|TUCHAR, + SANY, TWORD, XSL(A), RESC1, " calr CL\nZB", }, { UCALL, INAREG, SCON, TANY, - SANY, TWORD|TCHAR|TUCHAR, + SANY, TWORD, XSL(A), RESC1, " calr CL\n", }, /* Indirect call (function pointer in pair), word result */ { CALL, INAREG, SBREG, TANY, - SANY, TWORD|TCHAR|TUCHAR, + SANY, TWORD, XSL(A), RESC1, " call (AL)\nZB", }, { UCALL, INAREG, SBREG, TANY, - SANY, TWORD|TCHAR|TUCHAR, + 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, @@ -976,19 +1015,19 @@ struct optab table[] = { * 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/char reg <- reg or mem (dest reg -> low byte; src reg -> low byte) */ -{ ASSIGN, FOREFF|INAREG|FORCC, - SAREG, TCHAR|TUCHAR, - SAREG|SNAME|SOREG|SCON, TCHAR|TUCHAR, - NEEDS(BYTEL, BYTER), RDEST|RESCC, - " ldb ZG,ZJ\n", }, +/* 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 (src reg -> low byte) */ -{ ASSIGN, FOREFF|INAREG|FORCC, +/* byte mem <- reg */ +{ ASSIGN, FOREFF|INDREG|FORCC, SNAME|SOREG, TCHAR|TUCHAR, - SAREG, TCHAR|TUCHAR, - NEEDS(BYTER), RDEST|RESCC, - " ldb AL,ZJ\n", }, + SDREG, TCHAR|TUCHAR, + 0, RDEST|RESCC, + " ldb AL,AR\n", }, /* byte mem <- const (ldb dst,#imm takes IR/DA/X - no BA) */ { ASSIGN, FOREFF, @@ -1046,19 +1085,16 @@ struct optab table[] = { NEEDS(NREG(C, 1)), RESC1, "ZE", }, -/* load signed char through pair register */ -{ UMUL, INAREG, - SANY, TANY, - SOREG, TCHAR, - NEEDS(NREG(A, 1), NSL(A), BYTET), RESC1, - " ldb ZH,AL\n extsb A1\n", }, - -/* load unsigned char through pair register */ -{ UMUL, INAREG, +/* 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, TUCHAR, - NEEDS(NREG(A, 1), NSL(A), BYTET), RESC1, - " ldb ZH,AL\n and A1,$0xff\n", }, + SOREG, TCHAR|TUCHAR, + NDREG, RESC1, + " ldb A1,AL\n", }, /* * Logical/comparison operators. @@ -1139,18 +1175,18 @@ struct optab table[] = { 0, RESCC, " cpl AL,AR\n", }, -/* compare char vs char (reg operands -> low byte) */ +/* compare char vs char (byte registers print as rlN) */ { OPLOG, FORCC, - SAREG, TCHAR|TUCHAR, - SAREG|SNAME|SCON, TCHAR|TUCHAR, - NEEDS(BYTEL, BYTER), RESCC, - " cpb ZG,ZJ\n", }, + SDREG, TCHAR|TUCHAR, + SDREG|SNAME|SCON, TCHAR|TUCHAR, + 0, RESCC, + " cpb AL,AR\n", }, { OPLOG, FORCC, - SAREG, TCHAR|TUCHAR, + SDREG, TCHAR|TUCHAR, SNBA, TCHAR|TUCHAR, - NEEDS(BYTEL), RESCC, - " cpb ZG,ZJ\n", }, + 0, RESCC, + " cpb AL,AR\n", }, /* compare char mem vs const (immediate-compare form) */ { OPLOG, FORCC, @@ -1209,26 +1245,14 @@ struct optab table[] = { NEEDS(NREG(C, 1)), RESC1, "ZE", }, -/* char already in a register, or a char constant: copy as a word */ -{ OPLTYPE, INAREG, - SANY, TANY, - SAREG|SCON, TCHAR|TUCHAR, - NAREG, RESC1, - " ld A1,AL\n", }, - -/* load signed char from mem into word register */ -{ OPLTYPE, INAREG, - SANY, TANY, - SOREG|SNAME, TCHAR, - NEEDS(NREG(A, 1), BYTET), RESC1, - " ldb ZH,AL\n extsb A1\n", }, - -/* load unsigned char from mem into word register */ -{ OPLTYPE, INAREG, +/* 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, - SOREG|SNAME, TUCHAR, - NEEDS(NREG(A, 1), BYTET), RESC1, - " ldb ZH,AL\n and A1,$0xff\n", }, + SDREG|SOREG|SNAME|SCON, TCHAR|TUCHAR, + NDREG, RESC1, + " ldb A1,AL\n", }, /* load address (lda) of SOREG/SNAME into pair register */ { OPLTYPE, INBREG, @@ -1242,11 +1266,19 @@ struct optab table[] = { */ { UMINUS, INAREG|FOREFF, - SAREG, TWORD|TCHAR|TUCHAR, + 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, @@ -1338,12 +1370,14 @@ struct optab table[] = { 0, RNULL, "ZP", }, -/* push char (promoted to word) */ +/* 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, - SAREG, TCHAR|TUCHAR, + SDREG, TCHAR|TUCHAR, SANY, TCHAR|TUCHAR, 0, RNULL, - " push (rr14),AL\n", }, + " push (rr14),ZG\n", }, /* push zero word */ { FUNARG, FOREFF, From eb84b22b1a8b957ff6d665d3b7d8b45dc18a347e Mon Sep 17 00:00:00 2001 From: Michal Pleban Date: Mon, 6 Jul 2026 10:16:07 +0200 Subject: [PATCH 11/67] z8001: block return-value coalescing into rr0 clocal rewrites "return v" into ASSIGN(REG-rr0, v); for a reg-reg assign, insnwalk moveadds the right-side temp with the precolored rr0 node, and Briggs coalescing bypasses the clregs selectable mask (the same bug class as the CALL-result coalesce fixed earlier with NEVER edges on the call rules). A pair value that is both dereferenced and returned then printed as the forbidden (rr0) indirect base, found in real programs: tail nextline "ldb rl2,(rr0)" and sort ralloc "ldl rr2,(rr0)". Fix: NEEDS(NORIGHT(RR0)) on the pair reg <- reg/mem ASSIGN rule. The conflict edge blocks only that coalesce, so such returns emit an explicit "ldl rr0,rrN" move, exactly like the native compiler. Co-Authored-By: Claude Fable 5 --- arch/z8001/table.c | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/arch/z8001/table.c b/arch/z8001/table.c index fb0b23d27..c29898879 100644 --- a/arch/z8001/table.c +++ b/arch/z8001/table.c @@ -981,11 +981,18 @@ struct optab table[] = { 0, 0, " ld AL,AR\n", }, -/* pair reg <- pair reg or mem (long/ptr/float) */ +/* 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, - 0, RDEST, + NEEDS(NORIGHT(RR0)), RDEST, " ldl AL,AR\n", }, /* pair mem <- pair reg (long/ptr/float) */ From 5e9d487852612d70cab6f1fe17ca2de47682194b Mon Sep 17 00:00:00 2001 From: Michal Pleban Date: Mon, 6 Jul 2026 10:16:44 +0200 Subject: [PATCH 12/67] z8001: promote char arguments to int at function calls The front end only applies the default argument promotion for FLOAT->DOUBLE on unprototyped calls (params.c oldarg()); char arguments were pushed as their containing word, whose high byte is undefined under the class-D byte register model. A K&R callee that declares the parameter as int reads the whole word and gets garbage: found via tail's skbl() doing fstat(fileno(infp), &stbuf), where the FILE _fd char field was pushed as "ldb rl0,rr2(23); push r0". Fix: funcode() wraps CHAR/UCHAR arguments in SCONV to INT/UNSIGNED before building the FUNARG, so the push is a properly extended word ("ldb rl0,...; extsb r0; push r0"), exactly like the native compiler. short/ushort are already full 16-bit words on this target and are left alone. Co-Authored-By: Claude Fable 5 --- arch/z8001/code.c | 36 ++++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/arch/z8001/code.c b/arch/z8001/code.c index 5c693860b..57648dae9 100644 --- a/arch/z8001/code.c +++ b/arch/z8001/code.c @@ -305,6 +305,29 @@ instring(struct symtab *sp) 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. @@ -315,17 +338,18 @@ 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) - r->n_right = block(FUNARG, r->n_right, NIL, - r->n_right->n_type, r->n_right->n_df, - r->n_right->n_ap); + 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 = l; - r->n_type = l->n_type; + r->n_left = argwiden(l); + r->n_type = r->n_left->n_type; } return p; } From 8105b099d20aed4847036140cb48ed40968bd7da Mon Sep 17 00:00:00 2001 From: Michal Pleban Date: Mon, 6 Jul 2026 12:24:50 +0200 Subject: [PATCH 13/67] ccom: fix chkpun crash on function pointer initialized from cast pointer chkpun's pointer-type walk assumed that when the left type reaches a function type the right side is one too, and dereferenced its dimfun pointer for the prototype check. A non-function right operand (e.g. int (*f)() = (char *)0, common in K&R code where NULL is defined as ((char *)0)) carries no dimfun at all -> NULL dereference, reported as "major internal compiler error". Break out to the ordinary illegal pointer combination warning instead. Found compiling Coherent learn.c; repro in ztests/real/e46.c. Co-Authored-By: Claude Fable 5 --- cc/ccom/trees.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cc/ccom/trees.c b/cc/ccom/trees.c index cd6f389cb..9a391946f 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"); From 203728e74ed0e94af81fb0c8d74e35010a77cf1d Mon Sep 17 00:00:00 2001 From: Michal Pleban Date: Mon, 6 Jul 2026 12:25:11 +0200 Subject: [PATCH 14/67] ccom: accept K&R constructs found throughout Coherent sources All four appear in Coherent 0.7.3 userland sources whose shipped .s goldens prove the native K&R compiler accepted them: - external declarations with no type specifier at all default to int with a warning (tar.c "unixbug = 0;", scat.c "puts1(), puts2();"): new notype_init_dcl_list under external_def. Adds one benign shift/reduce conflict (declarator . C_ATTRIBUTE at external scope, resolves to shift). - a scalar initializer for an array initializes element 0 as if braced, with a warning (tar.c "char tapedev[10] = '\0';"). - "return;" in a function returning a value is a warning, not an error - the constraint is C99, C89 allows it (mv.c NOTREACHED). - mismatched pointer types in ?: warn ("illegal pointer combination") and take the left type instead of erroring (diff3.c "mat ? NULL : fp13" with NULL = ((char *)0)). Repros in ztests/real/e47-e50.c. Co-Authored-By: Claude Fable 5 --- cc/ccom/cgram.y | 50 ++++++++++++++++++++++++++++++++++++++++++++++--- cc/ccom/init.c | 12 ++++++++++++ cc/ccom/trees.c | 7 +++++++ 3 files changed, 66 insertions(+), 3 deletions(-) diff --git a/cc/ccom/cgram.y b/cc/ccom/cgram.y index 90bfc3d4f..885c18e80 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,53 @@ 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); + werror("type defaults to int in declaration"); + 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); + werror("type defaults to int in declaration"); + $$ = 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 +982,12 @@ 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"); + /* legal in C89 (constraint is C99); + * common in K&R code */ + werror("return value required"); rch: if (!reached) warner(Wunreachable_code); diff --git a/cc/ccom/init.c b/cc/ccom/init.c index a08d6f10c..046f4b5b8 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 (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/trees.c b/cc/ccom/trees.c index 9a391946f..fdb4ff7d2 100644 --- a/cc/ccom/trees.c +++ b/cc/ccom/trees.c @@ -1539,6 +1539,13 @@ ptmatch(P1ND *p) if (BTYPE(td2->type) == VOID) break; } + if (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; From 80e7fe3cc74e8e43e1c3008d6264e864e95f50ad Mon Sep 17 00:00:00 2001 From: Michal Pleban Date: Mon, 6 Jul 2026 12:25:26 +0200 Subject: [PATCH 15/67] ccom: target hook OPTIM_KEEPZERO to protect x+0/x-0 identity folds optim() unconditionally rewrites PLUS/MINUS with a zero right side to the bare left operand. When the left side is the frame register that turns an address computation into a naked word register - fine on flat-memory targets, wrong on segmented ones where materializing a frame address must also supply the segment. Targets can now override OPTIM_KEEPZERO(p) (default 0) in macdefs.h to keep the operation. Co-Authored-By: Claude Fable 5 --- cc/ccom/optim.c | 2 +- cc/ccom/pass1.h | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) 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/pass1.h b/cc/ccom/pass1.h index 83dedae40..c8654e0cd 100644 --- a/cc/ccom/pass1.h +++ b/cc/ccom/pass1.h @@ -716,6 +716,16 @@ void dwarf_end(void); #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 + /* Generate a bitmask from a given type size */ #define SZMASK(y) ((((1LL << ((y)-1))-1) << 1) | 1) From d985695cda7f150def1f0dde97d44d9e82b89014 Mon Sep 17 00:00:00 2001 From: Michal Pleban Date: Mon, 6 Jul 2026 12:25:27 +0200 Subject: [PATCH 16/67] z8001: frame address at offset zero must stay segmented; fold (long)ptr Two fixes from the _examples/cmd mass sweep (80/86 now compile clean through cpp/ccom/audit/shapediff/host-as/host-ld): - OPTIM_KEEPZERO: &arr[N] where the array end lands at frame offset exactly 0 was identity-folded to bare r13 - a 16-bit word with no segment - and compared against a pointer pair, emitting the illegal "cpl rr8,r13" (tee.c "fpp >= &fp[NUFILE]"). Keeping the PLUS lets the ZF rule emit the normal lda+mask pair. Repro ztests/real/e44.c. - clocal SCONV: an explicit (long)ptr cast feeding arithmetic left an SCONV(ptr->long) that pass2 has no rule for ("Cannot generate code op SCONV", units.c/load.c/uload.c). Same-size no-op fold now also covers pointer sources; conversions TO pointers were already folded in the PCONV case. Repro ztests/real/e45.c. Co-Authored-By: Claude Fable 5 --- arch/z8001/local.c | 9 +++++++-- arch/z8001/macdefs.h | 10 ++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/arch/z8001/local.c b/arch/z8001/local.c index 577ff9db0..65fef627f 100644 --- a/arch/z8001/local.c +++ b/arch/z8001/local.c @@ -153,8 +153,13 @@ clocal(NODE *p) return l; } - /* long <-> long in same class: no-op */ - if ((m == LONG || m == ULONG) && + /* 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); diff --git a/arch/z8001/macdefs.h b/arch/z8001/macdefs.h index 581e53598..b9c4ef148 100644 --- a/arch/z8001/macdefs.h +++ b/arch/z8001/macdefs.h @@ -403,6 +403,16 @@ int COLORMAP(int c, int *r); #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) + /* Soft-float: big-endian IEEE binary32 and binary64 */ #define USE_IEEEFP_32 #define FLT_PREFIX IEEEFP_32 From e3e2d85dd70116542c3854537485dba8e7e45993 Mon Sep 17 00:00:00 2001 From: Michal Pleban Date: Mon, 6 Jul 2026 14:19:50 +0200 Subject: [PATCH 17/67] ccom: check ferror instead of putw return value in pr_wr putw()'s return value is not portable: POSIX returns 0 on success but MinGW's _putw returns the value written, so any nonzero word made pr_wr() report a spurious write error. Check the stream error flag instead, matching how pr_rd() detects failure. Co-Authored-By: Claude Fable 5 --- cc/ccom/params.c | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) 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"); } From a203f54bb1f4c998ba84728b676a6cc9321c3c35 Mon Sep 17 00:00:00 2001 From: Michal Pleban Date: Mon, 6 Jul 2026 17:01:42 +0200 Subject: [PATCH 18/67] ccom: findops xssa 2-op preference must not demote SPECIAL shape matches The xssa-only heuristic in findops() that demotes a directly-matched left operand to SRREG (preferring 2-op insns after SSA) tested n2osh(q->lshape) & INREGS. For SPECIAL shapes the low bits are a shape number, not register-class bits, so any special shape whose number intersects INREGS misfired: z8001's SR13 (SPECIAL|8, 8 = INCREG) made the &frameobj lda rule demote its own REG r13 operand into a C-class register, for which no rule exists -> "Cannot generate code op REG" ICE on any ¶m/&local at -xssa; where a fallback rule existed it instead silently emitted ldl rrN,r13 (word frame reg into a pointer pair, segment half garbage). SCCON (SPECIAL|3) and friends alias INAREG the same way on other targets. Guard the demotion with (n2osh(q->lshape) & SPECIAL) == 0; correct for both the int shape encoding and NEWSHAPES SPECON. xssa-gated, so non-ssa output is unchanged (verified byte-identical on the 7 z8001 test suites and 6 real programs at -O0). Repro: take the address of a parameter with -xtemps -xdeljumps -xssa. Co-Authored-By: Claude Fable 5 --- mip/match.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/mip/match.c b/mip/match.c index 77a1cd341..911c977b1 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; From 9aeb73a200661157f26d277e16208e551381fb45 Mon Sep 17 00:00:00 2001 From: Michal Pleban Date: Mon, 6 Jul 2026 19:19:21 +0200 Subject: [PATCH 19/67] z8001: push arguments directly from constants, globals and frame slots FUNARG went through a scratch register for everything but registers (ld A1,AL / push (rr14),A1), where native pushes most operands directly: push src accepts IM/DA/X/IR (as machine.c S_PUSH; word immediates get the dedicated PUSHI encoding) and the corpus uses `push (rr14), $1` over a thousand times. Add direct-push rules for word SCON/SNAME/SNBA and long/ptr/float SNAME/SNBA; only BA OREGs (pair base + displacement) keep the register detour, and long constants keep the ldl path - the assembler special-cases IM for PUSHW only, pushl-immediate does not assemble. Also split the FUNARG TDBL "SNAME|SFRAME" rule in two: OR-ing a special shape with SNAME builds a value tshape()/special() can never match, so the combined rule was dead and doubles always bounced through a quad register. Special shapes must appear alone in a rule. Verified in the C900 emulator against both a rebuilt -O0 and a new -O1 pcc-built libc (95/95 libc sources compile at -O1): 7 test suites x {-O0,-O1} all pass (524 ok), echo/wc/tail/sort/ls byte-identical to native /bin, factor byte-identical to the -O0 reference build. Co-Authored-By: Claude Fable 5 --- arch/z8001/table.c | 63 ++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 56 insertions(+), 7 deletions(-) diff --git a/arch/z8001/table.c b/arch/z8001/table.c index c29898879..eb0392519 100644 --- a/arch/z8001/table.c +++ b/arch/z8001/table.c @@ -1340,10 +1340,34 @@ struct optab table[] = { 0, RNULL, " push (rr14),AL\n", }, -/* push word constant or memory */ +/* 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|SNAME|SOREG, TWORD|TSHORT|TUSHORT, - SANY, TWORD, + 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", }, @@ -1354,10 +1378,26 @@ struct optab table[] = { 0, RNULL, " pushl (rr14),AL\n", }, -/* push long/ptr/float from memory or constant */ +/* 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, - SCON|SNAME|SOREG, TLONG|TULONG|TPOINT|TFLT, - SANY, TLONG|TULONG|TPOINT|TFLT, + 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", }, @@ -1371,8 +1411,17 @@ struct optab table[] = { 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, - SNAME|SFRAME, TDBL, + SFRAME, TDBL, SANY, TDBL, 0, RNULL, "ZP", }, From 901212486049b4b45183cfd670be4b7b1f16e70e Mon Sep 17 00:00:00 2001 From: Michal Pleban Date: Tue, 7 Jul 2026 00:41:34 +0200 Subject: [PATCH 20/67] z8001: truth tests - EQ/NE against zero via test/testl/testb TEST performs "dst OR 0" and sets only Z and S, leaving P/V as parity, so the ordered conditions (S xor V) cannot use it - but EQ/NE read only the Z flag, which TEST sets correctly. Add EQ/NE rules emitting test/testl/testb on register, SNAME and SNBA shapes, placed before the generic OPLOG cp/cpl rules (equality compares match both at the same level and the first table entry wins). test takes dst = R/IR/DA/X (as S_CLR class), so unlike cpl it works directly on memory: testl mem absorbs an ldl+cpl pair and testb mem a ldb/extsb/cp triple. Native cc uses these heavily (943 test / 561 testl / 274 testb across the golden corpus); we previously emitted none. Co-Authored-By: Claude Fable 5 --- arch/z8001/table.c | 94 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 88 insertions(+), 6 deletions(-) diff --git a/arch/z8001/table.c b/arch/z8001/table.c index eb0392519..a7fce2ba3 100644 --- a/arch/z8001/table.c +++ b/arch/z8001/table.c @@ -1109,12 +1109,19 @@ struct optab table[] = { */ /* - * Compare vs zero. 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. + * 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 @@ -1123,6 +1130,81 @@ struct optab table[] = { * 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", }, + /* compare word vs zero */ { OPLOG, FORCC, SAREG|SNAME, TWORD, From 97506de9f9e4ce986aae9bc994baa4130d6ef55e Mon Sep 17 00:00:00 2001 From: Michal Pleban Date: Tue, 7 Jul 2026 00:41:50 +0200 Subject: [PATCH 21/67] z8001: narrow char equality compares to byte width in clocal The front end promotes both sides of a compare to int, hiding the byte operand behind an SCONV so pass 2 could never use testb/cpb: "if (c)" compiled to ldb/extsb/cp where native emits one testb, and "c == 'x'" to ldb/extsb/cp $imm where native emits cpb mem,$imm. For EQ/NE the promotion is value-preserving in both directions whenever the constant fits the char's value range (CHAR -128..127, UCHAR 0..255), so clocal strips the SCONV and retypes the constant to the char type. Ordered compares are NOT narrowed: an unsigned char would also need the operator flipped to the unsigned condition. Bitfield (FLD) sources are skipped - their SCONV wrapper controls the extraction. The narrowed forms cpb mem,$imm and cpb rlN,$imm are corpus-novel but verified legal from the assembler source: S_CP with immediate source accepts IR/DA/X dests via the CPI encoding, and byte immediates to register dests are replicated into both halves by outxx exactly as CPB Rbd,#data requires. Co-Authored-By: Claude Fable 5 --- arch/z8001/local.c | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/arch/z8001/local.c b/arch/z8001/local.c index 65fef627f..1e106856f 100644 --- a/arch/z8001/local.c +++ b/arch/z8001/local.c @@ -167,6 +167,51 @@ clocal(NODE *p) } break; + case EQ: + case NE: { + /* + * Narrow a char equality 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. For EQ/NE the promotion is value-preserving + * in both directions whenever the constant fits the char's + * value range, so strip the SCONV and retype the constant + * to the char type. Ordered compares are NOT narrowed: + * an unsigned char would also need the operator flipped + * to the unsigned condition. + */ + NODE *sc, *cn; + + sc = p->n_left; + cn = p->n_right; + if (sc->n_op != SCONV && cn->n_op == SCONV) { + sc = p->n_right; + cn = p->n_left; + } + if (sc->n_op != SCONV || cn->n_op != ICON || cn->n_sp != NULL) + 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_left == sc) + p->n_left = sc->n_left; + else + p->n_right = sc->n_left; + nfree(sc); + cn->n_type = m; + break; + } + case FORCE: /* * FORCE ensures the return value is in the correct register. From b2d8f269590b4840399203b371149daaa8295edd Mon Sep 17 00:00:00 2001 From: Michal Pleban Date: Tue, 7 Jul 2026 00:42:08 +0200 Subject: [PATCH 22/67] z8001: SNBA accepts zero-displacement dereferences as SROREG UMUL operands never reached the SNBA special shape: special() only matched already-folded OREGs, but pointer-temp dereferences are still UMUL(TEMP) at instruction-selection time (canon's oreg2 folds only real-REG bases). So "if (*p)" loaded the value into a register before testing it instead of using the memory form. Teach special() SNBA to accept UMUL of a bare pair register/temp as SROREG: offstar evaluates the address into a pair and gencode's canon folds UMUL(REG) into a zero-displacement OREG, which prints as (rrN) = IR mode, legal in every SNBA consumer (test/testl/testb, cp/cpb immediate-compare, ldb/ld $imm stores, clr, inc/dec, add/sub/and/or/xor sources, push/pushl). PLUS/MINUS(base,con) addresses are rejected: they would fold to a displaced pair base = BA mode, which these instructions cannot encode. Besides the truth tests (testl (rrN) absorbs an ldl, testb (rrN) a ldb) this also delivers the indirect push family for free: FUNARG of *p now emits pushl (rr14),(rrN) like native instead of the ldl detour, and dereferenced arithmetic operands fuse (sub r1,(rr2)). Co-Authored-By: Claude Fable 5 --- arch/z8001/local2.c | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/arch/z8001/local2.c b/arch/z8001/local2.c index 09214f787..329e0abb8 100644 --- a/arch/z8001/local2.c +++ b/arch/z8001/local2.c @@ -1123,6 +1123,26 @@ 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) From c81529ec0d72168f8897e19bd8f2fd70e44f28cb Mon Sep 17 00:00:00 2001 From: Michal Pleban Date: Tue, 7 Jul 2026 01:39:37 +0200 Subject: [PATCH 23/67] z8001: load small word constants with the 2-byte ldk New special shape SLDK matches a nameless ICON in 0..15, the exact range of the ldk instruction (as S_LDK rejects anything else). An OPLTYPE rule and an ASSIGN reg<-const rule use it ahead of the generic ld $imm rules, halving those loads from 4 to 2 bytes; the goldens use ldk for every such constant. ldk sets no flags, so the ASSIGN rule deliberately claims no FORCC/RESCC - a compare-context assign falls back to the generic ld rule. Co-Authored-By: Claude Fable 5 --- arch/z8001/local2.c | 5 +++++ arch/z8001/macdefs.h | 2 ++ arch/z8001/table.c | 18 ++++++++++++++++++ 3 files changed, 25 insertions(+) diff --git a/arch/z8001/local2.c b/arch/z8001/local2.c index 329e0abb8..1576c3db8 100644 --- a/arch/z8001/local2.c +++ b/arch/z8001/local2.c @@ -1158,6 +1158,11 @@ special(NODE *p, int shape) 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; } return SRNOPE; } diff --git a/arch/z8001/macdefs.h b/arch/z8001/macdefs.h index b9c4ef148..b3fa8377a 100644 --- a/arch/z8001/macdefs.h +++ b/arch/z8001/macdefs.h @@ -389,6 +389,8 @@ int COLORMAP(int c, int *r); 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 */ /* * sizeof and pointer-difference results are plain 16-bit integers, NOT diff --git a/arch/z8001/table.c b/arch/z8001/table.c index a7fce2ba3..7217abf13 100644 --- a/arch/z8001/table.c +++ b/arch/z8001/table.c @@ -953,6 +953,15 @@ struct optab table[] = { 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, @@ -1311,6 +1320,15 @@ struct optab table[] = { * 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, From 7e26e371131e5dcfc6a70fb079dd0d7a9ede1f1a Mon Sep 17 00:00:00 2001 From: Michal Pleban Date: Tue, 7 Jul 2026 02:31:22 +0200 Subject: [PATCH 24/67] z8001: pointer +- constant operates on the offset word only Under the Coherent segmented model no object crosses a segment boundary, so valid pointer arithmetic can never carry out of the 16-bit offset half of a pair - the same assumption native cc makes (user-approved). New TPOINT-only PLUS/MINUS rules ahead of the generic pair rules emit inc/dec UL,$1..16 (2 bytes, new special SP16) and add/sub UL,$imm (4 bytes, new special SPCON limited to word immediates without symbol relocations) instead of the 6-byte addl/subl, leaving the segment word untouched. upput already prints the low word for every operand flavour. Plain longs are excluded: for a real 32-bit integer the carry is arithmetic, so TLONG/TULONG keep addl/subl. Memory RMW forms (gp++ as inc gp_+2,$1 via FINDMOPS) use SNAME and SFRAME shapes only - the low word of a zero-displacement pair-base OREG would be BA mode, which inc cannot encode; and there are no SPCON memory forms since add/sub take register destinations only. Co-Authored-By: Claude Fable 5 --- arch/z8001/local2.c | 10 ++++++ arch/z8001/macdefs.h | 5 +++ arch/z8001/table.c | 79 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+) diff --git a/arch/z8001/local2.c b/arch/z8001/local2.c index 1576c3db8..f650acbea 100644 --- a/arch/z8001/local2.c +++ b/arch/z8001/local2.c @@ -1163,6 +1163,16 @@ special(NODE *p, int shape) 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; } return SRNOPE; } diff --git a/arch/z8001/macdefs.h b/arch/z8001/macdefs.h index b3fa8377a..ff4d0bf25 100644 --- a/arch/z8001/macdefs.h +++ b/arch/z8001/macdefs.h @@ -391,6 +391,11 @@ int COLORMAP(int c, int *r); 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 */ /* * sizeof and pointer-difference results are plain 16-bit integers, NOT diff --git a/arch/z8001/table.c b/arch/z8001/table.c index 7217abf13..7cf1aafec 100644 --- a/arch/z8001/table.c +++ b/arch/z8001/table.c @@ -517,6 +517,52 @@ struct optab table[] = { 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", }, + /* add pair + pair (long/ptr); pointer offsets are widened to a pair */ { PLUS, INBREG|FOREFF, SBREG, TLONG|TULONG|TPOINT, @@ -590,6 +636,39 @@ struct optab table[] = { 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", }, + /* subtract pair (long/ptr); pointer offsets are widened to a pair */ { MINUS, INBREG|FOREFF, SBREG, TLONG|TULONG|TPOINT, From b7f1d6d27f1f5dd3486d7079fe8895f9e6c30a29 Mon Sep 17 00:00:00 2001 From: Michal Pleban Date: Tue, 7 Jul 2026 13:24:48 +0200 Subject: [PATCH 25/67] z8001: byte-op rules - clrb, char-mask andb, subb high-byte zero-extend clrb: ASSIGN char <- SZERO rules on SDREG/SNAME/SNBA, placed before the generic SCON assigns. clrb (as S_CLR, dst R/IR/DA/X) sets no flags, so like ldk the rules carry no FORCC/RESCC and compare-context assigns fall back to the generic ldb. andb: char-typed AND rules (SDREG dst; SDREG/SNAME/SCON and SNBA src) for the char-mask truth tests that clocal narrows back to byte width. andb is S_RSRC (dst register, src R/IM/IR/DA/X); a byte immediate is replicated into both halves by as, the cpb Rb,IM encoding path. RESCC is consumed by eq/ne only (Z correct, P/V parity - same as testb), which the narrowing guarantees by construction. subb: uchar->int zero-extension now emits the 2-byte native idiom "subb rhN,rhN" via the new ZU zzzcode when the result word has an addressable high byte (r0-r7), falling back to the 4-byte word "and A1,$0xff" for r8-r12. Native golden counts: clrb 124, andb 72 (all mask truth tests feeding eq/ne), subb 90 (all rh self-subtract zero-extends). Co-Authored-By: Claude Fable 5 --- arch/z8001/local2.c | 16 +++++++++++++++ arch/z8001/table.c | 47 +++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/arch/z8001/local2.c b/arch/z8001/local2.c index f650acbea..f4f488f82 100644 --- a/arch/z8001/local2.c +++ b/arch/z8001/local2.c @@ -461,6 +461,8 @@ quadmem(NODE *mem, int q, int store) * 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 @@ -498,6 +500,20 @@ zzzcode(NODE *p, int c) 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 '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 diff --git a/arch/z8001/table.c b/arch/z8001/table.c index 7cf1aafec..116280d6d 100644 --- a/arch/z8001/table.c +++ b/arch/z8001/table.c @@ -194,12 +194,14 @@ struct optab table[] = { NEEDS(NREG(A, 1), NSL(A)), RESC1, "ZK extsb A1\n", }, -/* unsigned char (byte reg) -> (u)int: zero-extend via word and */ +/* 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, - "ZK and A1,$0xff\n", }, + "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 @@ -928,6 +930,25 @@ struct optab table[] = { 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. */ +{ 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. */ @@ -1110,6 +1131,28 @@ struct optab table[] = { * 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", }, + /* byte/char reg <- reg, mem or const (byte registers print as rlN) */ { ASSIGN, FOREFF|INDREG|FORCC, SDREG, TCHAR|TUCHAR, From 0019c3dd4867b267d8a5bc5eab8f217d4c713203 Mon Sep 17 00:00:00 2001 From: Michal Pleban Date: Tue, 7 Jul 2026 13:25:17 +0200 Subject: [PATCH 26/67] z8001: clocal narrows char compares and mask truth tests to byte width Extend the session-14 EQ/NE char narrowing to the ordered compares and to AND-mask truth tests, unlocking the cpb/andb rules: Ordered compares vs a nameless ICON: sign-extension preserves signed order, so a signed char keeps LT/LE/GT/GE (constant must fit -128..127). Zero-extension puts both sides in 0..255 where the signed word compare equals the UNSIGNED byte compare, so uchar flips the operator to ULT/ULE/UGT/UGE (constant 0..255); an already-unsigned compare keeps its operator. A sign-extended char under an unsigned word compare has no byte equivalent and is left alone. Mask truth tests: EQ/NE(AND(SCONV(char->int), ICON m), ICON k) with m,k both 0..255 - the mask clears every promoted high bit regardless of how the char extended, so equality is decided by the low byte alone. Strip the SCONV and retype the AND to the char type; pass 2 then emits "andb rlN,$m" whose Z flag feeds the eq/ne branch directly. Constant-to-the-right canonicalization on narrowed compares: the parser builds a>b / a<=b as b=a (cgram.y eve C_GT/C_LE swap), leaving the constant on the LEFT where pass 2 has no imm-compare rule and would materialize it into a byte register. Swapping the (side-effect-free) constant back reverses the ordered operator; EQ/NE are symmetric. "if (gc > 'z')" is now "cpb gc_,$122; jr le" like native instead of "ldb rl0,$122; cpb rl0,gc_; jr ge". Verified: 7-suite -O0 diffs are pure byte-width substitutions (misctest range checks 8->2 insns); sweepO1 106 status-identical (100 OK); O1 build 13/13; emulator 7 suites x {O0,O1} ALL TESTS PASSED (262 ok each); echo/wc/tail/sort -O1 byte-identical vs native /bin. Gap vs native goldens 49253 -> 49068 insns (1.086x -> 1.082x); we now emit clrb 324 / andb 122 / subb 110 / cpb 416. Repro ztests/real/e55.c. Co-Authored-By: Claude Fable 5 --- arch/z8001/local.c | 122 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 110 insertions(+), 12 deletions(-) diff --git a/arch/z8001/local.c b/arch/z8001/local.c index 1e106856f..2b3787ffa 100644 --- a/arch/z8001/local.c +++ b/arch/z8001/local.c @@ -168,27 +168,82 @@ clocal(NODE *p) break; case EQ: - case NE: { + case NE: + case LT: + case LE: + case GT: + case GE: + case ULT: + case ULE: + case UGT: + case UGE: { /* - * Narrow a char equality 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. For EQ/NE the promotion is value-preserving - * in both directions whenever the constant fits the char's - * value range, so strip the SCONV and retype the constant - * to the char type. Ordered compares are NOT narrowed: - * an unsigned char would also need the operator flipped - * to the unsigned condition. + * 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; sc = p->n_left; cn = p->n_right; - if (sc->n_op != SCONV && cn->n_op == SCONV) { + if (cn->n_op != ICON && sc->n_op == ICON) { sc = p->n_right; cn = p->n_left; } - if (sc->n_op != SCONV || cn->n_op != ICON || cn->n_sp != NULL) + 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; + if (p->n_left == cn) { + /* constant to the right so the zero-compare + * elision and imm-compare shapes match; + * EQ/NE are symmetric */ + p->n_left = sc; + p->n_right = cn; + } + break; + } + + if (sc->n_op != SCONV) break; if (sc->n_type != INT && sc->n_type != UNSIGNED) break; @@ -203,12 +258,55 @@ clocal(NODE *p) 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; + } + } if (p->n_left == sc) p->n_left = sc->n_left; else p->n_right = sc->n_left; nfree(sc); cn->n_type = m; + if (p->n_left == cn) { + /* + * Constant to the right: the parser builds a>b and + * a<=b as b=a, leaving the constant on the + * left where pass 2 has no imm-compare rule and + * would materialize it into a byte register. + * Swapping a (side-effect-free) constant reverses + * the ordered operator; EQ/NE are symmetric. + */ + 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; + } + } break; } From a24b2d237e0c0f90ea8b1d29da9e94b61bd5ebcc Mon Sep 17 00:00:00 2001 From: Michal Pleban Date: Tue, 7 Jul 2026 14:28:13 +0200 Subject: [PATCH 27/67] regs.c: PICKCOLOR and SPILLSHADOW target hooks for AssignColors PICKCOLOR: colfind's fallback takes the lowest set bit of okColors, which allocates callee-save registers bottom-up. Targets whose prologue saves one contiguous register block (cost set by the lowest callee-save register used) want the opposite order, so let the target pick the color from the mask. Move-related recommendations still take precedence; without the macro the ffs behavior is unchanged. SPILLSHADOW: a permreg shadow whose own register was taken by a value would color into ANOTHER callee-save register, emitting entry/exit reg-reg moves and dragging that register into the save set. The old bottom-up order only avoided this by accident: values collided with the lowest shadows, whose okColors then came up empty and forced the spill path (nsavregs -> prologue save, zero moves). With a target- directed color order that accident no longer happens, so let the target declare that a shadow losing its register must always spill; the existing ONLYPERM rewrite then marks it in nsavregs and recolors. Both hooks are inert for targets that do not define the macros. Co-Authored-By: Claude Fable 5 --- mip/regs.c | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/mip/regs.c b/mip/regs.c index b8bbcf70a..ad4263f5d 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 From 4adb918505718b3c2ecfa4436980a3e783af1e61 Mon Sep 17 00:00:00 2001 From: Michal Pleban Date: Tue, 7 Jul 2026 14:28:35 +0200 Subject: [PATCH 28/67] z8001: callee-save registers allocated top-down (family #6) Define PICKCOLOR/SPILLSHADOW (new regs.c hooks) and add pickcolor(): caller-saved registers keep their lowest-first order, callee-saved ones are preferred TOP-DOWN - words r12..r6, pairs rr10,rr8,rr6, quads rq0,rq8,rq4 (rq4 overlaps r6/r7), bytes rl7 before rl6. The prologue saves one contiguous ldm block from the lowest used callee register through r13, so its stack/cycle cost is set by that lowest register alone; picking from the top keeps the block minimal. SPILLSHADOW makes a displaced permreg shadow spill into that ldm block (free) instead of parking in another callee register behind entry/exit moves. Effect on the 73-file golden sweep: prologue starts were 690x r6 (everything saved r6-r13); now r8:213, r6:184, rr10:152, r9:91, r7:50 - the common case matches native's "ldm r8,$6" or better. Insn gap 48854 vs 49068 (1.082x -> 1.078x): the -214 are eliminated entry/exit shadow moves. 7-suite -O0 diffs are pure register renames plus smaller ldm/frames (insn count identical); sweepO1 106 status-identical (100 OK); O1 build 13/13; emulator 7 suites x {O0,O1} ALL TESTS PASSED (262 ok each); echo/wc/tail/sort -O1 byte-identical vs native /bin. Repro ztests/real/e56.c (ldm r6,$8 -> ldm r9,$5, no moves). Co-Authored-By: Claude Fable 5 --- arch/z8001/local2.c | 46 ++++++++++++++++++++++++++++++++++++++++++++ arch/z8001/macdefs.h | 12 ++++++++++++ 2 files changed, 58 insertions(+) diff --git a/arch/z8001/local2.c b/arch/z8001/local2.c index f4f488f82..da02c0afe 100644 --- a/arch/z8001/local2.c +++ b/arch/z8001/local2.c @@ -1070,6 +1070,52 @@ COLORMAP(int c, int *r) 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 diff --git a/arch/z8001/macdefs.h b/arch/z8001/macdefs.h index ff4d0bf25..46e6d2e7e 100644 --- a/arch/z8001/macdefs.h +++ b/arch/z8001/macdefs.h @@ -336,6 +336,18 @@ typedef long long OFFSZ; 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 */ From 2cf534a4855385ac901a2fa9bc8a4862eb158b79 Mon Sep 17 00:00:00 2001 From: Michal Pleban Date: Tue, 7 Jul 2026 16:46:43 +0200 Subject: [PATCH 29/67] reader/match: CCOKFORCOMP elision gate hook, pure-FORCC findops fix Two mip changes serving condition-code correctness on targets whose instructions set only a subset of the flags: 1. geninsn's compare-vs-zero elision (computing the compare's child with FORCC so the child insn's own flags feed the branch) fired for ALL ten compare ops on any FLO/DIV/SIMP/SHF child with a FORCC rule. That is only correct when the child instruction sets every flag the branch condition reads; on machines where e.g. the word logical ops leave the overflow flag untouched, an ordered signed branch after an elided compare reads a stale flag. New target hook CCOKFORCOMP(compareop, childop) vetoes such pairs; the default (1) keeps the historic behavior for all targets. 2. findops returned ffs(cookie & visit & INREGS)-1 as the matched node's result class. For a rule visited ONLY for its condition codes (visit FORCC, no result register - e.g. pdp11's "bit" test rules, which exist exactly for the elision path above) the mask is empty and the return value ffs(0)-1 = -1 collides with FFAIL, so the caller treats the successful match as a failure and the rule can never be selected. Return 0 for such rules (the established no-result value, as for FOREFF) and accept 0 in the elision check. --- mip/match.c | 8 ++++++-- mip/reader.c | 21 +++++++++++++++++++-- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/mip/match.c b/mip/match.c index 911c977b1..f03a387d6 100644 --- a/mip/match.c +++ b/mip/match.c @@ -720,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; } diff --git a/mip/reader.c b/mip/reader.c index 2bad7ff9f..8133174f6 100644 --- a/mip/reader.c +++ b/mip/reader.c @@ -902,6 +902,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) { @@ -932,12 +946,15 @@ again: switch (o = p->n_op) { 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))) { + (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 } From 6f1b2b4139d0677588908b555113dfa195077eb0 Mon Sep 17 00:00:00 2001 From: Michal Pleban Date: Tue, 7 Jul 2026 16:47:27 +0200 Subject: [PATCH 30/67] z8001: gate compare-vs-zero elision to flags the child insn really sets Wrong-code fix: the word logical instructions AND/OR/XOR leave P/V unaffected (manual: "P: AND -unaffected", same for OR/XOR; the byte forms set it to parity), yet their table rules claim FORCC, so an ordered signed compare against zero elided onto them read a stale V: "if ((x & -2) < 0)" compiled to "and r0,$-2; jr ge" where ge = S xor V inverts whenever the last V-setting insn left V=1. Only LT/GE were reachable (the parser swaps GT/LE operands, leaving the constant on the left where the elision cannot fire); no site exists in the sweep corpus, so this never bit at runtime. Define CCOKFORCOMP (the new mip hook) to allow the elision for EQ/NE always (they read Z alone, correct after every FORCC rule we have) and for PLUS/MINUS children under any compare (add/sub/inc/dec set V arithmetically, which is exactly the signed compare against zero). Ordered compares on logical results now emit their cp. Repro ztests/real/e57.c. 7-suite -O0 output unchanged by the gate itself; all suites pass in the emulator at -O0 and -O1. --- arch/z8001/macdefs.h | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/arch/z8001/macdefs.h b/arch/z8001/macdefs.h index 46e6d2e7e..1093db666 100644 --- a/arch/z8001/macdefs.h +++ b/arch/z8001/macdefs.h @@ -408,6 +408,19 @@ int pickcolor(int class, int mask); #define SPCON (MAXSPECIAL+6) /* nameless ICON representable as a word immediate (-32768..65535): pointer offset arithmetic on the pair's low word */ +/* + * 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) /* * sizeof and pointer-difference results are plain 16-bit integers, NOT From 9dff3369b4741cd283ef81487931a5ccabe3f784 Mon Sep 17 00:00:00 2001 From: Michal Pleban Date: Tue, 7 Jul 2026 16:48:21 +0200 Subject: [PATCH 31/67] z8001: bit/bitb rules for single-bit truth tests (family #7) "(x & pow2) ==/!= 0" now compiles to a single bit/bitb test instead of computing the and. BIT sets ONLY Z (with the same sense as the and: Z = 1 iff the tested bit is 0), so the rules are pure-FORCC and are reachable exclusively through the compare-vs-zero elision, which the CCOKFORCOMP gate restricts to EQ/NE consumers for logical children - an ordered branch can never read bit's unset S/V. New SPOW2 special shape (nameless ICON that is a single-bit mask in its type's width, either sign representation: bit 15 may arrive as 32768 or -32768) and ZJ zzzcode printing the mask as a bit number. Four rules: word SAREG|SNAME + SNBA, char SDREG|SNAME + SNBA, placed before the general and/andb rules (first match at equal shape level). Wins per site: bit is 2 bytes vs 4 for and-immediate; the register form leaves the operand intact, killing the copy the destructive and needed when the value stays live; the memory forms (dst R/IR/DA/X per the manual and as S_BIT - no BA) absorb the operand load outright: ld r0,gw_; and r0,$32768; jr eq -> bit gw_,$15; jr eq ldb rl0,(rr2); andb rl0,$128; .. -> bitb (rr2),$7; jr eq 188 sites fire across the -O1 sweep (103 bit + 85 bitb). Legality: manual 3-28 (BIT dst R/IR/DA/X, Z only) + as machine.c S_BIT imm path (ROK|IROK|DAOK|XOK). bit IR,IM / bitb Rb,IM / bitb DA,IM / bitb IR,IM are corpus-novel (whitelisted); bit R/DA/X and bitb X exist in native output. Repro ztests/real/e58.c (5 firing forms + 3 must-not-fire controls). Verified: audit+shapediff clean, sweepO1 100 OK status-identical, O1 build 13/13, emulator 7 suites x {O0,O1} ALL TESTS PASSED (262 ok) and echo/wc/tail/sort at -O1 byte-identical to native /bin. --- arch/z8001/local2.c | 38 ++++++++++++++++++++++++++++++++++++++ arch/z8001/macdefs.h | 4 ++++ arch/z8001/table.c | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+) diff --git a/arch/z8001/local2.c b/arch/z8001/local2.c index da02c0afe..4d5f83528 100644 --- a/arch/z8001/local2.c +++ b/arch/z8001/local2.c @@ -514,6 +514,24 @@ zzzcode(NODE *p, int c) printf("\tand\t%s,$0xff\n", rnames[n]); break; + case 'J': /* bit number of the single-bit mask in the right + * ICON (an SPOW2 shape), for the bit/bitb test + * rules: "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 '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 @@ -1235,6 +1253,26 @@ special(NODE *p, int shape) getlval(p) >= -32768 && getlval(p) <= 65535) 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; + } } return SRNOPE; } diff --git a/arch/z8001/macdefs.h b/arch/z8001/macdefs.h index 1093db666..26be18bb2 100644 --- a/arch/z8001/macdefs.h +++ b/arch/z8001/macdefs.h @@ -408,6 +408,10 @@ int pickcolor(int class, int mask); #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 */ + /* * Compare-vs-zero elision gate (reader.c geninsn): the word logical * instructions AND/OR/XOR leave P/V unaffected (and the byte forms set diff --git a/arch/z8001/table.c b/arch/z8001/table.c index 116280d6d..d43dee226 100644 --- a/arch/z8001/table.c +++ b/arch/z8001/table.c @@ -906,6 +906,28 @@ struct optab table[] = { * 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", }, + { AND, INAREG|FOREFF|FORCC, SAREG, TWORD, SAREG|SNAME|SCON, TWORD, @@ -937,6 +959,21 @@ struct optab table[] = { * (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, From fa5b65cf5931203ab203c6e998d05e4fd53b9d42 Mon Sep 17 00:00:00 2001 From: Michal Pleban Date: Tue, 7 Jul 2026 16:48:54 +0200 Subject: [PATCH 32/67] z8001: variable pointer index in the word domain (family #2 extension) The front end converts the index of pointer arithmetic to the 32-bit offset type before scaling, so pass 2 paired it up and added with addl: "ld r3,r0; exts rr2; slll rr2,$2; addl rr4,rr2" where native emits "sla r9,$2; add r11,r9". Under the Coherent segmented assumption (no object crosses a segment boundary - the user-approved license already recorded on the TPOINT SP16/SPCON constant rules) only the low 16 bits of the index ever reach the pointer's offset word, so the widen is dead. clocal now rewrites the index subtree of a pointer-typed PLUS/MINUS to word type (new offword() helper): SCONV from int/unsigned/short is stripped, SCONV from char/uchar becomes the word widen, and constant LS/MUL scaling moves into the word domain with it (low 16 shift and product bits are identical; struct-array indexing turns multl into the native word mult). POINTERS ONLY - a plain long +/- int keeps exts+addl, its 32-bit carry is real arithmetic. New TPOINT table rules "add/sub UL,AR" (SAREG|SNAME + SNBA, TWORD|TSHORT|TUSHORT - add/sub src is R/IM/IR/DA/X, no BA) do the offset-word add; a memory index even feeds it directly (add r3,name_). ~310 sites fire across the -O1 sweep: exts drops 750 -> 136, multl 147 -> 35. Whole-corpus gap vs the native goldens 48854 -> 47979 insns = 1.078x -> 1.058x (with the bit rules); crypt 1.23 -> 1.12. Repro ztests/real/e59.c (char/int/long/struct/uchar/short index, p[i] loads, *pp += i memory RMW, plus long controls that must keep the pair arithmetic). Verified: 7-suite -O0 diffs are pure family substitutions (shifttest/strret byte-identical), audit+shapediff clean, sweepO1 100 OK status-identical, O1 build 13/13, emulator 7 suites x {O0,O1} ALL TESTS PASSED (262 ok) and echo/wc/tail/sort at -O1 byte-identical to native /bin. --- arch/z8001/local.c | 79 ++++++++++++++++++++++++++++++++++++++++++++++ arch/z8001/table.c | 33 +++++++++++++++++++ 2 files changed, 112 insertions(+) diff --git a/arch/z8001/local.c b/arch/z8001/local.c index 2b3787ffa..4ba0247f2 100644 --- a/arch/z8001/local.c +++ b/arch/z8001/local.c @@ -46,6 +46,69 @@ #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; +} + /* * clocal: perform local pass-1 transformations. * @@ -167,6 +230,22 @@ clocal(NODE *p) } 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: diff --git a/arch/z8001/table.c b/arch/z8001/table.c index d43dee226..d60a41563 100644 --- a/arch/z8001/table.c +++ b/arch/z8001/table.c @@ -565,6 +565,26 @@ struct optab table[] = { 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", }, + /* add pair + pair (long/ptr); pointer offsets are widened to a pair */ { PLUS, INBREG|FOREFF, SBREG, TLONG|TULONG|TPOINT, @@ -671,6 +691,19 @@ struct optab table[] = { 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, From 8cae72869d578ec5be09ab9e231848c6e931ea5d Mon Sep 17 00:00:00 2001 From: Michal Pleban Date: Tue, 7 Jul 2026 19:29:55 +0200 Subject: [PATCH 33/67] z8001: put compare constants on the right for every width (family #8) The parser builds a>b and a<=b as b=a (cgram.y eve()), leaving the constant on the left of the compare where no imm-compare rule matches: pass 2 materialized it into a register ("ldk r1,$0; cp r1,r0; jr ge" for "x > 0" - 2 wasted insns and a register). The session-15 swap-back covered only narrowed byte compares; generalize it: clocal now swaps any constant-left / non-constant-right compare operands, reversing the ordered operator (EQ/NE are symmetric), for word, pair and byte compares alike. The byte-narrowing path's own tail swaps become unreachable and are removed. "Constant" includes the pre-fold ADDROF(NAME) form of an address constant (conaddr(), andable-gated): optim folds "sp > fname" arrays to a named ICON only after the compare has been through clocal. Safe post-CCOKFORCOMP: GT(AND(x,m),0) after the swap emits "and; cp r0,$0; jr gt" because the ordered elision onto the AND is gated off - still one insn shorter than the old ldk detour. Kills 259 of 267 detour sites in the -O1 corpus sweep (native has zero); the 8 leftovers are deljumps artifacts fixed separately. Native at the same sites uses exactly these imm compares (cp N,$k 634x, cpl 54x, incl. "cp rN,$0; jr le/gt" for the swapped >0/<=0 forms). Repro ztests/real/e60.c. 7-suite -O0 diffs are pure ldk/ld+cp -> cp-imm substitutions; audit+shapediff clean (all shapes corpus- present); sweepO1 100 OK status-identical; O1 build 13/13. --- arch/z8001/local.c | 82 +++++++++++++++++++++++++--------------------- 1 file changed, 45 insertions(+), 37 deletions(-) diff --git a/arch/z8001/local.c b/arch/z8001/local.c index 4ba0247f2..cb9ea637b 100644 --- a/arch/z8001/local.c +++ b/arch/z8001/local.c @@ -109,6 +109,23 @@ offword(NODE *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. * @@ -274,12 +291,35 @@ clocal(NODE *p) */ NODE *sc, *cn; - sc = p->n_left; - cn = p->n_right; - if (cn->n_op != ICON && sc->n_op == ICON) { - sc = p->n_right; + /* + * 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; @@ -312,13 +352,6 @@ clocal(NODE *p) sc->n_type = m; msk->n_type = m; cn->n_type = m; - if (p->n_left == cn) { - /* constant to the right so the zero-compare - * elision and imm-compare shapes match; - * EQ/NE are symmetric */ - p->n_left = sc; - p->n_right = cn; - } break; } @@ -358,34 +391,9 @@ clocal(NODE *p) p->n_op = UGE; } } - if (p->n_left == sc) - p->n_left = sc->n_left; - else - p->n_right = sc->n_left; + p->n_left = sc->n_left; nfree(sc); cn->n_type = m; - if (p->n_left == cn) { - /* - * Constant to the right: the parser builds a>b and - * a<=b as b=a, leaving the constant on the - * left where pass 2 has no imm-compare rule and - * would materialize it into a byte register. - * Swapping a (side-effect-free) constant reverses - * the ordered operator; EQ/NE are symmetric. - */ - 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; - } - } break; } From cc24251aa5dc3df8ae82a98e6be2bd018b9d7bd0 Mon Sep 17 00:00:00 2001 From: Michal Pleban Date: Tue, 7 Jul 2026 19:30:11 +0200 Subject: [PATCH 34/67] z8001: sign-only test/testl for ordered compares against zero LT and GE against 0 are decided by the S flag alone. TEST sets S and Z correctly (dst OR 0; dst R/IR/DA/X) but leaves P/V as parity, so the generic lt/ge conditions (S xor V) cannot be used after it - the same V-staleness that motivated CCOKFORCOMP. The MI/PL conditions read S alone (manual: PL S=0 code 1101, MI S=1 code 0101), making "test AL; jr mi/pl" the correct sign test; native cc uses this idiom at 169 word + 8 long sites. GT/LE also need Z and a valid V and keep cp/cpl. New LT/GE-vs-SZERO FORCC rules (word SAREG|SNAME + SNBA, pair SBREG|SNAME + SNBA) emit test/testl plus a new ZO template escape; ZO sets a flag that the immediately following cbgen call consumes to print mi/pl instead of lt/ge. The handoff is safe because cbgen has a single call site (reader.c emit CBRANCH), directly after the FORCC gencode that expanded the template, and OPLOG nodes only reach pass 2 under CBRANCH. Elided compares (PLUS/MINUS children) never run ZO and keep lt/ge, which add/sub set correctly. Covers all 439 ordered-vs-zero sites in the -O1 corpus (100% were lt/ge): insn-neutral, 2 bytes per site, and the mem forms (test DA/X, testl DA/X/IR) absorb a load like the EQ/NE test rules do. All emitted shapes are corpus-present; jr mi/pl is native idiom. Repro ztests/real/e60.c (with the const-swap). 7-suite -O0 diffs = pure cp$0+lt/ge -> test+mi/pl substitutions; audit+shapediff clean; sweepO1 100 OK status-identical; O1 build 13/13. --- arch/z8001/local2.c | 30 +++++++++++++++++++++++- arch/z8001/table.c | 57 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 1 deletion(-) diff --git a/arch/z8001/local2.c b/arch/z8001/local2.c index 4d5f83528..1790bed20 100644 --- a/arch/z8001/local2.c +++ b/arch/z8001/local2.c @@ -471,7 +471,14 @@ quadmem(NODE *mem, int q, int store) * 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 */ + +/* set by ZO, consumed by the cbgen call that follows the compare */ +static int signonlycc; + void zzzcode(NODE *p, int c) { @@ -514,6 +521,13 @@ zzzcode(NODE *p, int c) printf("\tand\t%s,$0xff\n", rnames[n]); 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 'J': /* bit number of the single-bit mask in the right * ICON (an SPOW2 shape), for the bit/bitb test * rules: "x & 0x40" prints as "$6". */ @@ -1013,9 +1027,23 @@ static char *ccnames[] = { void cbgen(int o, int lab) { + char *cc; + if (o < EQ || o > UGT) comperr("cbgen: bad op %d", o); - printf("\tjr\t%s," LABFMT "\n", ccnames[o - EQ], lab); + 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); } /* diff --git a/arch/z8001/table.c b/arch/z8001/table.c index d60a41563..c51c8d7d9 100644 --- a/arch/z8001/table.c +++ b/arch/z8001/table.c @@ -1406,6 +1406,63 @@ struct optab table[] = { 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, From bd49863122f4bfbad3460418ef2aadaae2c292e9 Mon Sep 17 00:00:00 2001 From: Michal Pleban Date: Tue, 7 Jul 2026 19:30:18 +0200 Subject: [PATCH 35/67] deljumps: negate the operator when inverting a branch, keep operands The c2-derived branch inversion in iterate() (collapsing "cbr L1; jbr L2; L1:") negated LE/GT/ULE/UGT by SWAPPING THE COMPARE OPERANDS and using the reversed operator (LE -> swap+LT etc.) instead of the direct negation (LE -> GT). Truth-preserving, but it physically moves a constant operand back to the left of the compare, where backends have no imm-compare match and must materialize the constant into a register - undoing any pass-1 canonicalization. Only branch-if-true shapes (if (e) goto L, loop exits) produce the cbr/jbr/label pattern; plain if/else negates in pass 1, which is why the damage was sporadic (8 sites in the Coherent corpus). Use the direct negations GT/LE/UGT/ULE with operand order preserved; every backend's cbgen handles all ten conditions. Removes the SWAP macro and the now-unused NODE *r. Repro ztests/real/e61.c: "if ('9'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"); } From 608f858a549abf5dba31b2b1500c37eaae87f7b7 Mon Sep 17 00:00:00 2001 From: Michal Pleban Date: Tue, 7 Jul 2026 19:30:18 +0200 Subject: [PATCH 36/67] regalloc: clear precolored move lists in the onlyperm reset The onlyperm/recalc restart memsets the nblock temporaries but only cleared the precolored ablock nodes' NCLASS counts, leaving their r_moveList entries from the previous round in place. A second-round moveadd() whose DEF is a precolored register (the return-value move "ASSIGN(REG r1, TEMP)" that FORCE generates) then finds the stale entry in the already-there check and returns without re-inserting the move into worklistMoves - the temp is no longer move-related, gets simplified early, and colors away from the return register. Visible as "clr r0; ld r1,r0" before the epilogue for "return 0" in every function that entered the ONLYPERM round (i.e. saves callee registers); shadow moves were unaffected because their def is an nblock node whose list the memset had cleared. The moveadd "XXX How can that happen ???" duplicate check is exactly what this tripped over. Clear the ablock move lists in the same reset loop, mirroring the nblock memset, so round 2 rebuilds the move worklist from scratch. Repro ztests/real/e62.c (-xtemps -xdeljumps -xinline -xdce -xssa): epilogue now "clr r1", no move; 20 such sites in the Coherent corpus. --- mip/regs.c | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/mip/regs.c b/mip/regs.c index ad4263f5d..9181f0093 100644 --- a/mip/regs.c +++ b/mip/regs.c @@ -3048,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)); From c3bc192854f3b33d61447f981a16f87ce7d33dd4 Mon Sep 17 00:00:00 2001 From: Michal Pleban Date: Tue, 7 Jul 2026 22:28:49 +0200 Subject: [PATCH 37/67] relops: honor the cookie, return the result class Historically every relop table rule was visited FORCC only and relops() ignored its context entirely: no visit filtering, and a hardcoded "return 0". A target keeping relationals in value context (see the KEEPLOGOPVALUE hook) adds OPLOG rules that materialize the 0/1 result in a register, and those must not be picked under a branch nor a flags-only rule where a result is wanted. - pass the cookie down from geninsn and skip rules whose visit mask does not intersect it (FOREFF bypasses, preserving the historic any-rule behavior for statement relops); - compute the result class for RESC1-style value rules from the rule's visit mask, exactly as findops does; - return the class instead of 0: shswitch feeds geninsn's return value to a rewriting parent, so a value relop under an RLEFT parent would otherwise leave that parent classless (n_reg -1, garbage register in the emitted move); - gate the compare-vs-zero elision on cookie == FORCC: in value context the child's flags alone produce no register result. FORCC behavior is unchanged: all existing rules keep matching, and the FORCC-path return value is not consumed by any caller. --- mip/match.c | 20 ++++++++++++++++++-- mip/pass2.h | 2 +- mip/reader.c | 9 +++++++-- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/mip/match.c b/mip/match.c index f03a387d6..377e116ae 100644 --- a/mip/match.c +++ b/mip/match.c @@ -754,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; @@ -776,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 */ @@ -820,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/pass2.h b/mip/pass2.h index d4fec3b6f..72abae584 100644 --- a/mip/pass2.h +++ b/mip/pass2.h @@ -245,7 +245,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 8133174f6..1960bc85f 100644 --- a/mip/reader.c +++ b/mip/reader.c @@ -945,7 +945,12 @@ 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 && + /* 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? */ @@ -958,7 +963,7 @@ again: switch (o = p->n_op) { break; #endif } - rv = relops(p); + rv = relops(p, cookie); break; case PLUS: From 9e2022a719048a458513b2a9248271b3de5a1bc8 Mon Sep 17 00:00:00 2001 From: Michal Pleban Date: Tue, 7 Jul 2026 22:28:49 +0200 Subject: [PATCH 38/67] reader: elide the compare against a value just stored For CBRANCH(relop(ASSIGN(x, op(x, e)), 0)) - built by pass1 for "if ((x = expr))", or by a target's myoptim fusing an assignment with the following compare - a read-modify-write match (findmops) computes the stored value, so its flags ARE the compare result: "dec r5,$1 ; jr ne" needs no separate test. Same contract as the existing plain-op elision, with CCOKFORCOMP keyed on the modifying op; findmops already accepted a FORCC cookie, this just wires the call. Also fix the ISMOPS rmove guard in gencode: "cookie != FOREFF" was a proxy for "a register result was requested", but an ASSIGN elided under a compare is evaluated with cookie 0 and has no result register at all - n_reg is -1 and DECRA of it names a garbage register. Test for (cookie & INREGS) instead; identical on all previously reachable paths. --- mip/reader.c | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/mip/reader.c b/mip/reader.c index 1960bc85f..a5b062a20 100644 --- a/mip/reader.c +++ b/mip/reader.c @@ -963,6 +963,25 @@ again: switch (o = p->n_op) { break; #endif } +#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; @@ -1377,7 +1396,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 From 334ac0416635bef612ffe39f1caab90a23fa63c0 Mon Sep 17 00:00:00 2001 From: Michal Pleban Date: Tue, 7 Jul 2026 22:28:58 +0200 Subject: [PATCH 39/67] pass1: KEEPLOGOPVALUE hook - keep value-context relationals in the tree rmcops() rewrites a relational used for its value (f = (a < b), return x == y) into a branch diamond storing 1/0 to a temp. A target whose instruction set materializes a truth value directly (Z8000 tcc) can now override KEEPLOGOPVALUE (default 0) to keep the bare relop in the tree; pass2 then matches it with a value-context OPLOG rule. ANDAND/OROR/NOT are still rewritten regardless: they require lazy evaluation. The operands of a kept relop are ordinary expressions and just get the recursive cleanup. --- cc/ccom/pass1.h | 12 ++++++++++++ cc/ccom/trees.c | 14 ++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/cc/ccom/pass1.h b/cc/ccom/pass1.h index c8654e0cd..52221c0c4 100644 --- a/cc/ccom/pass1.h +++ b/cc/ccom/pass1.h @@ -726,6 +726,18 @@ void dwarf_end(void); #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 fdb4ff7d2..ae35ff36f 100644 --- a/cc/ccom/trees.c +++ b/cc/ccom/trees.c @@ -2383,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: From e0e5bf6887a3c4403a54986f458a1251655b5596 Mon Sep 17 00:00:00 2001 From: Michal Pleban Date: Tue, 7 Jul 2026 22:30:52 +0200 Subject: [PATCH 40/67] z8001: tcc boolean materialization for value-context relationals f = (a < b), return x == y, and !x used as a value lowered to a five-insn branch diamond (compare; jr; ldk $1; jr; clr). Keep the relop in the tree (KEEPLOGOPVALUE, integer/pointer operands only - float and longlong compares have no value rules and keep the diamond) and materialize the truth value straight-line: cp AL,AR (test/testl/testb for EQ/NE vs zero, clr A1 cp/cpl/cpb otherwise; cp's V is valid tcc cc,A1 so ordered-vs-0 needs no mi/pl escape) TCC cc,Rd sets only bit 0 of Rd when cc holds - "all other bits in the destination are unaffected", "no flags affected", dst R only (manual, Test Condition Code) - 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; the compare reads first). The ZV escape prints the relop's own cc name, non-negated; all ten names are as S_CC entries. The result is always a word (relop type is INT), so the tcc is always the word form regardless of operand width. clocal folds NOT as well: !(a REL b) on integer operands becomes 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 and pessimize every branch (float compares keep their NOT for andorbr's label swap; ANDAND/OROR/NOT operands keep their lazy evaluation). Plain-scalar !e becomes EQ(e, 0) so it materializes through tcc too. Native cc never uses tcc (0 corpus uses): first beat-native family. 39 sites fire across the sweep at -O1; branch contexts are unchanged. Corpus-novel shape "tcc cc,R" whitelisted, audit.py checks the cc name and the register-only destination (as S_TCC: ROK|UP4). --- arch/z8001/local.c | 40 ++++++++++ arch/z8001/local2.c | 37 +++++---- arch/z8001/macdefs.h | 15 ++++ arch/z8001/table.c | 179 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 258 insertions(+), 13 deletions(-) diff --git a/arch/z8001/local.c b/arch/z8001/local.c index cb9ea637b..85ba9f87c 100644 --- a/arch/z8001/local.c +++ b/arch/z8001/local.c @@ -397,6 +397,46 @@ clocal(NODE *p) 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. diff --git a/arch/z8001/local2.c b/arch/z8001/local2.c index 1790bed20..06c2b5716 100644 --- a/arch/z8001/local2.c +++ b/arch/z8001/local2.c @@ -474,11 +474,27 @@ quadmem(NODE *mem, int q, int store) * 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) */ /* 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) { @@ -528,6 +544,14 @@ zzzcode(NODE *p, int c) 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: "x & 0x40" prints as "$6". */ @@ -1011,19 +1035,6 @@ adrput(FILE *io, NODE *p) /* * cbgen: emit a conditional branch. */ -static char *ccnames[] = { - "eq", /* EQ */ - "ne", /* NE */ - "le", /* LE */ - "lt", /* LT */ - "ge", /* GE */ - "gt", /* GT */ - "ule", /* ULE */ - "ult", /* ULT */ - "uge", /* UGE */ - "ugt", /* UGT */ -}; - void cbgen(int o, int lab) { diff --git a/arch/z8001/macdefs.h b/arch/z8001/macdefs.h index 26be18bb2..a36b4fcff 100644 --- a/arch/z8001/macdefs.h +++ b/arch/z8001/macdefs.h @@ -449,6 +449,21 @@ int pickcolor(int class, int mask); #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 diff --git a/arch/z8001/table.c b/arch/z8001/table.c index c51c8d7d9..bbd897e16 100644 --- a/arch/z8001/table.c +++ b/arch/z8001/table.c @@ -1548,6 +1548,185 @@ struct optab table[] = { 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. */ From 41085023f1d9866ab26edb77edcb10d1a3f7eeef Mon Sep 17 00:00:00 2001 From: Michal Pleban Date: Tue, 7 Jul 2026 22:31:54 +0200 Subject: [PATCH 41/67] z8001: set/res for single-bit or/and (family #9) x |= (1<n_right); @@ -570,6 +572,24 @@ zzzcode(NODE *p, int c) } 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 '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 @@ -1312,6 +1332,28 @@ special(NODE *p, int shape) 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; } diff --git a/arch/z8001/macdefs.h b/arch/z8001/macdefs.h index a36b4fcff..7e084445f 100644 --- a/arch/z8001/macdefs.h +++ b/arch/z8001/macdefs.h @@ -411,6 +411,11 @@ int pickcolor(int class, int mask); #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 */ /* * Compare-vs-zero elision gate (reader.c geninsn): the word logical diff --git a/arch/z8001/table.c b/arch/z8001/table.c index bbd897e16..117e1dfcb 100644 --- a/arch/z8001/table.c +++ b/arch/z8001/table.c @@ -961,6 +961,28 @@ struct optab table[] = { 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, @@ -1023,6 +1045,25 @@ struct optab table[] = { * 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, From 02daba8633c054d08f9a1b1e8b8396b99b94a24c Mon Sep 17 00:00:00 2001 From: Michal Pleban Date: Tue, 7 Jul 2026 22:32:27 +0200 Subject: [PATCH 42/67] z8001: lda X mode for address constant + word index (family #10) &garr[i] built the address with "ldl rrN,$garr_; add rN+1,rIDX". lda's Indexed mode encodes the whole thing in one instruction, "lda rrN,garr_(rIDX)" - the native compiler's own idiom (336 corpus uses). New PLUS(SCON ptr, SAREG word) rule with the ZX escape. X mode CANNOT encode index r0 (an index field of 0 decodes as DA - as machine.c outxx UP4), and a nameless address constant has no DA/X spelling: ZX emits the old ldl+add pair for those two cases, which is why this is a table rule and not a peephole. A1 never overlaps the index register (plain NBREG, no sharing), so the fallback's ldl cannot clobber the index before the add reads it. 240 lda X sites fire across the sweep at -O1; 93 sites keep the fallback because the allocator's lowest-first preference put the index in r0 - steering those is possible future allocator work. --- arch/z8001/local2.c | 31 +++++++++++++++++++++++++++++++ arch/z8001/table.c | 16 ++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/arch/z8001/local2.c b/arch/z8001/local2.c index 8703553d0..1f26ece29 100644 --- a/arch/z8001/local2.c +++ b/arch/z8001/local2.c @@ -478,6 +478,9 @@ quadmem(NODE *mem, int q, int store) * 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 */ @@ -590,6 +593,34 @@ zzzcode(NODE *p, int c) } 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]); + } + } + 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 diff --git a/arch/z8001/table.c b/arch/z8001/table.c index 117e1dfcb..aac6280b1 100644 --- a/arch/z8001/table.c +++ b/arch/z8001/table.c @@ -585,6 +585,22 @@ struct optab table[] = { 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 NBREG, no sharing), so the fallback's ldl + * cannot clobber the index before the add reads it. + */ +{ PLUS, INBREG, + SCON, TPOINT, + SAREG, TWORD|TSHORT|TUSHORT, + NBREG, RESC1, + "ZX", }, + /* add pair + pair (long/ptr); pointer offsets are widened to a pair */ { PLUS, INBREG|FOREFF, SBREG, TLONG|TULONG|TPOINT, From dc09a225f1ab0b1f2335ec74f4f7b629ee580766 Mon Sep 17 00:00:00 2001 From: Michal Pleban Date: Tue, 7 Jul 2026 22:32:39 +0200 Subject: [PATCH 43/67] z8001: fuse ASSIGN with the following compare of the stored value 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 pass1 builds for "if ((t = expr))". When expr is a read-modify-write of t the compare-vs-zero elision then branches on the operation's own flags: "dec r5,$1 ; jr ne" instead of dec; test; jr (gated per-op by CCOKFORCOMP - "and" plus an ordered compare keeps its test/jr pl). 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. Integer and pointer temps only: float compares are rewritten to fcomp helper calls and must keep their canonical shape. --- arch/z8001/local2.c | 59 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/arch/z8001/local2.c b/arch/z8001/local2.c index 1f26ece29..2c4aaa882 100644 --- a/arch/z8001/local2.c +++ b/arch/z8001/local2.c @@ -1415,10 +1415,67 @@ myxasm(struct interpass *ip, NODE *p) return 0; } -/* No target-specific pass2 optimisation. */ +/* + * 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. + */ +static void +fusecmp(struct interpass *ipole) +{ + struct interpass *ip, *inext; + 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; + DLIST_REMOVE(ip, qelem); + } +} + void myoptim(struct interpass *ip) { + fusecmp(ip); } /* From 108ce90a801198b77e9fb7677996e2aa6299a5bb Mon Sep 17 00:00:00 2001 From: Michal Pleban Date: Tue, 7 Jul 2026 22:32:39 +0200 Subject: [PATCH 44/67] z8001: shift-left-by-1 as self-add sll rN,$1 is 4 bytes; add rN,rN is 2 and is the native idiom (116 corpus uses; native emits sll $1 only 3 times). Same for the pair: slll rrN,$1 -> addl rrN,rrN. add sets all flags arithmetically for the doubled value, unlike sll (V untouched), so the word rule may claim FORCC/RESCC; the CCOKFORCOMP gate limits elided compares on an LS child to EQ/NE anyway. 44 word + 3 pair sites across the sweep. --- arch/z8001/table.c | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/arch/z8001/table.c b/arch/z8001/table.c index aac6280b1..2c0f2710f 100644 --- a/arch/z8001/table.c +++ b/arch/z8001/table.c @@ -853,6 +853,17 @@ struct optab table[] = { * 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, @@ -874,6 +885,13 @@ struct optab table[] = { 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, From 399903afab7d8a3e56d9ec12c3038c9eab8ae875 Mon Sep 17 00:00:00 2001 From: Michal Pleban Date: Tue, 7 Jul 2026 23:43:22 +0200 Subject: [PATCH 45/67] z8001: fusecmp must patch basic-block boundaries when unlinking The basic blocks built during optimize() are REUSED by the register allocator when xtemps is set (Build() only fakes a single block for xtemps == 0), and LivenessAnalysis walks each block from bb->last back to bb->first in the CIRCULAR interpass list. fusecmp unlinked the fused-away ASSIGN without looking at the blocks, so whenever that ASSIGN was a block's first element (the first statement after a label: ar.c update(), "p = namep[i]" at the top of the for body) the backward walk never met bb->first and ccom spun forever. Patch any bb->first/bb->last naming the removed element before the DLIST_REMOVE, under the same xtemps||xssa condition optimize() uses to build blocks at all. Repro ztests/real/e66.c (hangs only with preceding functions shifting the block layout; ar.c at any optimization level was the corpus trigger). --- arch/z8001/local2.c | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/arch/z8001/local2.c b/arch/z8001/local2.c index 2c4aaa882..27f3c88c3 100644 --- a/arch/z8001/local2.c +++ b/arch/z8001/local2.c @@ -1437,11 +1437,20 @@ myxasm(struct interpass *ip, NODE *p) * 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) { @@ -1468,6 +1477,15 @@ fusecmp(struct interpass *ipole) 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); } } From db116c14f93788de72e59038a4d19ad5f21dbe84 Mon Sep 17 00:00:00 2001 From: Michal Pleban Date: Wed, 8 Jul 2026 01:19:53 +0200 Subject: [PATCH 46/67] mip: implement tail merging (comjump/xjump) in deljumps Complete the c2-derived deljumps with its missing tail-merge half, present until now only as #ifdef notyet stubs. comjump() merges the identical statement tails of two jumps to the same label; xjump() cross-jumps a statement that is identical ahead of a jump and ahead of the jump's target. Both operate on the dlnod work list and the interpass list in step (codemove precedent). Statement equality (dltcmp) is deliberately strict, unlike the loose-by-design treecmp() in match.c: n_type must agree on every node, ops carrying state outside the common node fields (STASG, STARG, STCALL, USTCALL, STCLR, FLD, XASM, XARG) never merge, and unknown leaves compare unequal, since a false match here is wrong code, not a missed optimization. Tail merging is enabled only for the pre-regalloc deljumps runs (new file-static tailmerge flag, set around the two calls in optimize()): after ngenregs the trees carry n_reg/n_su and two structurally equal trees may still use different scratch registers. Both gated runs happen before bblocks_build, so no basic-block boundary patching is needed. New labels come from getlab2() and their interpasses from tmpalloc, which is why deljumps skips its markset/markfree rollback when tail merging is enabled - the label interpasses must survive the pass (add_labels allocates the same way); the work list then simply lives until the function's arena is freed. Corpus effect (Coherent cmd, 73 files vs native cc): 46947 -> 46260 instructions, gap 1.036x -> 1.020x; login 1.21 -> 1.19, mv 1.14 -> 1.12, tar 1.10 -> 1.09. The 7 synthetic suites are byte-identical at -O0 (no duplicated tails). Repro ztests/real/e67.c. --- mip/optim2.c | 227 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 218 insertions(+), 9 deletions(-) diff --git a/mip/optim2.c b/mip/optim2.c index 440a3f208..207ea48bc 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,6 +576,192 @@ 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) { @@ -661,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); } } @@ -674,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); @@ -683,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 From 881898eeaa8d46fa40e2791228c8d97d822eac71 Mon Sep 17 00:00:00 2001 From: Michal Pleban Date: Wed, 8 Jul 2026 01:39:35 +0200 Subject: [PATCH 47/67] z8001: recover the RMW compare elision at -O1 (rmwrename) At -O0 the fused countdown tree ASSIGN(t, op(t, e)) under a compare-vs-zero branches on the operation's own flags ("dec r5,$1 ; jr ne"). At -O1 SSA renames the two halves of the same tree to different temps, and after removephi the loop reads ASSIGN(s, x) entry copy L: CBRANCH(rel(ASSIGN(d, op(s, e)), 0)) ... body uses d ... ASSIGN(s, d) back-edge copy GOTO L so findmops' treecmp(d, s) can never match and the elision is lost; the register allocator does coalesce d and s into one register, but geninsn has already emitted the separate test by then (the 45 dec;test;jr + 8 sub + 3 and corpus sites, session 18 reality check). New myoptim pass rmwrename: when the whole-function interpass list proves this exact structure - d defined only by the RMW, s used only by the RMW, s defined by exactly two plain top-level temp-to-temp copies, one before the RMW sourcing neither temp and one after it reading d - rename s to d. The entry copy then initializes d, the back-edge copy becomes a self-copy and is deleted (basic-block boundaries patched, fusecmp precedent), and the RMW reads and writes one temp again, so the elision fires. The two-copy requirement pins the removephi phi-lowering shape in which every path back to the RMW passes one of the consistently-renamed copies; it rejects aliasing shapes like "while (n = m - 1)" where the source stays live, and do-while shapes where the copy precedes the branch. Gated on xssa (only SSA produces split temps, and the argument leans on removephi's copy-on-every-edge invariant). Runs after fusecmp, which rebuilds the fused tree for pairs SSA split completely apart. Repro ztests/real/e68.c (countdown, -=, &=, body-use, live-out, plus aliasing / do-while / two-backedge controls). --- arch/z8001/local2.c | 162 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 162 insertions(+) diff --git a/arch/z8001/local2.c b/arch/z8001/local2.c index 27f3c88c3..b8075ac4c 100644 --- a/arch/z8001/local2.c +++ b/arch/z8001/local2.c @@ -1490,10 +1490,172 @@ fusecmp(struct interpass *ipole) } } +/* + * 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); + } +} + void myoptim(struct interpass *ip) { fusecmp(ip); + rmwrename(ip); } /* From c5d0ab1bbe53b3b8c89451480e2faafdab59bd84 Mon Sep 17 00:00:00 2001 From: Michal Pleban Date: Wed, 8 Jul 2026 01:39:35 +0200 Subject: [PATCH 48/67] z8001: steer the lda X index away from r0 (NORIGHT on the ZX rule) The ZX rule (PLUS(SCON ptr, SAREG word) -> "lda A1,sym+off(rIDX)") falls back to ldl+add whenever the index register colors to r0, because an X-mode index field of 0 decodes as DA. pickcolor's caller-saved-lowest-first preference put the index temp in r0 at 93 of the 334 corpus sites (session 18). NORIGHT(R0) in the rule's needs adds one interference edge between the matched node's right operand and r0 (insnwalk cNOR -> addedge_r), which is the targeted constraint: only temps actually used as an lda X index lose r0, everything else keeps the existing preference order. This is the light needs form - unlike NLEFT/NRIGHT/NEVER it does not addalledges the physical register. --- arch/z8001/table.c | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/arch/z8001/table.c b/arch/z8001/table.c index 2c0f2710f..26345a4c0 100644 --- a/arch/z8001/table.c +++ b/arch/z8001/table.c @@ -592,13 +592,16 @@ struct optab table[] = { * 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 NBREG, no sharing), so the fallback's ldl - * cannot clobber the index before the add reads it. + * 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, - NBREG, RESC1, + NEEDS(NREG(B, 1), NORIGHT(R0)), RESC1, "ZX", }, /* add pair + pair (long/ptr); pointer offsets are widened to a pair */ From 41d23e38728541d0c7c875b014b72d9c1e77a0c3 Mon Sep 17 00:00:00 2001 From: Michal Pleban Date: Wed, 8 Jul 2026 01:49:12 +0200 Subject: [PATCH 49/67] cc/cpp, cc/ccom: open the output file in binary mode On Windows the text-mode stdout turns every line ending into CRLF. The Coherent assembler does not skip CR (its getnb treats it as an ordinary character), so every line of a driver-produced .s file was a syntax error. "wb" is a no-op on POSIX systems. --- cc/ccom/main.c | 3 ++- cc/cpp/cpp.c | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/cc/ccom/main.c b/cc/ccom/main.c index 34e833b25..5d25f7f16 100644 --- a/cc/ccom/main.c +++ b/cc/ccom/main.c @@ -274,7 +274,8 @@ main(int argc, char *argv[]) } } if (argc > 1 && strcmp(argv[1], "-") != 0) { - if (freopen(argv[1], "w", stdout) == NULL) { + /* "b": the output must not get CRLF line endings on Windows */ + if (freopen(argv[1], "wb", stdout) == NULL) { fprintf(stderr, "open output file '%s':", argv[1]); perror(NULL); diff --git a/cc/cpp/cpp.c b/cc/cpp/cpp.c index 9272fcba9..57d1f5330 100644 --- a/cc/cpp/cpp.c +++ b/cc/cpp/cpp.c @@ -356,7 +356,8 @@ main(int argc, char **argv) } if (argc == 2) { - if (freopen(argv[1], "w", stdout) == NULL) + /* "b": the output must not get CRLF line endings on Windows */ + if (freopen(argv[1], "wb", stdout) == NULL) error("Can't freopen %s", argv[1]); } if (argc && strcmp(argv[0], "-")) { From 0e14a95c43ca281cd0ba5b10735d560abb2bdba0 Mon Sep 17 00:00:00 2001 From: Michal Pleban Date: Wed, 8 Jul 2026 01:49:13 +0200 Subject: [PATCH 50/67] cc: make the cross-driver usable for z8001-coherent (Windows host) Driver changes, generic side: - find_file() tests access() on bare tool names, which never match Windows executables; retry with .exe appended (_WIN32). - Append the directory the driver executable lives in to progdirs/ crtdirs/libdirs (_WIN32, GetModuleFileName), after -B directories, so one folder can hold the tools, crts0.o and libc.a. - DEFLIBS entries without a leading dash are resolved through the library search directories via strap() instead of being passed through, for linkers that have no usable -l/-L. Coherent ld constraints (option table read from the linker source: -L means LARGE MEMORY MODEL, -l hardcodes /lib and /usr/lib, entry is the first object's start): - never emit -L or -g to ld (os_coherent guards), - do not pass -e STARTLABEL: crts0.s's "start" is not .globl; the linker enters at the first object, which is crts0.o, - ccconfig.h: CRT0 is the bare "crts0.o" resolved through crtdirs, DEFLIBS { "libc.a" } resolved through libdirs, DEFLIBDIRS empty, and PCC_EARLY_AS_ARGS passes -g to the assembler (undefined symbols become external references; native cc relies on this too). Diagnosis and the as/CRLF legs were verified piecewise (session 19); the supported regression route stays the cpp|ccom pipeline in ztests/compile.sh, which this does not affect. --- cc/cc/cc.c | 49 ++++++++++++++++++++++++++++++++++++++---- os/coherent/ccconfig.h | 26 ++++++++++++++++------ 2 files changed, 64 insertions(+), 11 deletions(-) diff --git a/cc/cc/cc.c b/cc/cc/cc.c index 9b2c0cd43..89600aed8 100644 --- a/cc/cc/cc.c +++ b/cc/cc/cc.c @@ -1029,6 +1029,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 +1275,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); @@ -2053,7 +2081,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 +2118,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 +2137,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 +2156,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/os/coherent/ccconfig.h b/os/coherent/ccconfig.h index 87169fe81..b5e202f53 100644 --- a/os/coherent/ccconfig.h +++ b/os/coherent/ccconfig.h @@ -33,21 +33,33 @@ #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. */ -#define CRT0 "/lib/crts0.o" + * 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 */ -#define DEFLIBS { "-lc", 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 } /* - * Library search paths: /lib first, then /usr/lib. - * Confirmed from linker source (_examples/cmd/ld/main.c). + * 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 { "/lib/", "/usr/lib/", 0 } +#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. From 1640638f97ff752c10e9081d3a0c59294999ddcc Mon Sep 17 00:00:00 2001 From: Michal Pleban Date: Wed, 8 Jul 2026 11:04:13 +0200 Subject: [PATCH 51/67] mip: add SWDISP pass2 op + precise computed-goto CFG edges Add a backend-usable pass2 operator SWDISP (a UTYPE; MAXOP 58->59), matched through finduni, for targets that lower a switch to a single computed-dispatch instruction whose scratch registers must be reserved by the register allocator (the Z8001 cpir idiom). cfg_build: a computed GOTO whose child is SWDISP now takes its successor edges from a per-dispatch, null-terminated label list carried on the GOTO node (n_name), instead of the function-global ip_labels. This keeps the CFG precise per switch: otherwise a function with several dispatches makes every case body a successor of every dispatch, so each case body becomes a false multi-predecessor merge and SSA removephi floods the shared dispatch point with phi copies (large spills). A genuine "goto *p" (no SWDISP) still conservatively assumes any address-taken label in ip_labels. Co-Authored-By: Claude Opus 4.8 (1M context) --- mip/common.c | 1 + mip/node.h | 3 ++- mip/optim2.c | 20 ++++++++++++++++++-- mip/reader.c | 1 + 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/mip/common.c b/mip/common.c index 6a911688b..f1de12812 100644 --- a/mip/common.c +++ b/mip/common.c @@ -568,6 +568,7 @@ struct dopest { { USTCALL, "USTCALL", UTYPE|CALLFLG, }, { STCLR, "STCLR", BITYPE, }, { ADDROF, "U&", UTYPE, }, + { SWDISP, "SWDISP", UTYPE, }, { -1, "", 0 }, }; diff --git a/mip/node.h b/mip/node.h index 8faed4593..5f1aedecb 100644 --- a/mip/node.h +++ b/mip/node.h @@ -222,7 +222,8 @@ typedef struct node { #define STCLR 56 #define FUNARG 57 #define ADDROF 58 +#define SWDISP 59 /* sparse-switch cpir dispatch (z8001) */ -#define MAXOP 58 +#define MAXOP 59 #endif diff --git a/mip/optim2.c b/mip/optim2.c index 207ea48bc..8f3459b8f 100644 --- a/mip/optim2.c +++ b/mip/optim2.c @@ -1088,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/reader.c b/mip/reader.c index a5b062a20..e036dbe88 100644 --- a/mip/reader.c +++ b/mip/reader.c @@ -1040,6 +1040,7 @@ again: switch (o = p->n_op) { case UCALL: case USTCALL: case ADDROF: + case SWDISP: rv = finduni(p, cookie); break; From f4b6c4d91bbb10a1c9d534adf34c3732b81ede50 Mon Sep 17 00:00:00 2001 From: Michal Pleban Date: Wed, 8 Jul 2026 11:04:13 +0200 Subject: [PATCH 52/67] z8001: sparse-switch dispatch via the cpir block-search idiom Native Coherent cc dispatches a scattered (typically ASCII-code) switch not with a compare ladder but with one cpir that linearly searches a compact .word case-value table, then indexes a parallel .long target table and jumps through it - eight instructions regardless of the case count. The idiom cannot be built from pass1 trees (there is no C-level "search", and cpir's fixed register roles need pass2 allocation), so mygenswitch() only MARKS an eligible switch (word-sized value, at least 5 cases) with /SW... comment interpasses carrying the case table. myreader() rewrites the marks into GOTO(SWDISP(value)) plus the value/ target tables - emitted as one opaque IP_ASM blob so no collectable DEFLAB is created for a label referenced only textually - and reserves the search pair + count register through the SWDISP table rule's NEEDS. The case and default labels are registered in ip_labels (kept alive so deljumps inuse never drops a case body reached only by the jp) and also carried on the GOTO node for precise cfg_build edges. Corpus -240 instructions at -O1; several switch programs now match or beat native (col 928 vs 941, lc 684 vs 699, ls 1077 vs 1100). Runtime- verified in the emulator: sort and tr are byte-identical to native, and od's format switch dispatches the octal and -c cases correctly. Known limitation: a very large switch inside a loop with variables live across it (deroff, the sole corpus case) makes SSA removephi fan out phi copies at the single dispatch point (+43 insns). The opaque jump table cannot be retargeted through copy-pads, so de-concentrating those copies would need a retargetable table plus removephi support; left as backlog. Switch size is not the trigger - sort's 90-case switch is fine. Co-Authored-By: Claude Opus 4.8 (1M context) --- arch/z8001/code.c | 62 +++++++++++++++- arch/z8001/local2.c | 172 ++++++++++++++++++++++++++++++++++++++++++++ arch/z8001/table.c | 16 +++++ 3 files changed, 248 insertions(+), 2 deletions(-) diff --git a/arch/z8001/code.c b/arch/z8001/code.c index 57648dae9..5e02af7ce 100644 --- a/arch/z8001/code.c +++ b/arch/z8001/code.c @@ -360,11 +360,69 @@ fldty(struct symtab *p) { } -/* Use PCC's default switch generation. */ +/* + * 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 at least SWCPIR_THRESH + * cases, below which the ladder is as short or shorter (and already matches + * or beats native, which uses cpir even for tiny switches). + */ +#define SWCPIR_THRESH 5 + int mygenswitch(int num, TWORD type, struct swents **p, int n) { - return 0; + char buf[64]; + int i, deflab, ltab; + + if (n < SWCPIR_THRESH) + return 0; + if (type != INT && type != UNSIGNED) + return 0; + + /* 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 * diff --git a/arch/z8001/local2.c b/arch/z8001/local2.c index b8075ac4c..c62412814 100644 --- a/arch/z8001/local2.c +++ b/arch/z8001/local2.c @@ -66,6 +66,17 @@ 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/.long table label */ +}; + /* * Register names indexed by PCC register number. * r0-r15 are 16-bit word registers; rr0,rr2,...,rr10 are 32-bit pairs; @@ -542,6 +553,38 @@ zzzcode(NODE *p, int c) printf("\tand\t%s,$0xff\n", rnames[n]); 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 + * index the parallel .long target table and load the + * target address into the result pair A2 - the outer + * GOTO(SWDISP) then jumps through it with "jp (A2)". + * A2 is both the search pointer and (reloaded) the + * target; A1 is the count; the pair's low word carries + * the byte offset of the matched entry. Displacement + * 2N-4: after cpir the low word is 2*(idx+1) past the + * table base, doubled to 4*(idx+1); the .long section + * starts 2N bytes in, so target idx sits at + * base + 2N + 4*idx = (base + 2) + (2N-4) + 4*(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 result 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("\tadd\t%s,%s\n", rnames[lo], rnames[lo]); + printf("\tldl\t%s," LABFMT "+%d(%s)\n", + rnames[pair], d->ltab, 2 * d->n - 4, 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 @@ -1876,12 +1919,141 @@ findstcall(NODE *p, void *arg) 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) */ + 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.long\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; + } +} + void myreader(struct interpass *ip) { struct interpass *ip2; int off = p2autooff; + swcpir(ip); DLIST_FOREACH(ip2, ip, qelem) { if (ip2->type != IP_NODE) continue; diff --git a/arch/z8001/table.c b/arch/z8001/table.c index 26345a4c0..db2445f45 100644 --- a/arch/z8001/table.c +++ b/arch/z8001/table.c @@ -1821,6 +1821,22 @@ struct optab table[] = { 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. From 6e4f3f4f7e93f2d749ff1fae200187bbf3799ff7 Mon Sep 17 00:00:00 2001 From: Michal Pleban Date: Wed, 8 Jul 2026 13:30:19 +0200 Subject: [PATCH 53/67] mip: add BCLR pass2 op (block memory clear) Add a backend-usable pass2 operator BCLR (a UTYPE; MAXOP 59->60) for targets that fold a run of per-element zero stores into one block-clear instruction (the Z8001 overlapping-ldirb idiom). Registered in the dope table (UTYPE) and matched through finduni. isuseless(): BCLR writes memory - it is not a dead expression statement, so it must never be dropped by deluseless. List it with STASG. Co-Authored-By: Claude Opus 4.8 (1M context) --- mip/common.c | 1 + mip/node.h | 3 ++- mip/reader.c | 2 ++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/mip/common.c b/mip/common.c index f1de12812..551102e59 100644 --- a/mip/common.c +++ b/mip/common.c @@ -569,6 +569,7 @@ struct dopest { { STCLR, "STCLR", BITYPE, }, { ADDROF, "U&", UTYPE, }, { SWDISP, "SWDISP", UTYPE, }, + { BCLR, "BCLR", UTYPE, }, { -1, "", 0 }, }; diff --git a/mip/node.h b/mip/node.h index 5f1aedecb..06d634baa 100644 --- a/mip/node.h +++ b/mip/node.h @@ -223,7 +223,8 @@ typedef struct node { #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 59 +#define MAXOP 60 #endif diff --git a/mip/reader.c b/mip/reader.c index e036dbe88..7978152a9 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; @@ -1041,6 +1042,7 @@ again: switch (o = p->n_op) { case USTCALL: case ADDROF: case SWDISP: + case BCLR: rv = finduni(p, cookie); break; From 757fd71907e55fef68aec4eca7b6b70890cb679b Mon Sep 17 00:00:00 2001 From: Michal Pleban Date: Wed, 8 Jul 2026 13:30:19 +0200 Subject: [PATCH 54/67] z8001: compact auto-aggregate zero-fill (BCLR overlapping ldirb) An ANSI partial initializer of an auto aggregate ("char buf[128] = "exec ";") zero-fills the uninitialized tail; pass1 endinit->clearbf emits one clrb per byte (login 123, tar 101 = 224 insns at 2 sites). Native K&R cc omits the fill, but ANSI requires it - so keep it, emit it compactly. myreader()'s clrfill() folds a run of consecutive single-byte zero stores to a frame slot into a compact clear: - >= 15 bytes: one BCLR node -> the Z8000 overlapping-ldirb idiom (clear byte 0, then propagate it forward with a single ldirb), ~7 insns regardless of length. The BCLR result pair doubles as the ldirb dst (DECRA packs the result + A1 count + A2 src pair); the byte count rides in n_rval (n_lval aliases n_left = the child). - 2..14 bytes: merge each word-aligned byte pair into one word clear (r13 is even, so an even tree offset is a word-aligned frame addr). Corpus -210 insns at -O1 (login 989->873, tar 2568->2474; both far closer to native). Runtime-verified in the emulator: the char and int tails are zeroed correctly (e71 "FILL OK") and tar produces a byte- identical archive to native /bin/tar. Co-Authored-By: Claude Opus 4.8 (1M context) --- arch/z8001/local2.c | 147 ++++++++++++++++++++++++++++++++++++++++++++ arch/z8001/table.c | 14 +++++ 2 files changed, 161 insertions(+) diff --git a/arch/z8001/local2.c b/arch/z8001/local2.c index c62412814..b95facc3e 100644 --- a/arch/z8001/local2.c +++ b/arch/z8001/local2.c @@ -553,6 +553,34 @@ zzzcode(NODE *p, int c) 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 @@ -2047,6 +2075,124 @@ swcpir(struct interpass *ipole) } } +/* + * 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; + } +} + void myreader(struct interpass *ip) { @@ -2054,6 +2200,7 @@ myreader(struct interpass *ip) int off = p2autooff; swcpir(ip); + clrfill(ip); DLIST_FOREACH(ip2, ip, qelem) { if (ip2->type != IP_NODE) continue; diff --git a/arch/z8001/table.c b/arch/z8001/table.c index db2445f45..7164d9a15 100644 --- a/arch/z8001/table.c +++ b/arch/z8001/table.c @@ -1301,6 +1301,20 @@ struct optab table[] = { 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, From 9131e455304d670274ca914ab56d8ae831d35e11 Mon Sep 17 00:00:00 2001 From: Michal Pleban Date: Wed, 8 Jul 2026 14:22:52 +0200 Subject: [PATCH 55/67] z8001: shrink switch cpir tables to .word offsets + size-gate it The cpir dispatch stored case targets as .long (32-bit segmented) addresses, so despite fewer instructions it was LARGER than the compare ladder in bytes (tables + dispatch overhead: +1780 corpus). Two changes make it a real size win: - The target table is now a .word 16-bit OFFSET table, not .long addresses. Every case body shares the dispatch's code segment, so the matched target offset is loaded into the search pair's low word (its high/segment word - which cpir leaves untouched - still points at this segment) and jp jumps through the pair. This halves the target table (4N->2N bytes) and drops the index-scaling add. - mygenswitch now SIZE-gates: cpir (~28 + 4N bytes) is used only when it beats the ladder (~6N bytes), i.e. for the larger switches (N >= ~14). Smaller switches keep the compare ladder, which is itself smaller than native's cpir. Real code+data size vs native improves 0.9844x -> 0.9776x (-1186 bytes; sort +118->+54, pr +88->+26, col/lc/ls and the rest back below native). Runtime-verified: e72's 16-case switch dispatches every case and the default correctly through the .word offset table (emulator "SW OK"); sweep 100 OK, status byte-identical. Co-Authored-By: Claude Opus 4.8 (1M context) --- arch/z8001/code.c | 15 ++++++++------- arch/z8001/local2.c | 37 ++++++++++++++++++++----------------- 2 files changed, 28 insertions(+), 24 deletions(-) diff --git a/arch/z8001/code.c b/arch/z8001/code.c index 5e02af7ce..899107e45 100644 --- a/arch/z8001/code.c +++ b/arch/z8001/code.c @@ -387,22 +387,23 @@ fldty(struct symtab *p) * 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 at least SWCPIR_THRESH - * cases, below which the ladder is as short or shorter (and already matches - * or beats native, which uses cpir even for tiny switches). + * 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). */ -#define SWCPIR_THRESH 5 - int mygenswitch(int num, TWORD type, struct swents **p, int n) { char buf[64]; int i, deflab, ltab; - if (n < SWCPIR_THRESH) - return 0; 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) */ diff --git a/arch/z8001/local2.c b/arch/z8001/local2.c index b95facc3e..3a2fe7822 100644 --- a/arch/z8001/local2.c +++ b/arch/z8001/local2.c @@ -74,7 +74,7 @@ void mygenregs(struct interpass *ip); struct swdesc { int n; /* number of cases */ int deflab; /* default (no-match) label */ - int ltab; /* .word/.long table label */ + int ltab; /* .word value/offset table label */ }; /* @@ -584,20 +584,22 @@ zzzcode(NODE *p, int c) 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 - * index the parallel .long target table and load the - * target address into the result pair A2 - the outer - * GOTO(SWDISP) then jumps through it with "jp (A2)". - * A2 is both the search pointer and (reloaded) the - * target; A1 is the count; the pair's low word carries - * the byte offset of the matched entry. Displacement - * 2N-4: after cpir the low word is 2*(idx+1) past the - * table base, doubled to 4*(idx+1); the .long section - * starts 2N bytes in, so target idx sits at - * base + 2N + 4*idx = (base + 2) + (2N-4) + 4*(idx+1). */ + * 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 result pair */ + 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); @@ -607,9 +609,8 @@ zzzcode(NODE *p, int c) 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("\tadd\t%s,%s\n", rnames[lo], rnames[lo]); - printf("\tldl\t%s," LABFMT "+%d(%s)\n", - rnames[pair], d->ltab, 2 * d->n - 4, rnames[lo]); + printf("\tld\t%s," LABFMT "+%d(%s)\n", + rnames[lo], d->ltab, 2 * d->n - 2, rnames[lo]); } break; @@ -2040,14 +2041,16 @@ swcpir(struct interpass *ipole) dip->ip_node = go; DLIST_INSERT_AFTER(prev, dip, qelem); - /* value/target tables as one opaque blob (label defined in it) */ + /* 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.long\t" LABFMT "\n", labs[k]); + q += sprintf(q, "\t.word\t" LABFMT "\n", labs[k]); bip = tmpalloc(sizeof(struct interpass)); bip->type = IP_ASM; From dcb2dd2b8284a279b77625a3481d4b48f20ea65d Mon Sep 17 00:00:00 2001 From: Michal Pleban Date: Wed, 8 Jul 2026 21:16:45 +0200 Subject: [PATCH 56/67] Add -ftraditional flag to gate K&R leniency (off by default) Four pre-ANSI constructs that PCC otherwise rejects as errors were being accepted unconditionally on this branch to build Coherent's K&R sources. Gate them behind a new -ftraditional language flag (default off, so strict conformance is the default) plumbed like -ffreestanding: - external declarations with no type specifier (default int) - missing return value in a non-void function - scalar initializer for an array (first element) - mismatched pointer types in ?: Each becomes a warning only with -ftraditional; without it they are hard errors as before. The chkpun() function-pointer null-deref fix stays unconditional -- it is a crash fix, not a language relaxation. Co-Authored-By: Claude Opus 4.8 (1M context) --- cc/cc/cc.1 | 3 +++ cc/cc/cc.c | 4 ++++ cc/ccom/ccom.1 | 11 +++++++++++ cc/ccom/cgram.y | 18 ++++++++++++++---- cc/ccom/init.c | 2 +- cc/ccom/main.c | 3 +++ cc/ccom/trees.c | 2 +- mip/manifest.h | 1 + 8 files changed, 38 insertions(+), 6 deletions(-) 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 89600aed8..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")) { @@ -1983,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" }, 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 885c18e80..f896525d2 100644 --- a/cc/ccom/cgram.y +++ b/cc/ccom/cgram.y @@ -320,7 +320,10 @@ notype_init_dcl_list: notype_init_dcl: declarator attr_var { P1ND *tn = mkty(INT, 0, 0); - werror("type defaults to int in declaration"); + if (traditional) + werror("type defaults to int in declaration"); + else + uerror("type specifier missing"); init_declarator(tn, $1, 0, $2, 0); p1tfree(tn); } @@ -340,7 +343,10 @@ notype_init_dcl: declarator attr_var { notype_xnfdcl: declarator attr_var { P1ND *tn = mkty(INT, 0, 0); - werror("type defaults to int in declaration"); + if (traditional) + werror("type defaults to int in declaration"); + else + uerror("type specifier missing"); $$ = xnf = init_declarator(tn, $1, 1, $2, 0); p1tfree(tn); } @@ -984,10 +990,14 @@ statement: e ';' { ecomp(eve($1)); symclear(blevel); } branch(retlab); if (cftnsp->stype != VOID && (cftnsp->sflags & NORETYP) == 0 && - cftnsp->stype != VOID+FTN) + cftnsp->stype != VOID+FTN) { /* legal in C89 (constraint is C99); * common in K&R code */ - werror("return value required"); + if (traditional) + werror("return value required"); + else + uerror("return value required"); + } rch: if (!reached) warner(Wunreachable_code); diff --git a/cc/ccom/init.c b/cc/ccom/init.c index 046f4b5b8..e4436bafe 100644 --- a/cc/ccom/init.c +++ b/cc/ccom/init.c @@ -1229,7 +1229,7 @@ simpleinit(struct symtab *sp, NODE *p) * its first element, as if braced ("char tapedev[10] = '\0';"). * Falling through would build ASSIGN(array, scalar) and give * "lvalue required". */ - if (ISARY(sp->stype) && !ISARY(p->n_type)) { + if (traditional && ISARY(sp->stype) && !ISARY(p->n_type)) { werror("array initialized with scalar; braces assumed"); ctx = beginit(sp); scalinit(ctx, p, NULL); diff --git a/cc/ccom/main.c b/cc/ccom/main.c index 5d25f7f16..79b14455d 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/trees.c b/cc/ccom/trees.c index ae35ff36f..762a3ba42 100644 --- a/cc/ccom/trees.c +++ b/cc/ccom/trees.c @@ -1539,7 +1539,7 @@ ptmatch(P1ND *p) if (BTYPE(td2->type) == VOID) break; } - if (ISPTR(td1->type) && ISPTR(td2->type)) { + 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 */ 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; From 0c3be0b1a36fc7cdc7a2eac59dd597c80437e726 Mon Sep 17 00:00:00 2001 From: Michal Pleban Date: Wed, 8 Jul 2026 23:13:31 +0200 Subject: [PATCH 57/67] Revert wb binary output mode in ccom/cpp CRLF handling now lives in the assembler: the Windows z8001-coherent-as was patched to accept CR, so the compiler no longer needs to force binary output. Restore the original w freopen on the named-output-file path. Co-Authored-By: Claude Opus 4.8 (1M context) --- cc/ccom/main.c | 3 +-- cc/cpp/cpp.c | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/cc/ccom/main.c b/cc/ccom/main.c index 79b14455d..be9f3c86e 100644 --- a/cc/ccom/main.c +++ b/cc/ccom/main.c @@ -277,8 +277,7 @@ main(int argc, char *argv[]) } } if (argc > 1 && strcmp(argv[1], "-") != 0) { - /* "b": the output must not get CRLF line endings on Windows */ - if (freopen(argv[1], "wb", stdout) == NULL) { + if (freopen(argv[1], "w", stdout) == NULL) { fprintf(stderr, "open output file '%s':", argv[1]); perror(NULL); diff --git a/cc/cpp/cpp.c b/cc/cpp/cpp.c index 57d1f5330..9272fcba9 100644 --- a/cc/cpp/cpp.c +++ b/cc/cpp/cpp.c @@ -356,8 +356,7 @@ main(int argc, char **argv) } if (argc == 2) { - /* "b": the output must not get CRLF line endings on Windows */ - if (freopen(argv[1], "wb", stdout) == NULL) + if (freopen(argv[1], "w", stdout) == NULL) error("Can't freopen %s", argv[1]); } if (argc && strcmp(argv[0], "-")) { From dcd1d25d2a581ff1360b246446cf36d1cca99f31 Mon Sep 17 00:00:00 2001 From: Michal Pleban Date: Thu, 9 Jul 2026 13:02:18 +0200 Subject: [PATCH 58/67] z8001: integer-promote byte shift counts in clocal A shift with a char/uchar count (e.g. Coherent's restor.c "lb >> shifts[il]" where shifts[] is char[]) failed with "Cannot generate code ... op >>". buildtree() promotes the value being shifted but only *demotes* an oversized count (long -> int); a sub-int count is left unchanged. The Z8001 dynamic-shift instructions (sdl/sda/sdll/sdal) take the count in a word register, so every register-count table rule requires a TWORD right operand and a byte count matches nothing. Widen a CHAR/UCHAR shift count to int in clocal (SHORT/USHORT are already word-sized here). Built with block()/direct retype rather than makety() so this shared file compiles under both ccom and cxxcom, whose makety() prototypes differ. Co-Authored-By: Claude Opus 4.8 (1M context) --- arch/z8001/local.c | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/arch/z8001/local.c b/arch/z8001/local.c index 85ba9f87c..b3583e249 100644 --- a/arch/z8001/local.c +++ b/arch/z8001/local.c @@ -206,6 +206,32 @@ clocal(NODE *p) } 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. From 4ab56b62b4aecb7ff739714a8333c1ba884e0c22 Mon Sep 17 00:00:00 2001 From: Michal Pleban Date: Fri, 10 Jul 2026 16:57:21 +0200 Subject: [PATCH 59/67] z8001: emit frame-base equate as SS|total, not 0|total The per-function frame-base equate that lets pass2 address stack slots as "L+off(r13)" was hardcoded to segment 0 (L=0|total). That is only correct when the stack segment SS is 0 -- true for user programs (crts0.s pins SS=0x0000, a single flat segment) but NOT for segmented code: the kernel's md.s sets SS=0x3F3F (system stack in segment 0x3F). With 0|total, every callee in the kernel read its arguments and autos out of segment 0 (ROM) instead of the stack, so e.g. setarena() got a garbage arena pointer and the boot wedged before printing its banner. Emit the equate with the external SS symbol (L=SS|total) so frame/arg addressing is correct in both ABIs. The symbolic segment is E_SEG; this relies on the assembler's E_SEG long-form emission being correct, which it now is (the outsof() bit-15 drop that motivated the numeric workaround has been fixed in the Coherent as). Verified: with SS|total the PCC-built kernel boots past setarena/alloc to its banner (and, with the companion build fixes, to an interactive shell); the userland corpus is unaffected since crts0 keeps SS=0. Co-Authored-By: Claude Opus 4.8 (1M context) --- arch/z8001/local2.c | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/arch/z8001/local2.c b/arch/z8001/local2.c index 3a2fe7822..cec624c8c 100644 --- a/arch/z8001/local2.c +++ b/arch/z8001/local2.c @@ -201,28 +201,24 @@ prologue(struct interpass_prolog *ipp) /* * Frame-base equate for stack-segment addressing (see framelab above). - * L = 0|total; slots are then addressed "L+off(r13)" with r13 at - * the frame bottom, so EA = seg0:(total + off + r13_bottom) recovers the - * absolute frame offset and supplies the stack segment. total >= 2 + * L = SS|total; slots are then 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. total >= 2 * always (R13 is always saved). * - * The native compiler wrote "L=SS|total" with SS an external symbol - * (crts0.s pins it: "SS = 0x0000"), but that form is POISON with the - * shipped Coherent "as": a symbolic segment expression is E_SEG, and - * outsof()'s E_SEG long-form path (needed whenever total+off > 255, - * i.e. any frame bigger than ~250 bytes) drops the bit-15 long-form - * flag from the first address word (machine.c:1011 sets it on the - * caller's expr, then emits the zeroed copy). The CPU then decodes - * the short form, eats one word too few, and executes the offset word - * as an instruction - echo.c crashed exactly this way, and even the - * native echo.s reassembled with the on-disk as loses its argv (MWC's - * factory binaries were built with an assembler that didn't have the - * bug). A NUMERIC segment ("0|total") is E_ASEG instead, whose short - * AND long emissions are both correct, and the segment truly is 0 on - * this ABI. + * 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"). */ framelab = getlab2(); - printf(LABFMT "=0|%d\n", framelab, total); + printf(LABFMT "=SS|%d\n", framelab, total); /* Step 1: allocate entire frame */ if (total > 0) { From e1e58b7ace353dba54580597214681b8f7a2c15c Mon Sep 17 00:00:00 2001 From: Michal Pleban Date: Fri, 10 Jul 2026 19:50:45 +0200 Subject: [PATCH 60/67] z8001: djnz loop fusion + SSA dead-source RMW compare elision Two -O1 code-generation improvements for the Z8001 backend plus the one machine-independent hook they need. djnz/dbjnz loop fusion (cbfuse): - New TARGET_CBRANCH_FUSE hook in emit() (mip/reader.c, default no-op; prototype in mip/pass2.h) lets a target collapse an RESCC compare-and- branch into a single instruction. - arch/z8001 defines it as cbfuse(): a fused countdown branch CBRANCH(NE(ASSIGN(reg d, dec-by-1), 0)) with a word counter and a BACKWARD target becomes one "djnz r,L" instead of "dec r,$1; jr ne,L". Backward-only (a forward one would grow to dec;jp), tracked via a per-function emitted-label list (deflab/prologue). The decrement is accepted as MINUS(r,1) or PLUS(r,-1). - 7 corpus sites convert (dd,lc,ls,sum,tar); the -O1 sweep stays 100 OK status-identical and sum's output is byte-identical to native. An out-of-range djnz relies on the Coherent assembler relaxing it to dec;jp nz; that assembler change lives outside this tree. Dead-source RMW compare elision (rmwrenamesd): - Recovers the read-modify-write shape for a straight-line "if ((x op= y))" whose source temp dies at the op. SSA splits the stored and read halves apart (d != s) with no removephi copies, so findmops' treecmp fails and a redundant test is emitted. When s is single-def / single-use - so by SSA its definition dominates the RMW - it renames s to d, restoring the in-place op and eliding the compare. EQ/NE only (the Z flag is the only one every op sets correctly). - A live-source variant that inserts an explicit copy was attempted and reverted (it destabilised the SSA register allocator); see the NOTE in local2.c. Verified end-to-end with ztests/e73.c in the emulator. Co-Authored-By: Claude Opus 4.8 (1M context) --- arch/z8001/local2.c | 236 +++++++++++++++++++++++++++++++++++++++++++ arch/z8001/macdefs.h | 6 ++ mip/pass2.h | 1 + mip/reader.c | 17 +++- ztests/e73.c | 160 +++++++++++++++++++++++++++++ 5 files changed, 418 insertions(+), 2 deletions(-) create mode 100644 ztests/e73.c diff --git a/arch/z8001/local2.c b/arch/z8001/local2.c index cec624c8c..0a7a53697 100644 --- a/arch/z8001/local2.c +++ b/arch/z8001/local2.c @@ -124,9 +124,40 @@ 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); } @@ -192,6 +223,8 @@ prologue(struct interpass_prolog *ipp) { int firstsave, nsave, fsize, total; + seenlabs = NULL; /* start a fresh emitted-label set (see cbfuse) */ + firstsave = firstsavereg(); /* Save firstsave..R13 inclusive (always save R13 since we clobber it) */ @@ -1151,6 +1184,76 @@ adrput(FILE *io, NODE *p) } } +/* + * 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. */ @@ -1719,11 +1822,144 @@ rmwrename(struct interpass *ipole) } } +/* + * 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; + } +} + +/* + * 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); } /* diff --git a/arch/z8001/macdefs.h b/arch/z8001/macdefs.h index 7e084445f..accd519b0 100644 --- a/arch/z8001/macdefs.h +++ b/arch/z8001/macdefs.h @@ -431,6 +431,12 @@ int pickcolor(int class, int mask); #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 diff --git a/mip/pass2.h b/mip/pass2.h index 72abae584..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); diff --git a/mip/reader.c b/mip/reader.c index 7978152a9..b657858c8 100644 --- a/mip/reader.c +++ b/mip/reader.c @@ -777,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) { @@ -809,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); } 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; +} From 34ad3660139c08acabbcb3b6482774b1aa448f6d Mon Sep 17 00:00:00 2001 From: Michal Pleban Date: Sun, 12 Jul 2026 00:46:32 +0200 Subject: [PATCH 61/67] z8001: mask segment word after lda in ZX (&arr[i]) escape The ZX escape (address constant + word index, i.e. &global[index]) emitted its lda / ldl+add without the `and rN,$0x7F00` (32512) segment-normalising mask that the ZF (&local) and ZM (lda-of-name) paths already apply. That left the reserved bit of the segment byte set, so a variable-indexed &arr[i] pointer compared unequal to the same address materialised as a constant. Symptom: the Coherent kernel's sleep/wakeup hash queue (linkq[]) was initialised with constant-form self-pointers but probed via &linkq[hash(e)] (lda), so wakeup() spun forever on an untouched bucket, hanging boot on real hardware right after loading /etc/init. Normalise the high (segment) word after forming the pointer, matching the other lda sites. ZX was the only lda emitter in the backend missing it. Co-Authored-By: Claude Opus 4.8 (1M context) --- arch/z8001/local2.c | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/arch/z8001/local2.c b/arch/z8001/local2.c index 0a7a53697..a78015c06 100644 --- a/arch/z8001/local2.c +++ b/arch/z8001/local2.c @@ -719,6 +719,17 @@ zzzcode(NODE *p, int c) 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; From 3c2ff7304a21a80d10c5994a39ab83b0d051b393 Mon Sep 17 00:00:00 2001 From: Michal Pleban Date: Mon, 13 Jul 2026 17:25:23 +0200 Subject: [PATCH 62/67] z8001: smaller long-zero, small-const, and in-place loop-RMW codegen Three size wins for long/pointer codegen, verified on the -O1 sweep (no regressions) and at runtime in the c900 emulator (ZRT PASS at -O0 and -O1): - register long-zero: ASSIGN SBREG<-SZERO emits "subl AL,AL" (2 bytes) instead of the two-clr ZQ (4 bytes); memory ZQ variants keep clr. - small long/ptr constant: new SLCON shape + Z0 escape splits a numeric long constant into per-word clr/ldk/ld when that is no larger than the 6-byte "ldl #imm32", else emits the plain ldl. New ASSIGN and OPLTYPE rules feed it; symbolic constants fall through to the generic ldl. - in-place RMW for a body-live loop induction variable: rmwrenameloop() is the mirror of rmwrename() -- here the destination temp is single-use, so renaming it onto the source and deleting the adjacent back-edge copy makes "t += 1" emit "addl rr10,$1" in place instead of "ldl rr2,rr10; addl rr2,$1; ldl rr10,rr2". Rename+delete only (no mid-stream insert), xssa-gated, guarded by single def/use of the destination temp and literal adjacency of the "s = d" copy. Co-Authored-By: Claude Opus 4.8 (1M context) --- arch/z8001/local2.c | 180 +++++++++++++++++++++++++++++++++++++++++++ arch/z8001/macdefs.h | 6 ++ arch/z8001/table.c | 26 ++++++- 3 files changed, 210 insertions(+), 2 deletions(-) diff --git a/arch/z8001/local2.c b/arch/z8001/local2.c index a78015c06..2f68b048f 100644 --- a/arch/z8001/local2.c +++ b/arch/z8001/local2.c @@ -412,6 +412,39 @@ prword(NODE *p, int lo) } } +/* + * 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 @@ -992,6 +1025,45 @@ zzzcode(NODE *p, int c) } 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); } @@ -1525,6 +1597,12 @@ special(NODE *p, int shape) 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 @@ -1951,6 +2029,107 @@ rmwrenamesd(struct interpass *ipole) } } +/* + * 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, @@ -1971,6 +2150,7 @@ myoptim(struct interpass *ip) fusecmp(ip); rmwrename(ip); rmwrenamesd(ip); + rmwrenameloop(ip); } /* diff --git a/arch/z8001/macdefs.h b/arch/z8001/macdefs.h index accd519b0..e54cbc959 100644 --- a/arch/z8001/macdefs.h +++ b/arch/z8001/macdefs.h @@ -416,6 +416,12 @@ int pickcolor(int class, int mask); 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 diff --git a/arch/z8001/table.c b/arch/z8001/table.c index 7164d9a15..5ead579fd 100644 --- a/arch/z8001/table.c +++ b/arch/z8001/table.c @@ -1177,12 +1177,15 @@ struct optab table[] = { 0, 0, " clr AL\n", }, -/* pair reg <- zero (long); RDEST valid: the cleared pair IS the value */ +/* 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, - "ZQ", }, + " 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 @@ -1238,6 +1241,16 @@ struct optab table[] = { 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 @@ -1872,6 +1885,15 @@ struct optab table[] = { 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, From c006d7f3fb37b819c077207b0aba3bb94d0d3513 Mon Sep 17 00:00:00 2001 From: Michal Pleban Date: Mon, 13 Jul 2026 17:51:26 +0200 Subject: [PATCH 63/67] z8001: word +/- by 2..16 uses inc/dec instead of add/sub Add SP16 (2..16) arms to the word PLUS and MINUS rules, emitting the 2-byte "inc/dec AL,$k" in place of the 4-byte "add/sub AL,$k". The $1 case still matches the SONE/SMONE rules above (byte-identical output), so the new arms only cover 2..16; symbolic and out-of-range constants fall through to the generic add/sub, which these must precede. inc/dec set S/Z/V but not carry; this is sound because the RESCC compare-elision after PLUS/MINUS relies only on S/Z/V (the CCOKFORCOMP signed-compare-vs-zero path), and the pre-existing inc/dec $1 rules already carry RESCC through that same gate. Sweep -O1 status-identical (no regressions), -196 bytes across 103 programs (29 shrank, 0 grew); runtime self-check ZINC PASS in the emulator including the flag-sensitive compare/unsigned-wrap cases. Co-Authored-By: Claude Opus 4.8 (1M context) --- arch/z8001/table.c | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/arch/z8001/table.c b/arch/z8001/table.c index 5ead579fd..1429dfb0b 100644 --- a/arch/z8001/table.c +++ b/arch/z8001/table.c @@ -505,6 +505,22 @@ struct optab table[] = { 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, @@ -663,6 +679,21 @@ struct optab table[] = { 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, From f8c90b1b31f6fa43820853886253136900311581 Mon Sep 17 00:00:00 2001 From: Michal Pleban Date: Mon, 13 Jul 2026 18:32:45 +0200 Subject: [PATCH 64/67] z8001: elide frame setup for r13-unused fns; push/pop for total==2 Frameless fast-path in prologue()/eoftn(): when a function saves no callee-saved GPR (firstsave==R13), has no frame slots (fsize==0, which also rules out spills), and never references r13 as a frame base (usesfp() walks the interpass body for OREG/REG(FPREG)), emit no equate, no allocation, no r13 save, and no "ld r13,r15" -- epilogue is a bare "ret un". Kills the whole frame on leaf/no-arg helpers (usage, etc). For the remaining total==2 && nsave==1 case (only r13 saved, no autos, but r13 used for stack-arg access), combine allocate+store into a single "push (rr14),r13" (2B vs 4B) and restore+reclaim into "pop r13,(rr14)". Co-Authored-By: Claude Opus 4.8 (1M context) --- arch/z8001/local2.c | 87 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 86 insertions(+), 1 deletion(-) diff --git a/arch/z8001/local2.c b/arch/z8001/local2.c index 2f68b048f..44a96798f 100644 --- a/arch/z8001/local2.c +++ b/arch/z8001/local2.c @@ -171,6 +171,50 @@ deflab(int label) */ 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; + +/* + * 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. * @@ -226,10 +270,23 @@ prologue(struct interpass_prolog *ipp) seenlabs = NULL; /* start a fresh emitted-label set (see cbfuse) */ firstsave = firstsavereg(); + fsize = (p2maxautooff + 1) & ~1; /* p2maxautooff is bytes; round to word */ + + /* + * Frameless fast-path: a function that neither preserves a callee-saved + * word register (firstsave == R13 means none of R6-R12 was used) nor has + * any frame slot (fsize == 0 rules out autos AND spills) nor references + * r13 as a frame base (usesfp() -- no stack-arg reads, no &local) needs + * no frame at all. Skip the equate, the allocation, the r13 save and the + * "ld r13,r15"; eoftn() then emits only "ret un". Typical of leaf/no-arg + * helpers (e.g. a usage() that just fprintf()s and exit()s). + */ + curframeless = (firstsave == R13 && fsize == 0 && !usesfp(ipp)); + if (curframeless) + return; /* Save firstsave..R13 inclusive (always save R13 since we clobber it) */ nsave = R13 - firstsave + 1; - fsize = (p2maxautooff + 1) & ~1; /* p2maxautooff is bytes; round to word */ total = fsize + nsave * 2; /* @@ -253,6 +310,17 @@ prologue(struct interpass_prolog *ipp) framelab = getlab2(); printf(LABFMT "=SS|%d\n", framelab, total); + /* + * Special case total == 2 (only R13 saved, no autos): a single "push" + * does the SP decrement and the store in one 2-byte instruction, versus + * "dec r15,$2" + "ld (rr14),r13" (4 bytes). Then set the frame pointer. + */ + if (total == 2 && nsave == 1) { + printf("\tpush\t(rr14),r13\n"); + printf("\tld\tr13,r15\n"); + return; + } + /* Step 1: allocate entire frame */ if (total > 0) { if (total <= 16) @@ -287,12 +355,29 @@ eoftn(struct interpass_prolog *ipp) 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 same values as the prologue */ firstsave = firstsavereg(); nsave = R13 - firstsave + 1; fsize = (p2maxautooff + 1) & ~1; total = fsize + nsave * 2; + /* + * Mirror of the prologue "push" special case: "pop r13,(rr14)" restores + * r13 and reclaims the 2-byte frame in one instruction, versus + * "ld r13,(rr14)" + "inc r15,$2". + */ + if (total == 2 && nsave == 1) { + printf("\tpop\tr13,(rr14)\n"); + printf("\tret\tun\n"); + return; + } + /* Restore callee-saved registers (neither ld nor ldm changes r15) */ if (nsave == 1) printf("\tld\tr13,(rr14)\n"); From a7444c5d96d87bf79a3b53228ee6cd969e293993 Mon Sep 17 00:00:00 2001 From: Michal Pleban Date: Mon, 13 Jul 2026 19:28:00 +0200 Subject: [PATCH 65/67] z8001: skip r13 frame setup when unused; pushl/popl pair save combine Generalises the frameless work: r13 is now saved and set up as a frame pointer (ld r13,r15 + SS| equate) ONLY when the function needs one -- has autos/spills (fsize>0) or addresses a frame slot / stack arg through r13 (usesfp()). When it doesn't, the callee-saved run stops at the top used r6..r12 (new lastsavereg()) and r13 is never touched; the caller's r13 is preserved by not clobbering it. So a function that uses e.g. rr10 but not r13 no longer saves/restores r13 or emits the equate. Adds a pushl/popl combine for an aligned callee-saved pair (rr6/rr8/rr10, no autos): "pushl (rr14),rrN" / "popl rrN,(rr14)" (2B) replaces dec+ldm / ldm+inc (6B), mirroring the existing single-reg push/pop. Verified: sweepO1 status-identical to RESULTS_sess22.tsv; corpus 263 fns now skip r13 setup, 38 new pair combines; code+data -612B vs prior. pwd main_ (uses rr10, r13 dead) -> pushl/body/popl/ret, byte-identical to /bin/pwd in the emulator. popl RR,IR is legal (assembles + emulator StackPopL) but corpus-novel -> whitelisted in the sweep scripts. Co-Authored-By: Claude Opus 4.8 (1M context) --- arch/z8001/local2.c | 185 ++++++++++++++++++++++++++++---------------- 1 file changed, 120 insertions(+), 65 deletions(-) diff --git a/arch/z8001/local2.c b/arch/z8001/local2.c index 44a96798f..ac076d7f3 100644 --- a/arch/z8001/local2.c +++ b/arch/z8001/local2.c @@ -179,6 +179,16 @@ static int framelab; */ 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 @@ -218,10 +228,12 @@ usesfp(struct interpass_prolog *ipp) /* * Emit the function prologue. * - * Callee-saved word registers are R6-R12. We always save R13 (the frame - * pointer) because we change its value. We find the lowest-numbered - * callee-saved register that was actually used and save a contiguous run - * from that register through R13 with a single ldm or ld instruction. + * 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 @@ -262,6 +274,38 @@ firstsavereg(void) 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) { @@ -273,28 +317,39 @@ prologue(struct interpass_prolog *ipp) fsize = (p2maxautooff + 1) & ~1; /* p2maxautooff is bytes; round to word */ /* - * Frameless fast-path: a function that neither preserves a callee-saved - * word register (firstsave == R13 means none of R6-R12 was used) nor has - * any frame slot (fsize == 0 rules out autos AND spills) nor references - * r13 as a frame base (usesfp() -- no stack-arg reads, no &local) needs - * no frame at all. Skip the equate, the allocation, the r13 save and the - * "ld r13,r15"; eoftn() then emits only "ret un". Typical of leaf/no-arg - * helpers (e.g. a usage() that just fprintf()s and exit()s). + * 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. */ - curframeless = (firstsave == R13 && fsize == 0 && !usesfp(ipp)); - if (curframeless) - return; + 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; - /* Save firstsave..R13 inclusive (always save R13 since we clobber it) */ - nsave = R13 - firstsave + 1; + nsave = cursavetop - firstsave + 1; total = fsize + nsave * 2; /* - * Frame-base equate for stack-segment addressing (see framelab above). - * L = SS|total; slots are then 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. total >= 2 - * always (R13 is always saved). + * 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 @@ -307,41 +362,36 @@ prologue(struct interpass_prolog *ipp) * the earlier outsof() bit-15 drop that motivated the "0|" workaround has * been fixed in the Coherent "as"). */ - framelab = getlab2(); - printf(LABFMT "=SS|%d\n", framelab, total); + if (curneedfp) + printf(LABFMT "=SS|%d\n", framelab, total); /* - * Special case total == 2 (only R13 saved, no autos): a single "push" - * does the SP decrement and the store in one 2-byte instruction, versus - * "dec r15,$2" + "ld (rr14),r13" (4 bytes). Then set the frame pointer. + * 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 (total == 2 && nsave == 1) { - printf("\tpush\t(rr14),r13\n"); - printf("\tld\tr13,r15\n"); - return; - } - - /* Step 1: allocate entire frame */ - if (total > 0) { - if (total <= 16) - printf("\tdec\tr15,$%d\n", total); + 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("\tsub\tr15,$%d\n", total); + printf("\tldm\t(rr14),r%d,$%d\n", firstsave, nsave); } - /* Step 2: save callee-saved registers at current SP (no auto-decrement) */ - if (nsave == 1) - printf("\tld\t(rr14),r13\n"); - else - printf("\tldm\t(rr14),r%d,$%d\n", firstsave, nsave); - - /* - * Step 3: r13 = current SP = frame bottom. (Native keeps the frame - * pointer at the bottom and reaches slots via the SS|total equate; - * node offsets remain entry-SP-relative and are corrected by the +total - * baked into the equate.) - */ - printf("\tld\tr13,r15\n"); + /* r13 = current SP = frame bottom (only when it is the frame pointer). */ + if (curneedfp) + printf("\tld\tr13,r15\n"); } /* @@ -361,36 +411,41 @@ eoftn(struct interpass_prolog *ipp) return; } - /* Recompute the same values as the prologue */ + /* + * 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(); - nsave = R13 - firstsave + 1; fsize = (p2maxautooff + 1) & ~1; + nsave = cursavetop - firstsave + 1; total = fsize + nsave * 2; /* - * Mirror of the prologue "push" special case: "pop r13,(rr14)" restores - * r13 and reclaims the 2-byte frame in one instruction, versus - * "ld r13,(rr14)" + "inc r15,$2". + * Mirror of the prologue allocate-and-save combine: pop / popl restore the + * register(s) AND reclaim the frame in one instruction. */ - if (total == 2 && nsave == 1) { - printf("\tpop\tr13,(rr14)\n"); + 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 callee-saved registers (neither ld nor ldm changes r15) */ + /* Restore the saved run (neither ld nor ldm changes r15) */ if (nsave == 1) - printf("\tld\tr13,(rr14)\n"); + printf("\tld\tr%d,(rr14)\n", firstsave); else printf("\tldm\tr%d,(rr14),$%d\n", firstsave, nsave); /* Reclaim the whole frame */ - if (total > 0) { - if (total <= 16) - printf("\tinc\tr15,$%d\n", total); - else - printf("\tadd\tr15,$%d\n", total); - } + if (total > 0) + printf("\t%s\tr15,$%d\n", total <= 16 ? "inc" : "add", total); printf("\tret\tun\n"); } From e9c15ed6299873289e9ead359f10a3b62cf3b1ac Mon Sep 17 00:00:00 2001 From: Michal Pleban Date: Mon, 13 Jul 2026 22:06:53 +0200 Subject: [PATCH 66/67] z8001: CSE fixed address/const args shared by consecutive calls argcse() in myreader hoists an address or wide constant passed to a run of adjacent calls into one callee-saved register instead of rematerialising it per call. Handles &global/&auto (ADDROF), symbolic address constants (ldl $sym), and long/pointer numeric constants (ldl $imm32); generalised to multi-argument calls with longest-run selection and overlapping runs. Runs before canon/regalloc (only safe point to insert IP nodes); restricted to call-invariant, expensive-to-recompute operands so the hoist is always safe. -324B text across the -O1 corpus (36 programs: 30 smaller, 6 larger from 2-use ICON cases that force a register save), 100 OK status-identical, audit clean, no novel instructions. pr/sort/scat (addresses) and m4 (numeric const) verified byte-identical to native /bin in the c900 emulator. Co-Authored-By: Claude Opus 4.8 (1M context) --- arch/z8001/local2.c | 292 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 292 insertions(+) diff --git a/arch/z8001/local2.c b/arch/z8001/local2.c index ac076d7f3..8df92e379 100644 --- a/arch/z8001/local2.c +++ b/arch/z8001/local2.c @@ -2759,6 +2759,297 @@ clrfill(struct interpass *ipole) } } +/* + * 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) { @@ -2767,6 +3058,7 @@ myreader(struct interpass *ip) swcpir(ip); clrfill(ip); + argcse(ip); DLIST_FOREACH(ip2, ip, qelem) { if (ip2->type != IP_NODE) continue; From 75ce7acda5a8050592bf5ac6886738d37fae9fd8 Mon Sep 17 00:00:00 2001 From: Michal Pleban Date: Tue, 14 Jul 2026 00:41:12 +0200 Subject: [PATCH 67/67] z8001: collapse call-cleanup + re-push into an in-place store 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 (free K, re-allocate K). Collapse it to a single in-place store: drop the inc and rewrite the push as "ld/ldl (rr14),reg". SP is unchanged, the value is re-provided from the (preserved) register, and any following pushes proceed normally -- so no dec is needed and it stays correct even if the first callee overwrote its own argument slot. Only a register push source qualifies (a memory/immediate push has no store-to-memory form); an intervening label blocks the collapse, so basic-block boundaries are honored. Implemented as a whole-output text peephole: pass-2 assembly is printf'd straight to stdout with no instruction buffer, so bjobcode() captures the compilation's stdout to a temp file and ejobcode() streams it back through a one-line-lookback filter. arch/z8001/code.c only -- no shared MIP changes, and output stays direct (no peephole) if capture cannot be set up. -98B text across the -O1 corpus (48 sites); sweep 100 OK status-identical; sort/scat/m4/pr verified byte-identical to native /bin in the c900 emulator. Co-Authored-By: Claude Opus 4.8 (1M context) --- arch/z8001/code.c | 141 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) diff --git a/arch/z8001/code.c b/arch/z8001/code.c index 899107e45..b868ceacb 100644 --- a/arch/z8001/code.c +++ b/arch/z8001/code.c @@ -29,6 +29,10 @@ #include "pass1.h" +#include +#include +#include /* dup, dup2, close, fileno (output-capture peephole) */ + #ifndef LANG_CXX #define NODE P1ND #undef NIL @@ -230,9 +234,124 @@ bfcode(struct symtab **sp, int cnt) * 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); + } } /* @@ -266,6 +385,28 @@ bjobcode(void) * 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; + } + } } /*