Yeah! Most statements translate to a single instruction, and you have to pick registers, so even though performance hasn't been a priority I think it'll tend to be pretty good. It does have some simplifications for safety and clarity rather than optimization. For example, setting a register to zero generates a copy with a 32-bit immediate rather than the xor trick most Assembly programmers would use.
I haven't tried to implement C, but can't immediately think of a reason why not. Forks strongly encouraged; if you decide to try to implement one, I'd love to contribute.
> Was that latter copying your content from somewhere?
It had occurred to me that it might be a Markdown editing service. Maybe somebody tried it out with just the closest Markdown they happened to have on hand?
However, looking closer at the Mu history, this seems like the closest version:
With the description of "minimalist" I was expecting something more like http://www.terse.com/ but this turns out to be even more verbose than regular Asm, more like writing pure machine code. Also, I'm puzzled at the author calling opcode cd syscall --- that's "int imm8", and the real syscall instruction resides on the second page (0f 05).
If you're going to write x86 machine code, then memorising the instruction set is far easier in octal:
The stuff after / is often just comments. 'syscall' feels more accurate for int 80 than 'interrupt', but the language doesn't encode the name.
I only found out about the octal after I started on SubX. I considered switching to it, but thought it might be useful to keep the same opcodes as the Intel manual.
Thank you for making that. There are are articles that mention how we forgot the octal connection to the x86 isa. Your ascii art table is just a really clean way of visualizing it.
I hope you enjoy it! Since you mentioned chibicc earlier in this thread, and you've written an assembler of your own, this might be of interest to you: https://github.com/jart/cosmopolitan/blob/master/third_party... Your work was brought to my attention maybe a year and a half ago and I think what you're doing is pretty cool. Feel free to reach out by email if there's anything I can do to help.
Oh that's not intended as a library for things like jit. It's just a program that takes an asm file generated by chibicc and turns it into an ELF .o file. Building a library for x86 codegen would be such a rabbit hole. Intel Xed did that but it was just so huge that the only part I wanted to use was its instruction length decoder.
Sorry, I find this quite confusing. I'm familiar with more than one assembly language and SubX -- to the extent I can follow the docs -- seems excessively complicated and inconsistent, compared to a conventional one. Actually this may just be bad documentation. Turning to the list of opcodes[1], which exactly are the opcodes in,
01: add r32 to rm32 (add)
03: add rm32 to r32 (add)
05: add imm32 to EAX (add)
....etc
Is "01:" the opcode? Meaning we should memorize the hex values that it is the normal assembler's job to produce? And how does something like
bb: copy imm32 to EBX (mov)
relate to the example code
bb/copy-to-ebx 1/imm32
If "bb" is the opcode, why does it need to be repeated as text (metadata?) saying the same thing, only in a different order to the table? If the way to write a "bb" instruction is the above, what is the way to write an "0f" instruction?
0f 86: jump disp32 bytes away if lesser or equal (addr, float), if ZF is set or CF is set (jcc/jbe/jna)
I mean I'm guessing it would start "0f/" but after that, how much gets repeated and with what metadata?
Yeah, OP is probably not the best place to understand what the purpose of this project is.
The list of opcodes is just a list for consulting. It's only about the opcodes, it says nothing about the expected arguments and so on. To understand how to invoke an opcode you'd have to read either the forbidding Intel manual or something terser like https://c9x.me/x86 as cited at the bottom of OP. The '/copy-to-ebx' after the slashes is just my way of helping the reader understand what the instruction does. I don't want the reader to have to consult the Intel manual for every instruction, even if I'm forcing the writer to do so.
So why am I forcing the writer to do so? Mu's goal is to build a computer where compound things can be understood in terms of their constituent parts. Most assemblers are implemented in C, and if you wanted to understand what a line of C does, in a specific context, you might imagine expanding the C into.. its generated Assembly. Now you're in a circular situation where you need to understand Assembly to understand Assembly.
So I started out with the goal of implementing an assembler in machine code. And Assembly is surprisingly complex. There's a reason nobody builds it in C anymore. So I ended up on my current trajectory, of making the code writer do more work just so the code reader doesn't have to deal with cycles.
Sorry if I've asked these years ago and just don't remember the answer.
> The '/copy-to-ebx' after the slashes is just my way of helping the reader understand what the instruction does. I don't want the reader to have to consult the Intel manual for every instruction, even if I'm forcing the writer to do so.
Why not make the comment the instruction and the bytes the (maybe even optional?) comment in that case then?
From your first post.
> The fact that C compilers are written in C contributes a lot of the complexity that makes compilers black magic to most people.
Isn't this more a symptom of C though? I'm hoping this is generally not true if you replace C with other languages (but could be very wrong). But more generally, I'm thinking you could make "the compiler's inner workings is not black magic" a constraint rather than make not writing the higher level language in the higher level language the constraint.
In my case, I tried that first route and then moved to instead having the compiler written in the higher level language but emitting output that's close enough to (my) handwritten lower level language.
I'll have to read your two part post more carefully though. Glad to see this project getting some attention, even though in an unusual fashion.
Great questions! I've actually never considered putting the comment first! I'll have to think about that one.
You're right to point out that there are two components to "C compilers written in C make compilers seem complex": the metacircularity, and C-specific difficulties. I think I was focusing on the first when I wrote that, but I can't exclude the possibility you raise. A better language might reduce the need to understand it operationally, by looking under the hood to understand what a line of code is translated to. The Mu way may well be a dead end, since the requirement of understanding translated code restricts how complex compiler optimizations can get. You probably don't want to understand Haskell's loop fusion by comparing source and generated code.
In my mind there's an idea maze where there are 3 major possibilities for improving the future of software:
a) Simple languages and translators that are easy to understand by running them. This is the Mu way.
b) Type-driven languages that are easy to understand by reading them. Haskell and OCaml seem to fit here, and they may well be the right answers.
c) Complex languages that discourage abstractions atop them. This is the APL way, and it too might end up being the right way.
I'm doing a) mostly because it seems to fit my brain better. I just can't seem to get into Haskell or OCaml or APL.
> I've actually never considered putting the comment first! I'll have to think about that one.
I'm sure there are many competing constraints so definitely don't do it because I'm suggesting this on a whim. :) My reasoning is that as a human reader, the comment is the more readable part, so I'd want to see it first. And for a computer, it probably doesn't care if the op code appears first or not.
> You probably don't want to understand Haskell's loop fusion by comparing source and generated code.
Indeed. But even though C and Haskell are very different, I think they share a common philosophy about compilation where you can basically do whatever you want as long as it still produces the same result.
I vaguely remember looking at Python generate bytecode (with `dis.dis`) and seeing it wasn't too bad. I haven't tried it on a larger program though.
There's tcc (and more recently chibicc that I haven't had a chance to check out yet) that you're probably already aware of. Is the generated output still pretty bad.
even though it's not quite optimized for this purpose and the code itself is still a bit unclean. If there was a syntax highlighter for the low level language, I'd probably highlight "[", "]" and "bind:" as a start. I can try to clarify any obscure syntax or primitive.
Some more general ideas to get aroud the issue.
- Invoke optimization only when asked specifically (and apply the optimization locally). That is, optimization would need at least additional syntax in the language.
- Explicitly track correspondance between source and target (at the character or token level) and also do this in each optimization pass. Maybe even keep the intermediate values of each pass so you can browse through it like a stack trace.
> In my mind there's an idea maze where there are 3 major possibilities for improving the future of software:
I guess I'm trying another route even though I don't know if it fits the definition of improving the future of software.
d) Have programmers make their own compiler/interpreter and language by giving them the tools and knowledge to do that (more) easily.
This would (hopefully) avoid the black box/magic issue since the programmer would know the details of the inner workings by virtue of having written it. Though I'm most definitely very far from the goal and the questions can be asked about how to improve their target language.
Oh your project looks familiar. Though I might have seen it a long time ago. I'll take a closer look.
> My reasoning is that as a human reader, the comment is the more readable part, so I'd want to see it first. And for a computer, it probably doesn't care if the op code appears first or not.
Yeah, for sure. One rebuttal that comes to mind is the dictum, "don't get suckered by comments, debug code." Comments are useful, but too much emphasis on them has led to dark times in my past :)
I've read through more of you post can came across the bottom comment (don't know how to permalink to it) which better expresses my comment above.
> An optimizing linter has the problem of being destructive. It goes like this:
> The programmer will write his or her program in a readable way. They'll run it through the compiler, which points out that something can be optimized, the programmer—having already gone through the process of writing the first implementation with all its constraints and other ins and outs fresh in their mind—will slap their head and mutter "of course!", and then replace the original naive implementation with one based on the notes the compiler has given. Chances are high that the result will be less comprehensible to other programmers who come along—or even to the same programmer revisiting their own code 6 months later.
Also a data point and word of warning about (lack of) optimization. My own projects (one of which was mostly hand-written in x86 assembly) have been pretty heavily stalled from speed issues, that sent me on significant detours. Since you are working with your own compiler/interpreter to implement your levels, you are directly affected by their compilation speeds as you iterate. Even with modern hardware, they can quickly become too slow to be even usable.
This is unfortunately another consequence of having too much black magic in (C) compilers. So we get the wrong intuition about how fast computers are.
Were your languages very high-level? If so, that kinda rhymes with my experience on past projects. The more expressive the language, the easier it is for programs to create combinatorial explosions that slow everything down if compiled naively.
The language is high-level but I wouldn't necessarily say very high level. But because I'm trying to spin up language features at runtime (like a lot of Forth does), there are a few layers on top of the language primitives.
I wish there was some framework for me to add optimizations as I go along, especially if there could be some speed gauranttee. Though in my case, I'd like to also not lose the relation to the original source (like C does when values are optimized out).
By the way, I think the example has a mistake. It uses copy-to-ebx, but the following paragraph in subx.md refers to copy-to-eax. (Edit: learned a bit more) Since you're doing a syscall, it seems it may actually be correct for the "return value" (really the argument to the syscall) to be in ebx, so in that case you'd just want to change the text to agree.
https://github.com/akkartik/mu#readme
OP is an old version of the Readme from a year ago. Here's the current page on SubX: https://github.com/akkartik/mu/blob/main/subx.md