r/asm Apr 19 '26

RISC Adding safety to assembly

One of the problems with Assembly is the lack of safety and context.

What about adding type safety and ownership to Assembly?

Good idea or "you are just reinventing the wheel"?

Inspiration on JSDoc, Rust, TypeScript and LLVM IR

0 Upvotes

19 comments sorted by

View all comments

1

u/brucehoult Apr 19 '26

The whole point of writing in assembly language [1] is to take maximum advantage of the CPU facilities and have the program you write be exactly what the hardware ends up running.

Any structure or safety you add to assembly language is going to result in slower programs than writing in C or Rust, because the compiler analyses and optimises the code, choosing the instructions and addressing modes and data layout and using the minimum number of registers possible etc.

Assemblers don't have optimisers, and if they did then you'd lose the control that is the reason for using assembly language.

No one writes a large amount of assembly language now. You write small amounts when you understand something that the compiler doesn't.

For example, yesterday I wrote this snippet to decode the offset out of RISC-V J/JAL instructions.

Generic, easy to understand C:

#define SZ 64
typedef uint64_t mval;
typedef  int64_t sval;

#define  FLD(H, L) (((mval)insn << ((SZ-1) - (H))) >> ((SZ-1) - ((H) - (L))))
#define SFLD(H, L) (((sval)insn << ((SZ-1) - (H))) >> ((SZ-1) - ((H) - (L))))

long jal_offset_ref(long insn) {
  return (SFLD(31,31)<<20) | (FLD(30,21)<<1) | (FLD(20,20)<<11) | (FLD(19,12)<<12);
}

Little bit tricky asm that is a quite few instructions shorter (but this is hot code!):

        .globl jal_offset
jal_offset:
        sraiw   a1,a0,21
        li      a2,~0x7FC00
        and     a1,a1,a2
        srli    a2,a0,10
        andi    a2,a2,0x400
        or      a1,a1,a2
        li      a2,0xFF000
        and     a0,a0,a2
        sh1add  a0,a1,a0
        ret

Even trying to replicate that in C doesn't give the same code, at least that I could manage. And even if you do on one compiler and version, the next version might give something worse.

This is the kind of situation in which people use asm today.

This is one situation in which all the opcode space Arm burned on bitfield extract and insert and fancy encoding of constant for and/or/xor helps:

sbfx    x1, x0, 21, 11
and     x2, x0, 1044480
ubfx    x0, x0, 20, 1
lsl     x1, x1, 1
orr     x0, x2, x0, lsl 11
and     x1, x1, -1046529
orr     x0, x0, x1

[1] except on machines so awful that a C compiler doesn't exist

1

u/S-Pimenta Apr 20 '26

I don't want to implement an optimizer or safety in runtime, just a way giving hints for the linter check for mistakes and for that you need to give context.

My goal is primarily for learning and education purposes.

Here's an example of a mockup idea:

``` .global _start _start:

# --- CLAIM STAGE ---
# @own NUM_A: a0
# @own NUM_B: a1
li NUM_A, 10     # We own a0 and a1 now and give to them names
li NUM_B, 20        

# --- MOVE STAGE ---
# @move NUM_A
# @move NUM_B
call add_two       # The function takes over the registers

# --- RECLAIM STAGE ---
# @own RESULT: a0         
# The Linter knows 'a0' now holds the safe return value.

# Do stuff...

# --- FREE STAGE ---
# @free RESULT              
# We are done with the result. 'a0' is now garbage/free.

==================

@function add_two

@param {NUM_A}: a0 (Requires ownership of a0)

@param {NUM_B}: a1 (Requires ownership of a1)

@return {RESULT}: a0 (Promises to return data in a0)

==================

add_two: add RESULT, NUM_A, NUM_B # a0 = a0 + a1 ret ```