-
-
Notifications
You must be signed in to change notification settings - Fork 6.6k
Open
Labels
Description
Version
30.2.0
Steps to reproduce
In a ts project such as this, i find no problems when compiling and executing:
import { bin } from "./utils";
import { Register, Registers } from "./register";
import { InstructionRegistry } from "./instructionRegistry";
import { SubInstruction } from "./instructions/rtype";
let a = new SubInstruction(Registers.get(1), Registers.get(2), Registers.get(3))
for (let el of InstructionRegistry.getInstance().getBinaryRegistry().keys()) {
let i = InstructionRegistry.getInstance().getBinaryRegistry().get(el) as any;
if (i) {
let b = bin(el, 32);
console.log(b, i.tag, i.opcode, i.f3, i.f7);
}
}// instructionRegistry.ts
import { Instruction } from "./instruction";
export class InstructionRegistry {
private static instance: InstructionRegistry;
private binaryRegistry = new Map<number, typeof Instruction>();
private tagRegistry = new Map<string, typeof Instruction>();
private constructor() { }
static getInstance(): InstructionRegistry {
if (!InstructionRegistry.instance) {
InstructionRegistry.instance = new InstructionRegistry();
}
return InstructionRegistry.instance;
}
static key(opcode: number, f3: number | null, f7: number | null): number {
const f3Val = f3 ?? 0;
const f7Val = f7 ?? 0;
return (f7Val << 10) | (f3Val << 7) | opcode;
}
static register(f: Function) {
const i = f as typeof Instruction;
const key = InstructionRegistry.key(i.opcode, i.f3, i.f7);
console.log("registring " + i + " with key " + key);
InstructionRegistry.getInstance().binaryRegistry.set(key, i);
InstructionRegistry.getInstance().tagRegistry.set(i.tag, i);
}
getBinaryRegistry(): Map<number, typeof Instruction> {
return this.binaryRegistry;
}
getTagRegistry(): Map<string, typeof Instruction> {
return this.tagRegistry;
}
}// instruction.ts
import { InstructionRegistry } from "./instructionRegistry";
export abstract class Instruction {
abstract execute(): void;
abstract encode(): number;
abstract disassemble(): string;
static opcode: number = 0;
static f3: number | null = null;
static f7: number | null = null;
static tag: string = "";
// get fields for instances
get opcode(): number {
return (this.constructor as typeof Instruction).opcode;
}
get f3(): number | null {
return (this.constructor as typeof Instruction).f3;
}
get f7(): number | null {
return (this.constructor as typeof Instruction).f7;
}
get tag(): string {
return (this.constructor as typeof Instruction).tag;
}
static decode(encoded: number): Instruction {
let opcode = 0;
let f3: number | null = null;
let f7: number | null = null;
let key: number;
let instructionClass: typeof Instruction;
let possibleInstructionClass: typeof Instruction | undefined;
let prepareDecoding = () => { return (instructionClass as any).factoryFromBinary(encoded) }
opcode = encoded & 0b1111111;
key = InstructionRegistry.key(opcode, f3, f7);
possibleInstructionClass = InstructionRegistry.getInstance().getBinaryRegistry().get(key);
if (!possibleInstructionClass)
throw new Error("opcode not found");
instructionClass = possibleInstructionClass;
f3 = (encoded >> 12) & 0b111;
key = InstructionRegistry.key(opcode, f3, f7);
possibleInstructionClass = InstructionRegistry.getInstance().getBinaryRegistry().get(key);
if (possibleInstructionClass)
instructionClass = possibleInstructionClass;
f7 = (encoded >> 25) & 0b1111111;
key = InstructionRegistry.key(opcode, f3, f7);
possibleInstructionClass = InstructionRegistry.getInstance().getBinaryRegistry().get(key);
if (possibleInstructionClass)
instructionClass = possibleInstructionClass;
if (!instructionClass) {
throw new Error(Unknown instruction: opcode=${opcode}, f3=${f3}, f7=${f7});
}
return prepareDecoding();
}
static factoryFromBinary(encoded: number): Instruction {
throw new Error("factory must be implemented by subclass");
}
/**
* a static method to parse the assembly line into an object representation.
*/
static assemble(line: string): Instruction {
let tag = line.split(" ")[0];
let parameters = line.substr(line.indexOf(" ") + 1);
let instructionClass = InstructionRegistry.getInstance().getTagRegistry().get(tag);
if (!instructionClass)
throw new Error(instruction ${tag} not implemented.);
return (instructionClass as any).factoryFromBinary(parameters);
}
static factoryFromTag(parameters: string): Instruction {
throw new Error("factory must be implemented by subclass");
}
}// rtype.ts
import { Instruction } from "../instruction"
import { InstructionRegistry } from "../instructionRegistry";
import { Register, Registers } from "../register";
abstract class RTypeInstruction extends Instruction {
destination: Register;
source1: Register;
source2: Register;
static opcode = 0b0110011;
constructor(destination: Register, source1: Register, source2: Register) {
super();
this.destination = destination;
this.source1 = source1;
this.source2 = source2;
}
encode(): number {
let encoded = 0;
let shift = 0;
encoded += this.opcode;
shift += 7;
encoded += this.destination.index << shift;
shift += 5;
encoded += (this.f3 || 0) << shift;
shift += 3;
encoded += this.source1.index << shift;
shift += 5;
encoded += this.source2.index << shift;
shift += 5;
encoded += (this.f7 || 0) << shift;
return encoded;
}
static factoryFromBinary(encoded: number): Instruction {
const rd = (encoded >> 7) & 0b11111;
const rs1 = (encoded >> 15) & 0b11111;
const rs2 = (encoded >> 20) & 0b11111;
return new (this as any)(
Registers.get(rd),
Registers.get(rs1),
Registers.get(rs2)
) as Instruction;
}
disassemble(): string {
return ${this.tag} ${this.destination}, ${this.source1}, ${this.source2}
}
static factoryFromAssembly(parameters: string): Instruction {
let p = parameters.split(",");
const destination = Registers.parse(p[0])
const source1 = Registers.parse(p[1]);
const source2 = Registers.parse(p[2]);
return new (this as any)(
destination,
source1,
source2
) as Instruction;
}
}
@InstructionRegistry.register
export class AddInstruction extends RTypeInstruction {
static tag = "add";
execute(): void {
this.destination.value = this.source1.value + this.source2.value;
}
}
@InstructionRegistry.register
export class SubInstruction extends RTypeInstruction {
static tag = "sub";
static f7 = 0b0100000;
execute(): void {
this.destination.value = this.source1.value - this.source2.value;
}
}
@InstructionRegistry.register
export class XorInstruction extends RTypeInstruction {
static tag = "xor";
static f3 = 0b100;
execute(): void {
this.destination.value = this.source1.value ^ this.source2.value;
}
}
@InstructionRegistry.register
export class OrInstruction extends RTypeInstruction {
static tag = "or";
static f3 = 0b110;
execute(): void {
this.destination.value = this.source1.value | this.source2.value;
}
}
@InstructionRegistry.register
export class AndInstruction extends RTypeInstruction {
static tag = "and";
static f3 = 0b111;
execute(): void {
this.destination.value = this.source1.value & this.source2.value;
}
}
But in a Jest testing environment, the registry does not behave deterministically:
export default {
preset: 'ts-jest',
testEnvironment: 'node',
}import { InstructionRegistry } from "../../src/riscv/instructionRegistry";
import { AddInstruction, SubInstruction, XorInstruction, OrInstruction, AndInstruction } from "../../src/riscv/instructions/rtype";
import { Registers } from "../../src/riscv/register";
test("registry", () => {
let sub = new SubInstruction(Registers.get(1), Registers.get(2), Registers.get(3));
console.log(InstructionRegistry.getInstance().getBinaryRegistry());
});Expected behavior
Tests should behave in a way that is consistent with the production environment. The use of decorators is not respected in Jest, though.
Actual behavior
The output i'm logging shows how all the concrete Instruction classes are trying to be registred with the same key, cause opcode is the only defined number. The static properties f3 and f7 are not being correctly inherited by the parent class.
PASS test/riscv/execution.test.ts
PASS test/riscv/instructions.test.ts
● Console
console.log
registring class extends _classSuper {
execute() {
this.destination.value = this.source1.value + this.source2.value;
}
} with key 51
at Function.register (src/riscv/instructionRegistry.ts:26:11)
console.log
registring class extends _classSuper {
execute() {
this.destination.value = this.source1.value - this.source2.value;
}
} with key 51
at Function.register (src/riscv/instructionRegistry.ts:26:11)
console.log
registring class extends _classSuper {
execute() {
this.destination.value = this.source1.value ^ this.source2.value;
}
} with key 51
at Function.register (src/riscv/instructionRegistry.ts:26:11)
console.log
registring class extends _classSuper {
execute() {
this.destination.value = this.source1.value | this.source2.value;
}
} with key 51
at Function.register (src/riscv/instructionRegistry.ts:26:11)
console.log
registring class extends _classSuper {
execute() {
this.destination.value = this.source1.value & this.source2.value;
}
} with key 51
at Function.register (src/riscv/instructionRegistry.ts:26:11)
console.log
Map(1) {
51 => [class AndInstruction extends RTypeInstruction] { tag: 'and', f3: 7 }
}
at Object.<anonymous> (test/riscv/instructions.test.ts:7:10)
This is avoided with the use of static blocks:
// @InstructionRegistry.register
export class AddInstruction extends RTypeInstruction {
static tag = "add";
execute(): void {
this.destination.value = this.source1.value + this.source2.value;
}
static {
InstructionRegistry.register(this);
}
}And the output is:
vale@msi:~/.loc+/dir/sou+/ts/parser(master)# pnpm run test
> parser@1.0.0 test /home/valerio/.local/dir/source/ts/parser
> jest
PASS test/riscv/execution.test.ts
PASS test/riscv/instructions.test.ts
● Console
console.log
registring class AddInstruction extends RTypeInstruction {
execute() {
this.destination.value = this.source1.value + this.source2.value;
}
} with key 51
at Function.register (src/riscv/instructionRegistry.ts:26:11)
console.log
registring class SubInstruction extends RTypeInstruction {
execute() {
this.destination.value = this.source1.value - this.source2.value;
}
} with key 32819
at Function.register (src/riscv/instructionRegistry.ts:26:11)
console.log
registring class XorInstruction extends RTypeInstruction {
execute() {
this.destination.value = this.source1.value ^ this.source2.value;
}
} with key 563
at Function.register (src/riscv/instructionRegistry.ts:26:11)
console.log
registring class OrInstruction extends RTypeInstruction {
execute() {
this.destination.value = this.source1.value | this.source2.value;
}
} with key 819
at Function.register (src/riscv/instructionRegistry.ts:26:11)
console.log
registring class AndInstruction extends RTypeInstruction {
execute() {
this.destination.value = this.source1.value & this.source2.value;
}
} with key 947
at Function.register (src/riscv/instructionRegistry.ts:26:11)
console.log
Map(5) {
51 => [class AddInstruction extends RTypeInstruction] { tag: 'add' },
32819 => [class SubInstruction extends RTypeInstruction] { tag: 'sub', f7: 32 },
563 => [class XorInstruction extends RTypeInstruction] { tag: 'xor', f3: 4 },
819 => [class OrInstruction extends RTypeInstruction] { tag: 'or', f3: 6 },
947 => [class AndInstruction extends RTypeInstruction] { tag: 'and', f3: 7 }
}
at Object.<anonymous> (test/riscv/instructions.test.ts:12:11)
console.log
Map(5) {
51 => [class AddInstruction extends RTypeInstruction] { tag: 'add' },
32819 => [class SubInstruction extends RTypeInstruction] { tag: 'sub', f7: 32 },
563 => [class XorInstruction extends RTypeInstruction] { tag: 'xor', f3: 4 },
819 => [class OrInstruction extends RTypeInstruction] { tag: 'or', f3: 6 },
947 => [class AndInstruction extends RTypeInstruction] { tag: 'and', f3: 7 }
}
at Object.<anonymous> (test/riscv/instructions.test.ts:18:10)
PASS test/riscv/architecture.test.ts
But the semantics of decorators are lost.
Environment
System:
OS: Linux 6.14 Arch Linux
CPU: (20) x64 12th Gen Intel(R) Core(TM) i7-12700K
Binaries:
Node: 23.9.0 - /usr/bin/node
Yarn: 1.22.22 - /usr/bin/yarn
npm: 11.2.0 - /usr/bin/npm
pnpm: 9.5.0 - /home/valerio/.local/share/npm/bin/pnpm
Deno: 2.2.1 - /usr/bin/deno
npmPackages:
jest: ^30.2.0 => 30.2.0AndreaSimonetti2001