Pre-review on a new architecture, Clever-ISA

This is a bit early, but as the Clever-ISA project currently has a functional* toolchain for assembly, and the emulator should be close to complete, I would like to get it looked at for future inclusion in rust.

Clever-ISA is an architecture I have been developing a specfication for since June, 2021. It is a 64-bit CISC architecture, intended to be an x86 substitute. As it applies to rust, the specification prescribes a number of recommendations intended to unify toolchains producing or handling machine code, this intended to describe those recommendations as applied to rust that I would seek approval on when bringing the target formally, which will occur after a fork of rustc is capable of compiling to the target.

*Disclaimer: a fork of GNU binutils does not yet provide an assembler, and https://github.com/LightningCreations/lc-binutils is required for that (and there are some bugs to debug wrt. that, but I expect them to be solved soon).

The Proposal is:

Clever-ISA Target Architecture

Document References

All documents in this section reference either a extension specification or a technical document from the https://github.com/Clever-ISA/Clever-ISA repository, in the specs/ folder from the root. Documents labeled with an X- prefix refer to an extension which is a normative part of the Clever-ISA specification binding on implementations. Documents labeled with a D- prefix refer to a technical document that prescribes some forms of interaction with the architecture. Each named document is unique between types, and can be found in markdown form using the name of the document without a prefix within the specs folder. For example, X-main refers to the document https://github.com/Clever-ISA/Clever-ISA/blob/main/specs/main.md.

Target Names

The name of the architecture in target tuples is clever, or alternatively clever with a version suffix attached, such as clever1.0, to refer to the target features that should be enabled or disabled. The canonical name clever should be used in #[cfg(target_arch)]. Per D-toolchain's target names section, only a published version of the specification should be accepted with an explicit version name.

Target Features

Per D-toolchain, the list of target features is the same as the named list of extensions, excluding X-main which is always presumed to be available (as all implementations must implement X-main by definition). Each target feature name enables the instructions from the corresponding extension. For example, -C target-feature="+vector" would enable instructions from the X-vector extension.

By their nature, any extension that has been accepted by is not stablized in a public release is both subject to change or removal according to the governance process of Clever-ISA. It is recommended that those extensions be feature-gated for the #[target_feature] attribute until a version containing them is placed in a feature-freeze status (or at the very least, extension-freeze status when the list of extensions is stablized but the contents are not yet frozen or stable).

ABI

The C abi for Clever-ISA is prescribed by D-abi. extern "C" , primitive types, and the vector type has the ABI prescribed by D-abi. In particular, u128 and i128 have alignment 16 on this target.

Wrt. binaries, shared objects, etc. ELF is used. D-abi also prescribes the processor-specific portions for ELF applicable to the target

ABI impact on repr(Rust)/extern "Rust"

While this is non-binding, it is notably unnecessary for extern "Rust" to pass vector types on Clever-ISA indirectly, as the ABI for these types is well-specified to always use the integer registers.

core::arch::clever

Intrinsics for clever would be provided by the standard library under core::arch::clever.

The intrinsics are specified in the Intrinsics Section of D-toolchain, and would have the appropriate signatures adapted for rust. I will not list the exhaustive list for want of a (relatively) short proposal, but I will call out some specific ones with a special signature:

#[target_feature(enable="vector")]
extern "platform-intrinsic" unsafe fn __vec_unary<const OP: VecUnaryOp, const ELEM_SIZE: usize, E: Vector>(vec: &mut E);
#[target_feature(enable="vector")]
extern "platform-intrinsic" unsafe fn __vec_binary<const OP: VecBinaryOp, const ELEM_SIZE: usize, D: VectorOrScalar, S: VectorOrScalar>(a: &mut D, b: S);
#[target_feature(enable="vector")]
extern "platform-intrinsic" unsafe fn __vec_ternary<const OP: VecTernaryOp, const ELEM_SIZE: usize, D: Vector, S1: VectorOrScalar, S2: VectorOrScalar>(a: &mut D, b: S, c: S);

The enums VecUnaryOp, VecBinaryOp, VecTernaryOp are #[repr(u16)] enums that list each instruction by name and sets the discriminant to the instructions canonical opcode (For example, VecBinaryOp::Add would have a discriminant of 0x001 - do not use specialization opcodes). The input types to each intrinsic must have at least one Vector type, __vec128, __vec64, __vec32, __vec16, and __vec8). ELEM_SIZE must be a power of two that is at most the size of the largest vector type.

