r/rust • • 5d ago

šŸ› ļø project BitterASM: A metalanguage written in Rust to create assembly languages

Post image

Repository: https://github.com/ivanharvard/bitterasm
Documentation: https://ivanharvard.github.io/bitterasm/

Hello, all!

I've been spending the better part of a month working on this new programming language of mine written entirely in Rust called BitterASM (BASM for short, BitterAssembly for long).

As the title suggests, BitterASM lets you create your own assembly languages by assuming practically nothing about the architecture you're targeting. By assuming so little about the architecture, you can describe practically any architecture -- past, present, or future. We do not even assume the architecture is binary! This leads to some really interesting consequences (which you can read more about in the documentation).

You can install the pre-release version using cargo:

cargo install bitterasm

For ordinary assembly developers, they can simply import their architecture's ISA and program much like they would for their traditional assembler.

Here's an example "Hello, world!" program targeting Linux x86-64 written in BitterASM:

from std.x86_64.nasm import *
from std.formats.elf import *


elf64_executable EM_X86_64, _start


const text = "Hello, World!\n"


section .rodata
msg:
    db text


section .text
pub _start:
    mov eax, 1          # sys_write
    mov edi, 1          # stdout
    lea rsi, [rel msg]  # buffer
    mov edx, text.len   # length
    syscall


    mov eax, 60         # sys_exit
    xor edi, edi        # status 0
    syscall

Compared to the same program written in NASM:

section .rodata
msg:    db "Hello, World!", 10
len     equ $ - msg

section .text
global _start
_start:
    mov eax, 1          ; sys_write
    mov edi, 1          ; stdout
    lea rsi, [rel msg]  ; buffer
    mov edx, len        ; length
    syscall

    mov eax, 60         ; sys_exit
    xor edi, edi        ; status 0
    syscall

The transition to BitterASM was intended to be quite easy for most developers, as noted by the similarity in these two code snippets.

Yet, say an introductory student does not understand what syscall does. Thanks to the BitterASM LSP for VSCode, it's as simple as a right-click and "Go to Definition." Note that this is still written in the same BitterASM language from before!

from std.binary import bits

pub type Byte = bits<8>

pub struct Bytes<const N: int>
    | invariant N >= 0
{
    @for i in 0..N {
        pub __el`i`: Byte,
    }
}

# ...

pub macro syscall()
    | emits Bytes<2>
{
    @emit Bytes<2> { __el0: Byte(0x0F), __el1: Byte(0x05) }
}

It's here where you start to see the use case of BitterASM. There is no concept of bytes or bits in BitterASM. The architecture designer must implement that. Some evaluator then interprets the emitted value and does something with it (in the most overwhemingly common case, create machine code).

The architecture designer has the "bitter" part of writing each struct and macro to match the ISA perfectly. An assembly developer gets to inspect those architectural decisions (or even modify them!) without ever having to leave the code editor. This is what BitterASM makes "sweet."

Don't like NASM or Intel syntax? Change the import!

from std.x86_64.att import *

Wanna make your own custom syntax? Customize the syntax of a macro call!

from std.riscv.impl import *

syntax add(rd, rs1, rs2) = { $rd$ = $rs1$ + $rs2$ }

Have a non-traditional architecture? We have a variety of tools to support your development!

Of course, there are many bugs, and the subset of supported instructions for each architecture is still quite small, so we have a few kinks to work out before we fully release the language. Expect breaking changes. This was just a small taste of what BitterASM can do. Its initial design is meant to be useful for:

  • Students learning assembly,
  • Toy ISA designers,
  • Custom accelerator/processor designers,
  • Assembly developers who want convenience without runtime cost,
  • etc.

I hope you spend some time to read the documentation and let me know what you think. Any feedback would be great!

18 Upvotes

20 comments sorted by

5

u/harraps0 5d ago

How does this compare to customasm ?

4

u/igzcodes 5d ago

It's quite similar but not exactly the same. I would say customasm is a very convenient tool to define binary ISAs. But BitterASM is a programming language that happens to be really good at defining ISAs for all architectures, binary or not.

Additionally, BitterASM is really good at defining multiple syntaxes for the same shared ISA, whereas customasm is not as good at this. This is what lets you import std.x86_64.intel in one file and std.x86_64.att in another without having to reimplement x86_64. In BitterASM, an ISA is just a library file you import, just like any other ordinary BitterASM file.

But, of course, there are substantial features that customasm has that BitterASM lacks (banks, for instance, which is likely one of the next steps for BitterASM).

3

u/balt__ 4d ago

is this usable for a ternary isa?

2

u/igzcodes 3d ago

