#include #include #include #include #include #include /* hopefully not too difficult to follow what's goind on, but to start with we * assume we have an effectively infinite amount of virtual registers and we * move everything down into them. Typically the top output is used as input in * some other step. */ static void set_reg(struct loc *loc, size_t reg) { loc->kind = LOC_REG; loc->reg = reg; } static void set_mem(struct loc *loc, size_t reg, long long off, size_t width) { loc->kind = LOC_MEM; loc->reg = reg; loc->off = off; loc->width = width; } static size_t trivial_type_width(struct ast_node *type) { switch (AST_TYPE(type).kind) { case AST_TYPE_POINTER: return 3; case AST_TYPE_PRIMITIVE: { if (AST_PRIMITIVE_TYPE(type).type == AST_I27) return 3; if (AST_PRIMITIVE_TYPE(type).type == AST_I9) return 1; abort(); break; } default: abort(); } return 3; } static struct ops *create_ops() { struct ops *ops = calloc(1, sizeof(struct ops)); struct op *op = calloc(1, sizeof(struct op)); op->opcode = OP_COMMENT; op->string = strdup("start"); ops->base = op; ops->head = op; return ops; } static void destroy_ops(struct ops *ops) { if (!ops) return; struct op *op = ops->base; while (op) { struct op *prev = op; op = op->next; switch (prev->opcode) { case OP_COMMENT: free((void *)prev->string); break; case OP_LABEL: free((void *)prev->string); break; default: } free(prev); } free(ops); } static size_t next_virtual_reg() { static size_t reg = 1; return reg++; } static int lower_op(struct ast_node *n, struct ops *ops); #define HEAD_OUTPUTS(ops) ops->head->outputs static struct op *op_head(struct ops *ops) { return ops->head; } static struct op *append_op(struct ops *ops, enum opcode opcode) { static size_t i = 1; struct op *op = op_head(ops); struct op *n = calloc(1, sizeof(struct op)); n->opcode = opcode; op->next = n; op->loc = i++; ops->head = n; return n; } /* this should really only be called after lifetime analysis, should I add in * some checks against incorrect use...? */ static struct op *insert_op_after(struct op *op, enum opcode opcode) { struct op *n = calloc(1, sizeof(struct op)); n->opcode = opcode; n->next = op->next; op->next = n; return n; } static int lower_proc(struct ast_node *n, struct ops *ops) { struct op *op = append_op(ops, OP_LABEL); /** @todo name mangling */ struct ast_node *id = AST_PROC(n).id; op->string = strdup(AST_ID(id).id); int ret = lower_op(AST_PROC(n).body, ops); if (ret) return ret; return 0; } static int lower_block(struct ast_node *n, struct ops *ops) { struct ast_node *b = AST_BLOCK(n).body; while (b) { int ret = lower_op(b, ops); if (ret) return ret; b = b->next; } return 0; } static int lower_var(struct ast_node *n, struct ops *ops) { struct ast_node *d = scope_find_var(n->scope, AST_VAR(n).id); /* structs should be handled as well, would it be better to try and fit * them into regs or just dump them on the stack to make sure we don't * immediately run out of registers? */ struct ast_node *type = d->type; if (AST_TYPE(type).kind != AST_TYPE_PRIMITIVE && AST_TYPE(type).kind != AST_TYPE_POINTER) { semantic_error(n->scope->fctx, n, "only trivial type lowering implemented"); return -1; } d->reg = next_virtual_reg(); if (AST_VAR(n).init) { int ret = lower_op(AST_VAR(n).init, ops); if (ret) return ret; struct loc inputs = HEAD_OUTPUTS(ops); /* work with only primitive types for now */ assert(inputs.next == NULL); struct op *op = append_op(ops, OP_MV); op->inputs = inputs; set_reg(&op->outputs, d->reg); } return 0; } static int lower_cast(struct ast_node *n, struct ops *ops) { /** @todo lob off extra trits or something? */ return lower_op(AST_CAST(n).expr, ops); } static int lower_const(struct ast_node *n, struct ops *ops) { if (AST_CONST(n).kind != AST_CONST_INTEGER) { semantic_error(n->scope->fctx, n, "only integer constant lowering implemented"); return -1; } struct op *op = append_op(ops, OP_LI); op->constant = AST_CONST(n).integer; set_reg(&op->outputs, next_virtual_reg()); return 0; } static int lower_assign(struct ast_node *n, struct ops *ops) { int ret = lower_op(AST_ASSIGN(n).from, ops); if (ret) return ret; struct loc from = HEAD_OUTPUTS(ops); ret = lower_op(AST_ASSIGN(n).to, ops); if (ret) return ret; struct loc to = HEAD_OUTPUTS(ops); struct op *op = append_op(ops, OP_MV); op->inputs = from; op->outputs = to; return 0; } static int lower_unop(struct ast_node *n, struct ops *ops) { if (AST_UNOP(n).op != AST_DEREF) { semantic_error(n->scope->fctx, n, "unop lowering not implemented"); return -1; } if (AST_UNOP(n).op == AST_DEREF) { int ret = lower_op(AST_UNOP(n).expr, ops); if (ret) return ret; struct loc *l = &HEAD_OUTPUTS(ops); if (l->kind == LOC_MEM) { /* load value from memory and use it as the next step * location */ struct op *op = append_op(ops, OP_LDW); op->inputs = *l; /* hard coded 3 for now, pointer is three trytes */ set_mem(&op->outputs, next_virtual_reg(), 0, 3); return 0; } /* use the value as if it was a memory location */ size_t w = trivial_type_width(n->type); set_mem(l, l->reg, 0, w); return 0; } return -1; } static int lower_id(struct ast_node *n, struct ops *ops) { struct ast_node *d = scope_find_var(n->scope, n); assert(d->reg); /* feels like a massive hack, but gives fairly readable debug output so * I'll keep it for now. Eventually once I figure out what I'm doing * this whole system will probably have to be rewritten anyway. */ struct op *op = append_op(ops, OP_COMMENT); op->string = strdup(AST_ID(n).id); set_reg(&op->outputs, d->reg); return 0; } static int lower_ret(struct ast_node *n, struct ops *ops) { if (AST_RETURN(n).expr) { int ret = lower_op(AST_RETURN(n).expr, ops); if (ret) return ret; } struct op *op = append_op(ops, OP_RET); if (AST_RETURN(n).expr) set_reg(&op->inputs, HEAD_OUTPUTS(ops).reg); return 0; } static int lower_op(struct ast_node *n, struct ops *ops) { int ret = 0; switch (n->node_type) { case AST_PROC: ret = lower_proc(n, ops); break; case AST_BLOCK: ret = lower_block(n, ops); break; case AST_VAR: ret = lower_var(n, ops); break; case AST_CAST: ret = lower_cast(n, ops); break; case AST_CONST: ret = lower_const(n, ops); break; case AST_ASSIGN: ret = lower_assign(n, ops); break; case AST_UNOP: ret = lower_unop(n, ops); break; case AST_ID: ret = lower_id(n, ops); break; case AST_RETURN: ret = lower_ret(n, ops); break; default: semantic_error(n->scope->fctx, n, "unimplemented lowering"); return -1; } if (ret) return ret; return 0; } static void print_locs(struct loc *locs) { if (locs->kind == LOC_NONE) return; printf(" ( "); while (locs) { if (locs->kind == LOC_MEM) printf("%lld(", locs->off); printf("r%zd", locs->reg); if (locs->kind == LOC_MEM) printf(", %zd)", locs->width); printf(" "); locs = locs->next; } printf(")"); } static void print_op(struct op *op) { if (!op) return; print_locs(&op->outputs); putchar(' '); switch (op->opcode) { case OP_LABEL: printf("%s:", op->string); break; case OP_COMMENT: printf("/* %s */", op->string); break; case OP_LI: printf("li %lld", op->constant); break; case OP_LA: printf("la %s", op->string); break; case OP_ADD: printf("add"); break; case OP_ADDI: printf("addi %lld", op->constant); break; case OP_STT: printf("stt"); break; case OP_LDT: printf("ldt"); break; case OP_STW: printf("stw"); break; case OP_LDW: printf("ldw"); break; case OP_RET: printf("ret"); break; case OP_MV: printf("mv"); break; default: printf("unimp"); break; } putchar(' '); print_locs(&op->inputs); printf("\n"); } static void print_ops(struct ops *ops) { if (!ops) return; struct op *base = ops->base; while (base) { print_op(base); base = base->next; } } static enum opcode st_opc(struct loc *loc) { switch (loc->width) { case 1: return OP_STT; case 3: return OP_STW; } abort(); return OP_STW; } static enum opcode ld_opc(struct loc *loc) { switch (loc->width) { case 1: return OP_LDT; case 3: return OP_LDW; } abort(); return OP_LDW; } static int realize_moves(struct ops *ops) { /* completely arbitrary and *will* have to be made better in the near * future */ static const size_t tmp_reg = 9; struct op *prev = NULL; struct op *op = ops->base; for (; op; prev = op, op = op->next) { if (op->opcode != OP_MV) { continue; } struct loc *i = &op->inputs; struct loc *o = &op->outputs; assert(i->next == NULL); assert(i->next == NULL); if (i->kind == LOC_REG && o->kind == LOC_REG) { /* if move is between the same register, skip it */ if (i->reg == o->reg && prev) prev->next = op->next; } else if (i->kind == LOC_REG && o->kind == LOC_MEM) { op->opcode = st_opc(o); } else if (i->kind == LOC_MEM && o->kind == LOC_REG) { op->opcode = ld_opc(i); } else if (i->kind == LOC_MEM && o->kind == LOC_MEM) { op->opcode = ld_opc(i); /* our input/output are references, so call this before * setting registers for our original operation. Also, * at this point the lifetime stuff is finished, so no * big deal that our node IDs change */ struct op *n = insert_op_after(op, st_opc(o)); set_mem(&n->outputs, o->reg, o->off, o->width); set_reg(&n->inputs, tmp_reg); set_reg(&op->outputs, tmp_reg); } } return 0; } static int alloc_regs(struct ops *ops) { /** @todo analyze lifetime, for now just convert moves to correct ldst * etc. */ /** @todo lifetime analysis could be done by looping over all ops, and * when we encounter a virtual register we haven't seen before, add it * to a list. When we encounter it used again, extend its lifetime to * wherever we are in the function. */ return realize_moves(ops); } int lower_ops(struct scope *root, const char *fname) { FILE *f = fopen(fname, "w"); /* main should probably be mangled here as well */ fprintf(f, "jal x21, main\n"); /* tell simulator to turn off (very much temp) */ fprintf(f, "li x1, 3\n"); fprintf(f, "csrrw mpower, x0, x1\n"); /* this can potentially be parallelized in the future */ int ret = 0; for (struct actual *a = root->actuals; a; a = a->next) { assert(a->node); struct ops *ops = create_ops(); if ((ret = lower_op(a->node, ops))) { destroy_ops(ops); break; } printf("Lowered ops before lifetime analysis:\n"); print_ops(ops); if ((ret = alloc_regs(ops))) { destroy_ops(ops); break; } printf("Lowered ops after lifetime analysis:\n"); print_ops(ops); if ((ret = print_asm(ops, f))) { /* kind of silly as of now but eh */ destroy_ops(ops); break; } destroy_ops(ops); } fclose(f); return ret; }