The vector types defined in the section have alignment equal to the size, except for __vec256 which is 16-byte aligned. core::arch::clever should also have a f16 type (possibly with a different name) for use with 16-bit floating-point intrinsics.

Inline Assembly

Inline assembly on Clever-ISA uses (exclusively) the generic syntax specified by D-asm, with the directives specified by rust inline-assembly, except that the .quad directive is deprecated. The canonical reference assembler is lc-as from the lc-binutils project, but only the subset specified by D-asm is required.

The register classes and register names are as prescribed by D-toolchain, namely:

  • reg which refers to a general purpose register (r0-r15), excluding the stack pointer (r7), and the base pointer (r6),
  • freg which refers to a floating-point register (f0-f7),
  • veclo which refers to a low-half vector register (v0l-v15l),
  • vechi which refers to a hi-half vector register (v0h-v15h),
  • vechalf which refers to a vector half register, either veclo or vechi,
  • vec which refers to a vector register pair (v0-v15)

Other than the vector pair registers, there are no overlapping register. A vector pair overlaps with both of its constitutent vector-half registers, but the vector-half registers don't overlap with the others. If a register operand is of clas vec, vechalf, veclo, or vechi, using the v specifier refers to the vector pair, using the h specifier refers to the hi half of the vector pair, and using the l specifier refers to the lo half of the vector pair. Specifiers are not used for any other kind of register, as in-assembly size specifiers are used to refer to smaller portions of a standard register. Each register, except for the full vec registers are 64-bit, but can store any value up to 64-bit (note that the entire register is clobbered in this case, as moving into a smaller portion of any register zeroes the higher portion).

The following registers are assumed not to be modified when the option preserves_flags is specified:

  • flags
  • fpcw (unless the function does not have the float extension enabled). fpcw.RND, fpcw.EMASK, fpcw.EMASKALL, and fpcw.XOPSS are assumed not the be modified regardless of preserves_flags setting unless the float extension is not enabled.

Additionally, the mode register, stack pointer (r7), and base pointer (r6), are always assumed not to be modified. Other standard inline-assembly rules also apply.

Edit: Corrected r5 to r15.

That's not enough integer registers. [EDIT: I checked the spec and "r5" appears to have been a typo for "r15," sorry about that.] A greenfields design should offer at least 16 general purpose integer registers. You have 16 integer regs but several of them have dedicated functions; you should try to minimize the number of regs that the compiler has to avoid using.

1 Like

The architecture design is basically fixed by now (version 1.0 is under an extension freeze and soon will be under feature freeze in preparation for stablization), but even if it isn't, 16 integer registers including the stack pointer and base pointer is actually a very magic number. The "opcode" of an instruction is 2 bytes, but only 12 bits are used to distinguish the actual instruction, and 4 bits are used for a control field. This gives way for "GPR Specializations" of common instructions that encodes one register operand in the control field, to save an otherwise 2-byte operand control structure. Of the 16 integer registers (called "general purpose" registers in the spec), only 3 really have any kind of implicit use (r7, r14, and r15 - though a number are implictly used by a number of instructons, but many won't come up in automatically compiled code except for the byte instructions), and only r7 and r6 (r6 not being used implicitly) is given meaning by convention. Under most circumstances, the compiler has free reign over every GPR register other than r7. It can also use vector half registers, and if needed, they can be used as integer or floating-point registers.

To be clear, the thing I'm looking to get reviewed is how rust and rustc interacts with the architecture, rather than the architecture itself.

Yes, I misunderstood. Sorry about that.

How to you plan to do codegen? LLVM backend or is rustc directly writing binaries?

I have several plans for the future. There is an llvm fork but development on that is difficult. In the coming days I'll be experimenting with both a direct codegen and cranelift to see if either is easier. My own compiler collection, lccc, currently has codegen support for Clever-ISA as well that functions (for some definition) today.

lccc is neat. I honestly do not know how happy upstream LLVM is about clever. I believe M68 was doing GlobalIsel instead of SelectionDAG. For smaller ISAs, this should come with less effort.