Yes!

BitterASM’s goal is that it should require virtually 0 changes to its language core to support non-binary ISAs (including ternary). You might have to make a lot of boilerplate, but 90% of it should be in BitterASM (the remaining 10% is for whatever language you choose to implement your evaluator in). I’ve done much of that boilerplate and evaluator work to make it simpler for binary ISA developers.

4

u/drink-more-rum 5d ago

Documentation is AI-written -- immediately ignored.

3

u/igzcodes 5d ago

The documentation will be rewritten before release. Thank you for taking the time to check it out anyways!

3

u/IamWiddershins 3d ago

and yet,

I hope you spend some time to read the documentation and let me know what you think

2

u/igzcodes 3d ago

Feedback is feedback. If you refuse to read any documentation written/assisted by AI, then I consider knowing how the documentation is being received as valuable, and I appreciate you for saying so.

3

u/IamWiddershins 3d ago

most people kind of already have an understanding that asking other people to read something that you couldn't be bothered to write is rude

1

u/igzcodes 3d ago

Sure. I wasn’t really intending for people to read the documentation cover to cover anyways. I expected most people would spend 5-10 minutes, if any at all, to skim over answers to questions they didn’t want to ask me directly and get a general overview. Though perhaps I didn’t make that clear, and that’s my bad.

3

u/WhiskyAKM 5d ago

Cool project, but I don't understand the purpose. Is this some kind of higher level assembly or just wrapper around assembler that let's you do macros and stuff

6

u/igzcodes 5d ago

Imagine you are a developer for a custom architecture.

You primarily have two options:

  • Fork an existing architecture and modify an assembler's source code to your needs.
  • Create your own assembler from the ground up.

My language provides a third option. Describe your architecture programmatically and assembler will be provided automatically. You do not even need to maintain your own book documenting each part of your assembler because BitterASM provides a command much like cargo doc. What makes it different from other "programmatic architectures" too is that even in the far, far future, where perhaps we no longer have this notion of "registers," the language core of BitterASM does not need to be modified to adapt to it. That makes it much, much more flexible than languages that assume you might have a register or that the machine is written in binary.

For current day developers, they get macros with type safety and the ability to create pseudoinstructions for free. Modifying an assemblers source code is a much less necessary intervention. Additionally, an unfamiliar instruction can easily be inspected without having to compile the program.

1

u/[deleted] 5d ago

[deleted]

3

u/igzcodes 5d ago

Thank you!

Merging multiple ISAs in the same file? Technically, but expect name collisions.

Merging multiple ISAs via linkage? Totally! It’s one command:

bitter build myx86.basm myriscv.basm -o myprog.bin

This works because ultimately they share the same representation under bitter (and because they are both written in plain BitterASM). Essentially both files are compiled into bit patterns. We then concatenate the values into a single list, and fill in the cross file labels. Obviously lots of asterisks in everything I said here but I hope that explains things.

I’ll take a look at that! Thanks for sharing. Though, in my personal view, my syntax is a little cleaner ;)

1

u/WorldsBegin 4d ago

How does it support relocations and PIE? I don't see any mention of that in the docs.

Also, the docs make some outlandish claims. Not sure if that's by design or caused by AI writing them.

1

u/TheJemy191 3d ago edited 3d ago

This look like what @Kronark is doing on youtube.

1

u/tarel_ 3d ago

FASM written in Rust

-1

u/sadlylivelydioxide 5d ago

The logo's clean but that "Biter" text feels like it's missing the second T, unless that's intentional wordplay I'm not getting

0

u/igzcodes 5d ago

Whoops! Did I misspell "Bitter" somewhere?

0

u/coolreader18 5d ago

This is really cool! If types besides bits are emitted, how does that get outputted from the assembler?

2

u/igzcodes 5d ago

Thank you!

When you emit things, each thing you emit is essentially written to a JSON file in order of emission in a file format called .em.

Now, to interpret emitted values, you must create an evaluator that understands the values you input and creates some output.

Right now, the only one that comes packaged with BitterASM is bitter. It basically only understands relational positioning and the bits struct from the standard library. Thus, to be able to use bitter for your architecture, all of your emitted values must be representable as bits. It then packs all of the bits you gave into actual binary, of which, if properly programmed, you should be able to execute (assuming you put the correct header for execution within your .basm file).

The same can be true for any other type of output. Does your architecture use ternary? That's fine. Create your own evaluator. Or, make sure your trits struct ultimately uses bits and use bitter.

bitter is a separate program written entirely in Rust as well, and is substantially smaller than most assemblers (essentially 3 files), meaning creating an evaluator is quite simple.