7 ms·
Everyone says assembly is untyped—everyone is wrong
- Krssst 27d agoSorry, somewhat of a tangent but regarding: > The %0 and %1 are positional references into a list you have to count by hand. You can name your operands in gcc inline assembly. https://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html#Output-Operands https://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html#Output-... Look for "asmSymbolicName". On a phone so not checking if it builds, but something like `asm("add %[my_out], %[my_in], #3":[my_out]"=r"(outvar):[my_in]"r"(invar):);`.
- gingerBill 27d agoThe equivalent Odin syntax looks like this: add_three :: asm(my_in: u64) -> (my_out: u64) { add my_out, my_in, 3 } out_var = add_three(in_var) Which is already infinitely more readable and requires no parochial sigils nor the arcane clobbering syntax.
- layer8 25d agoThe parent was objecting to the syntax allegedly forcing one to use “positional references into a list you have to count by hand”. Being inaccurate in your criticism just makes it appear questionable as a whole.
- astrange 22d agoThe issue is you're usually using inline asm for special cases, or else CPU features so new the compiler doesn't know about them. Which means it also doesn't know the clobbering rules. That's why it looks like an escape hatch. It is one!
- genxy 27d agoAn avenuge of research worth being sniped on is Typed Assembly Language https://en.wikipedia.org/wiki/Typed_assembly_language https://en.wikipedia.org/wiki/Typed_assembly_language https://www.cs.cornell.edu/talc/overview.html https://www.cs.cornell.edu/talc/overview.html
- gingerBill 27d agoTALs are not what I am referring to here. I am arguing that assembly is already typed and does not need extra annotation to be typed. TALs are also solving an entirely different problem.
- genxy 27d agoThe technique is good, and compilers that interact with assembly should do this, but as you outline, they basically just shove blobs of text around and hope for the best. I didn't say you were referring to TALs. Yours is a syntax level check, not type checking of the program in the normative sense. It might be more accurate refer to your technique as an "instruction signature", rather than a type. I would argue that that are complementary and not entirely different. I thought it would be interesting for folks.
- questionableans 27d agoBut a language being “typed” doesn’t tell us anything useful. Untyped languages are typed too: they’re uni-typed (every expression is an expression). I think you do your analysis a disservice by focusing on “is assembly language typed?” as the top line question. The more interesting question you examine is what do the type constraints in inline asm offer, and how do they interact with the host language’s type system?
- gingerBill 27d agoI know that "untyped" means a single-type, but assembly operands have multiple different kinds of types (as I state in the article). What makes it really interesting is what you can know about each instruction and what it does (what operands it excepts, what it clobbers, what side-effects its has, etc). And from that huge table of type information, this can be used to give good error messages and suggestions to the user because the compiler actually knows all of this. The type constraints here allow for a lot more than information that normal assemblers just don't give.
- 27d ago
- magicalhippo 27d ago> But because of its time period, the built-in assembler only ever understood up to 80286 instructions, so the day you wanted a 386 and its 32-bit registers you were sent off to an external assembler anyway. Or you just prefixed the instructions with "db $66", et voila your instructions were 32bit. I wrote a lot of inline 32bit assembly that way in TP 6.0 and 7.0.
- adrian_b 25d agoTrue, but that still gave you access to only a subset of the 80386 instructions. For the others, you had to write them entirely in unreadable hexadecimal, adding a data-size prefix was not enough. By far the most useful were the 32-bit addressing modes. With your method, you could access those by adding just a "db $67" prefix, but then the addressing modes would have been greatly obfuscated by the 80286 notation, so that would not have been much better than writing the entire instruction in hexadecimal.
- amluto 27d agoI have very mixed opinions about the custom syntax. IMO the correct asm syntax, with very few exceptions, is the one in the manual. This is why Intel syntax is right and AT&T syntax is wrong: the ISA comes from Intel, the docs are from Intel and AMD, and those docs use Intel syntax. So I was kind of hoping that the custom syntax would at least result in a very, very strong checker, at least as good as Fil-C’s. Maybe with an escape hatch to say something like “I know it looks like I clobbered xyz, but I promise I really didn’t. Sadly, the CPUID example in the article apparently compiles, but IMO it shouldn’t have: CPUID takes two inputs, in EAX and ECX, and the example forgot to bind ECX as an input. One might argue that CPUID takes even more inputs if you’re on a VM and doing something special, but ECX is really quite unambiguous.
- CBLT 26d ago> the CPUID example in the article [...] forgot to bind ECX as an input. I'm not really familiar with this stuff, but the example uses what it calls a "pin" (which in their docs is a type of "binding") on ECX before calling CPUID.
- tialaramex 25d agoYou don't really need to be familiar with either "this stuff" or Odin to spot that this clearly takes a single parameter named "leaf" and that's the input, which goes in EAX. However CPUID may care about ECX as input and that's only used as an output in this uh, "template". Here's Rust implementing this same feature: https://doc.rust-lang.org/src/core/stdarch/crates/core_arch/src/x86/cpuid.rs.html#61-100 https://doc.rust-lang.org/src/core/stdarch/crates/core_arch/... Rust provides this for both x86-64 and the original 32-bit x86 and this is a function, not an Odin-style "template" but hopefully this helps show what you're supposed to do. [Edited to add the Rust example]
- tpmoney 25d agoI'm not sure the example template was supposed to be canonical, vs demonstrating multiple output destructuring. Certainly the actual instruction definition in the checker library seems to understand that there could be two possible inputs: https://github.com/odin-lang/Odin/blob/4247507dd5e31c9fd87166f53fd67b007ecca86c/core/rexcode/isa/x86/tablegen/instruction_table.odin https://github.com/odin-lang/Odin/blob/4247507dd5e31c9fd8716... But I'm also not entirely sure why the example should not have compiled. It seems to me that the idea here is to be able to define a typed set of something equivalent to a function that inlines some assembly, but nothing about that inherently requires that the number of input or output parameters to the template match the parameters in the underlying assembly calls. There's no reason (in my mind anyway) why this shouldn't be a perfectly valid template: // Returns the extended feature flags obtained by calling CPUID // with EAX=7 and ECX=1 cpu_extended_feature_flags :: asm() -> (a, b, c, d: u32) [ a = %eax, b = %ebx, c = %ecx, d = %edx, ] { mov %eax 0x7 mov %ecx 0x1 cpuid }
- childintime 27d agoI don't care much about the typed part, I care much more that this is a good take on what an assembler should be, far ahead of the GCC monstrosity, that serves just one purpose well: it screams "don't use me". This feature could make Odin the language of choice for some types of projects, for it seems to remove so much friction.
- deleted 25d ago[deleted]
- inkyoto 25d agoThe GCC assembly syntax is not a monstrosity, it was a necessity given how GCC represented the intermediate representation of the code. Historical GCC docs actually explain the rationale of the design pretty well. Moreover, since GCC was one of the very few C compilers that targeted a large number of very diverse ISA's at the time, they wanted to have a uniform way of injecting the assembly code across wildly varying ISA's.
- f13f1f1f1 25d agoSomething being a necessity doesn't mean it isn't a monstrosity
- fithisux 26d agoTrue. It takes some time to grasp but gingerbill is right.
- AshamedCaptain 26d ago> AT&T bakes the width into the mnemonic (movb, movw, movl, movq [...] Intel’s syntax is to prefix the memory operand with byte, word, dword, or qword, but Odin’s just uses the Odin type system directly. In GAS you can skip the width suffix from the mnemonic, and in most Intel assemblers you can skip the memory type operators like byte. They happily guess it from the operands. The problem is that on x86 (but also other ISAs, even if to a lower extent) the different operand sizes have a lot of side effects, which is why everyone just makes the operand size explicit, up to the point that apparently the author/LLM believes that it is mandatory to specify them. This kind of defeats the headline of the article... Tomorrow you need to pass a 128 bit int into two registers and your fancy syntax then also becomes a messy bunch of hacks. This is why everyone's inline assembly syntax looks like that, because they want to cover the weird cases (gcc's one is almost like an history book). You're normally using inline assembly for when you have some ridiculous corner case, if not, then what you ought to use is more akin to intrinsics... Also it forgets Watcom C, which does have a complete but messy syntax for inline assembly (which combines nicely with its ability to specify really weird calling conventions).
- winocm 26d agoOh man, #pragma aux.
- bananaboy 25d agoI love the Watcom C inline assembler. I use it frequently in my retro projects!
- WalterBright 26d agoZortech had a complete inline assembler in the 80's. It's now in the D compiler!
- 10000truths 25d ago> Tomorrow you need to pass a 128 bit int into two registers and your fancy syntax then also becomes a messy bunch of hacks. There are no 128-bit integer registers in x64 or arm64 or riscv64. There are operations that represent 128-bit scalar operands/results by storing the top and bottom halves in two 64-bit registers. From what I can gather, it would look something like this in Odin for x64: my_asm_mul :: asm(a: u64, b: u64) -> (c, d: u64) [ a -> d = %rax, c = %rdx, ] { mul b } my_mul :: proc(a: u64, b: u64) -> u128 { hi, lo := my_asm_mul(a, b) result := (u128(hi) << 64) | u128(lo) return result }
- Jblx2 26d agoCan you get an assemble-time or run-time type-error with assembly? Might be a fine article otherwise without the click-bait headline.
- measurablefunc 26d ago> However, every instruction has a set of valid forms. Each form dictates the kind of each operand (register, memory, immediate, label), the class of each register (general-purpose, vector, mask), the width of each operand, the range each immediate may take, and what the instruction clobbers (flags, memory, particular registers). In x86, a mulps wants a 128-bit vector register; a crc32 in one of its forms wants a 32-bit destination and an 8-bit memory source; div reads and writes rdx and rax whether ask to it do or not. The instructions have bit-width, arity/source/target requirements so technically there are types whereas an abstract virtual machine that only operates on some fixed set of integer registers is mostly untyped (modulo number of registers).
- IshKebab 26d agoEveryone is not wrong, they just don't mean the straw man that you are taking down. The fact that there are integer, float, vector registers etc. does not invalidate the point that people mean when they say "assembly is untyped".
- adrian_b 25d agoAssembly language itself is very strongly typed, because the types of the operands for any instruction are enforced in hardware by the CPU. However, most assemblers do not help in any way the programmer with this, because they do implicit conversions between any data types, for the values stored in memory or in registers, or used as immediate operands. This is only caused by a historical tradition. It would not be a problem to implement an assembler that strongly enforces the use of the right data types and which allows only a minimum of non-dangerous implicit data type conversions.
- IshKebab 25d agoI'm not sure what you mean. From the hardware's point of view data loaded from memory is just bytes. You can happily store a float to memory and read it back as an int. Hardware doesn't care and neither do assemblers. And there's no practical way you could write an assembler that would care.
- tialaramex 25d agoExactly, if I write a Rust function which is actually wrapping the Intel ADD integer addition on 64-bit registers but I give my function floating point types (f64) instead, the CPU merrily performs the integer addition even though that's "wrong" in some sense. I don't have Bill's brand new nightly Odin compiler with "assembly templates" but I don't really see any useful way it could "fix" this. The machine does not care what your values "mean" to you, that's a human idea and that's what types are for.
- IshKebab 25d agoActually that case might be caught, depending on the architecture. E.g. on RISC-V float and integer registers are separate and the compiler would fail if you tell it you want an integer register and you try to load that with a float. That's his "aha, they are typed!" gotcha, but it's really not what anyone was talking about. And anyway, even in that case it isn't guaranteed - RISC-V has an optional configuration where float and integer registers are the same.
- sxzygz 26d agoThis article is really about the inline assembly syntax developed for the author's programming language Odin (and definitely nothing about TALs, typed assembly languages). There are a lot of interesting ideas here. One of my criticisms, however, is simply pointing to how similar mainstream general purpose CPU architectures have become; they are all C machines. This radically simplifies the complexity on the compiler front where, it seems, the author is targeting amd64 and aarch64. Extending the compiler to rv64 will probably be straightforward. I don't know anything about Odin, or its compiler implementation, but I imagine the language adheres to a view of the machine that matches the C machine model. Imagine a more esoteric language, the compiler would probably need an intermediate language matching the C machine model and in which the inline assembly would have to have survive some idempotent lowering to the intermediate representation before being further lowered to the object code. These details are what I am really curious about and probably the most intellectually stimulating. The most interesting possibility is if the Odin compiler is itself written wholly in Odin. If this were the case, it would really show the power of the inline assembly syntax. As far as I am aware no optimizing compiler has really pushed this angle whilst targeting multiple instruction architectures. If I recall correctly, even the Plan9 C compiler moved some basic optimization to their genericized assembler, and I've not kept up with it as it's evolved into the current Go compiler. Very interesting work as I have often though about inline assembly syntax in a high-level language. Keep it up gingerbill.
- PythagoRascal 26d ago> The most interesting possibility is if the Odin compiler is itself written wholly in Odin. Currently, it is not (C++, mostly C style). As far as I can remember, Bill has previously said that a self-hosted version of the compiler might be a possibility, _after_ the 1.0 release and when the full spec of the language has been written.
- adrian_b 25d agoNo, modern CPUs are not at all C machines, they are about as far of C machines as one could imagine, because they now implement in hardware hundreds of instructions that were unheard of in a DEC PDP-11. The C language has only 2 kinds of integer data types, signed and unsigned, of various sizes. Moreover, the implicit conversions between them are erroneously defined and lead to data corruption, unless the programmer is extremely careful. Modern CPUs, like those implementing the Intel/AMD x86-64 ISA or the Arm Aarch64 ISA, have 8 different kinds of integer data types, all of various sizes. For all these different data types the CPUs have dedicated instructions that implement in hardware various operations with them. It is impossible to access in the right way from C all these data types. Only in C++ one can define custom data types and implement appropriate operations for them using inline assembly or separate assembly source files. Those 8 data types are signed integers where overflow causes an exception, signed integers where overflow causes saturation, non-negative integers where overflow causes an exception, non-negative integers where overflow causes saturation, integer residues a.k.a. modular integers, bit strings, binary polynomials and binary polynomial residues (i.e. elements of a Galois field). Unfortunately, most programming languages have not gone beyond the level of C, so they do not allow the efficient use of modern CPUs otherwise than by using inline assembly or compiler intrinsics. Thus there is a great mismatch between most high-level programming languages and modern CPUs, the opposite of what the poster above said. The mainstream CPUs have become very similar between themselves, but very different from the C machine model inherited by most modern programming languages.
- WalterBright 26d agoHere's how D does it for the x86_64: https://github.com/dlang/dmd/blob/master/druntime/src/core/internal/atomic.d#L175 https://github.com/dlang/dmd/blob/master/druntime/src/core/i... It's the statement form, uses Intel syntax, and the compiler keeps track of which registers are modified.
- mathisfun123 25d agois your fulltime job posting hn comments like "in D...", "this is how D ...", "for D..."?
- krapp 25d agoWalter's proud of his D and he likes to show it off. Don't make it weird.
- WalterBright 25d agoYes I am proud of it and like showing it off. It's also fun when other languages copy aspects of D.
- deleted 25d ago[deleted]
- gingerBill 25d agoWhy is a bad thing that another language design and compiler writer compares his language as a point of comparison? I really like it when he does because it allows me to see what he has done for D, and learn from it.
- mathisfun123 25d ago> Why is a bad thing that another language design and compiler writer compares his language as a point of comparison? "why is it a bad thing if you do X thing incessantly". in this specific case it's called shilling. if you've been on hn for longer than a week you'll notice he advertises D as if it's his fulltime job (which it probably is).
- taeric 26d agoThis is silly. Fun. But silly. Is like claiming that math on the numbers that everyone knows is actually typed. Ignoring that that is only true if you do the effort to also do your operations on the types.
- the-smug-one 26d agoGood article, but it's so LLM-y, wish it wasn't. Either Bill needs to stop slopping, or he needs to get an editor.
- appyn 25d agoThis syntax is far too simple and won't adequately capture semantics for some architectures, for example Hexagon with its packeted instructions or SHARC with its complex parallel instructions. One can already see how this syntax isn't up to the task by the decision to put x86 prefixes on a separate line. The author tries to justify it but this comes across as trying to excuse a poor design decision. Also the AI slop tone of this article is awfully grating. I nearly gave up reading it because the LLM editing artefacts were so jarring.
- tialaramex 25d agoDoes Odin feel like a language which would ever target SHARC ? SHARC is pretty weird, there's neither LLVM nor the GNU backends for SHARC. If you explained that you want to have something less crazy than ancient C they're going to say you want a Blackfin not SHARC because that's a more plausible target. SHARC's addressable memory comes in 32-bit uh, bytes.
- gingerBill 24d agoOdin is never going to support SHARC nor Hexagon, so it is literally not a problem. And I do not even seen why a universal syntax for such ISAs is impossible to support either at the syntax level. Hexagon's `.new`/`:sat`/`:<<1` stuff could be easily added into the universal syntax (with a better syntax), even if other ISAs do not support it. Same with SHARC's parallel-operation separators: you just pick a different syntax. Even now, the full `[base + indexscale + disp]` syntax is not semantically supported for RISCV64 because they do not support `indexscale` in their memory operands. Yes the prefix syntax is a quirk but if can tell me an alternative syntax that is context-free to solve this problem that is also not too stark nor dense too read, please do! I am open to new ideas, but it seems that even other assemblers like Plan9, Go, and D, all came to similar conclusion with `lock; xadd ...`. And the article was not LLM written.
- Razengan 25d agoWould there be any benefit from implementing types at the CPU level? Has it been tried? Like say adding an 8-bit type flag to each instruction, and keeping a table of which memory ranges have which type, then only allowing compatible instructions on that memory?
- ssrc 25d agoLisp machines come to mind, like the Symbolics 3600.
- lpribis 25d agoYes it's been done a bunch of times in the past, see https://en.wikipedia.org/wiki/Tagged_architecture https://en.wikipedia.org/wiki/Tagged_architecture. Probably most notably lisp machines which the sibling comment mentioned.
- benj111 25d ago>Assembly is usually considered the perfect example of such an “untyped” language. >However, every instruction has a set of valid forms. Each form dictates the kind of each operand (register, memory, immediate, label), the class of each register... So if I lea that means the type is pointer. If I add it's an int. If I print it's some kind of char. So it's about as typed as B. The untyped predecessor to c.... Will any errors get raised is you sign extend an unsigned int? Yes you can enforce types the processor doesn't care though, and if you want to treat assembly as distinct, I can't think of any assembly language that enforced types.
- jkhdigital 25d agoAssembly doesn’t have typed objects, it has typed instructions. It’s like checked exceptions in Java—you have to declare them in the type signature of the method, and the compiler enforces that they are either caught and handled, or also explicitly declared by the caller. The type declaration is all about possible side effects. It’s an effect in the type system, not a data type or behavior.
- benj111 25d agoYes. But that's like saying B is typed. If you give an untyped number to B's print function, it'll print the ASCII letter. That doesn't make B typed. And this is being generous. Types in typed languages aren't just about the data, it's about what you can do with that data. If a function requires a pointer, it needs to know that that arbitrary collection of 1s and 0s is a pointer. Typing is the mechanism to enforce that. All (?) functions on all(?) languages assume, if they don't outright know, something about the type, so are all languages typed? And in that case why is the distinction at all meaningful? You're talking about objects. I'm talking about integers, chars, pointers. A 32bit register could be handed to sign extend, it could be used as pointer, used as an interrupt number, printed as a letter. The processor doesn't care. Assemblers typically don't care. Different things you do with that number imply that you are using it as a type, but nothing cares if you use a pointer as a system call number and then print is out as a utf32 character.
- tialaramex 25d ago
- caspper69 25d agoI feel like this article means well, but assembly or machine level types are not the same. Sure, the assembler and cpu will execute the instruction with the given type, but the next instruction can use a different instruction with different types and no one will be any the wiser. So one operation’s uint64 is another operation’s int64. The type data in assembly doesn’t live with the data itself, nor are types for data stored anywhere. I get the point but I think it just misses the mark.
- PunchyHamster 25d agothat's not assembler tho - that's assembly like language translated into actual ISA machine code. And going with AT&T will just annoy people for no good reason (despise what article claims)
- gingerBill 24d agoHow is this not an assembler?—even with your description which matches an assembler to a tee. It genuinely is an assembler, and I am not sure how you are thinking otherwise. And where is the AT&T? Did you even look at the syntax or read the article? Is it just the use of `%rex` to prevent namespace collisions with parameters and constants which could hypothetically be named `rex` (and there could be good reasons they are named that too)? There are no other sigils in the grammar. The order of the operands is Intel-like. The memory operand syntax is Intel-like.
- pjmlp 25d agoI loved the PC way to inline Assembly, like Borland and Microsoft compilers[0], failing that better intrinsics or macro Assemblers. Never understood the gibberish from UNIX compilers that always forced me to look down what all the flags are about. At least Odin follows a similar approach. [0] - At least on some Amiga compilers, and D as well.
- jcranmer 25d agoOne of the problems with smart inline assembly syntax like this is that it turns out to be less helpful in a lot of practical inline assembly. If you look at the way, say, the Linux kernel uses inline assembly, it really just wants the inline assembly to pass directly to the assembler. There's a lot of assembler directives in the inline ASM to do stuff like define instructions the assembler doesn't know about yet, or do fancy stuff like build a runtime instruction-patching system. I have inline ASM in one of my projects that bounces around between 16-bit, 32-bit, and 64-bit instructions. Another issue is that larger blocks of code will use a myriad of approaches to save and restore registers, so you can't actually reliably rely on the instruction semantics to work out which registers are clobbered and which are preserved by a full block of assembly. So this syntax really only works for small bits of assembly, and these days, it's probably better to actually just use real compiler intrinsics for those uses (which is what most of the production compilers do).