Rendered at 22:42:04 GMT+0000 (Coordinated Universal Time) with Cloudflare Workers.
amluto 3 days ago [-]
I 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.
adrian_b 2 days ago [-]
Following the vendor syntax may be useful if you only ever program in assembly for only a single target ISA.
Otherwise, all the vendor syntaxes are different and all of them have many ugly quirks, for various historical reasons.
If you ever have to write assembly for at least 2 ISAs, e.g. x86-64 and Aarch64, then it is much more productive to use a unified, and better syntax, like the one described in TFA.
Nothing makes more likely the appearance of bugs than having to alternate frequently between two or more slightly different syntaxes.
Even with only one target ISA, if you frequently intersperse inline assembly within a high-level language source code, it is better to have harmonized syntaxes, as explained in TFA.
pjmlp 1 days ago [-]
And that is how you end up with Plan 9 Assembly, that requires mapping to every single ISA.
CBLT 2 days 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 2 days ago [-]
You 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".
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 1 days ago [-]
I'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:
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
}
tialaramex 1 days ago [-]
> But I'm also not entirely sure why the example should not have compiled
The source you presented seems fine - it's explicitly setting the register. The trouble with the cpuid definition in the article is that it just doesn't set ECX at all
It does actually seem as though Odin is intended to notice this problem but maybe is fooled that the register is pinned (because we want its result value) and so the diagnostic doesn't trigger.
Reading this code reminded me that my annual summer leave ends this weekend because it would get so much review feedback if he worked with me. "Commenting out" blocks of code is NOT OK and neither are "if (false)" blocks.
tpmoney 19 hours ago [-]
Sure, I agree that if you're making a CPUID inline assembly template that isn't setting the register value, and it accepts any values for EAX that would also cause it to read ECX then the behavior is going to be undefined and likely unexpected, but that doesn't seem to be a reason for this not to compile.
For one, requiring an input parameter when it isn't mandatory would mean that you're spending cycles setting a register with a value that you don't need and is just going to be overwritten anyway. A good number of the CPUID calls never read from ECX, and if you're going to call any of them, then the value in ECX is irrelevant. And sure, it's only an extra instruction or two, but presumably if you're dropping down to inline assembly, you kind of care about every wasted instruction.
// Returns the maximum input value for basic CPUID information
cpu_extended_feature_flags :: asm() -> (a, b, c, d: u32) [
a = %eax,
b = %ebx,
c = %ecx,
d = %edx,
] {
mov %eax 0x0
cpuid
}
I admit I never touch inline assembly or really assembly at all save for the occasional microcontroller project, so maybe I'm missing something that's obvious to people more familiar with this. But to me the article seems to be sayin that the Odin templates will type check and validate that IF you have inputs from Odin types that are being put into registers or used as operands to your assembly, or are mapping registers and outputs from your assembly back to Odin types, that those mappings will be type compatible, and when you get those mappings wrong, you'll get a more useful error. But I didn't read it as saying that it will prevent you from writing assembly that does something completely unrelated to those inputs or outputs.
tialaramex 11 hours ago [-]
For CPUID specifically this isn't going to be on a fast path. As its name might suggest it's a way to have the runtime CPU tell us what features are available so that we can adopt different implementations of perf-sensitive routines for that CPU.
So e.g. maybe the CAD software executes a dozen CPUID instructions during startup, and then based on those it uses AVX512 vectorized operations later on some hardware but uses SSE instead on other hardware.
I haven't read Intel's manual for a modern x86-64 CPU, and I certainly haven't read the community notes about this stuff which would tell you if, despite the documentation you need to behave differently, but my assumption would be that everybody clears ECX if they don't want a non-zero subleaf and that this is either known to be necessary or an obvious way to avoid nasty surprises in code that is never perf critical.
tpmoney 6 hours ago [-]
I think we might be talking past each other here. Whether the assembly op is `cpuid` or something else, isn’t really my driving concern. The comment that started this thread (which I’m pretty sure wasn’t yours, so I don’t necessarily expect you to have an answer) was that they didn’t think this example should have compiled because `cpuid` may read from a second register as part of its operations, depending on the value passed to the first register.
But that seems to be saying that the inline assembly template should have the same shape as the underlying assembly calls that are made. If the underlying call might read 3 registers, then the complaint appears to be that the templating system MUST require a template that calls that assembly to also have 3 inputs, even if it won’t use all 3.
My thinking on this is the templating is a syntax for function declarations where the function is assembly code and not more higher level language code. We don’t require that functions have the same number of inputs and outputs that the code called within the function has in order for it to compile, I don’t see why it would be necessary for the assembly templates to work any differently. I get that `cpuid` specifically isn’t likely to run in the hot path of any code, but the underlying principle is the same. If you’re dropping to assembly, you likely have some performance tuning you’re trying to do. A templating system that requires your template inputs to 1:1 map to all the possible inputs of all the assembly calls you use in the template, regardless of wether you need or use them is adding extra instructions and waste for (to me) no obvious benefits.
tialaramex 3 hours ago [-]
> A templating system that requires your template inputs to 1:1 map to all the possible inputs of all the assembly calls you use in the template, regardless of wether you need or use them is adding extra instructions and waste for (to me) no obvious benefits.
But we're not suggesting this ridiculous and arbitrary restriction. What we're suggesting, and in fact I think what Bill actually intended in Odin, is something much more useful.
A templating system that requires all possible inputs of all the assembly calls you use in the template are well defined.
What you seem to want is exactly the thing Bill doesn't like about existing functionality. You can cheerfully ADD two registers together and then use the result but without ever determining what's in those registers. What is the result? -shrug- might be anything.
What Odin seems to intend (but this CPUID example seems to suggest is buggy) is that it will check you've actually written code which means something. You do not need to make those registers inputs of your template, you just need to make sure they're well defined. For example you could set them directly in the template, as with your leaf 7 sub-leaf 1 example.
That (modulo bugs) is a big improvement for this particular corner of the language.
tpmoney 2 hours ago [-]
I'm starting to get a better handle on what you're getting at, but I still think I disagree that the code shouldn't compile. To really settle this I guess we would need Bill to weigh in on his intent, because I read this article about being all about types and type checking. He says:
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).
...
That is not the absence of a type system: that is a type system; a rather
rich, dependent, per-instruction one.
...
The params are your inputs, as plain names with Odin types. The results are
your outputs, again plain names with types, sharing the same signature
syntax as an Odin procedure.
...
The params and results are optional, like with a normal procedure type, and
the bindings are completely optional if they are not necessary.
That last part in particular is important here, bindings are optional if they're not necessary. Well if the "types" for any given ASM instruction come from their valid definitions, then we'd have to look at the intel instruction definitions[1].
Some instructions like ADD[2] (Vol 2A 3-14) are defined to have multiple forms and those forms each take 2 operands. So the type of the instruction would be `ADD T-OP1, T-OP2` for each of the possible combinations of operand types for the ADD forms. And the templating system and compiler would validate that if you pass a given parameter of a given type to one of those operands, that your types match up.
Some like ANDPD (Vol 2A 3-63) have two forms, one that takes 2 operands and one that takes 3. The 2 operand one says that the first operand is a read/write register, where as the 3 operand forms say the first operand is write only. Presumably if you took an input and bound it to the first operand in a 3 operand form, the compiler would at least emit a warning about this if not an error.
But now if we look at the definition of CPUID (Vol 2A 3-203) the only valid form it has is a form with no operands. So if we were defining a type for the type system to compare against, the only possible correct answer (to me) is `cpuid` with no operands. That is the type system should happily allow inline assembly that calls `cpuid` but not inline assembly that calls `cpuid %eax %ecx` because that's not a valid form of the instruction.
Further the definition explicitly says that for some values of EAX, the value in ECX would be ignored entirely, and for invalid values in either field, the output values are all "Reserved". That tells me that as far as a type checker is concerned, any invocation of `cpuid` as long as there are no operands is a valid invocation. A type checker doesn't check the values of the fields being used, and IMO any argument that since it could read ECX, then the type system should enforce you set a value it to it can be equally countered by an argument that since cpuid will explicitly ignore ECX with certain values, it should never require you to set ECX because that might cause you to set it with a value and get an unexpected result because the value you provided to EAX was one of the values that causes it to be ignored. In either case we're asking the type checker to help us prevent a logic bug, not a type bug.
You might say that the "always require both fields to be set" is at least an easy check that could be applied universally, then the question would become how would it intersect with multiple invocations of CPUID? If you invoke:
mov eax, 0x0
mov ecx, 0x0
cpuid
cpuid
that is valid and the outcome of that should be (assuming I'm reading the documentation correctly) that after the first invocation, EAX would contain the maximum valid value for EAX when invoking cpuid, and after the second invocation it should contain the results of invoking cpuid with whatever that value was (and obviously whatever was in ECX after the first invocation). If the purpose of the check is to prevent invocations where ECX might contain an arbitrary value that makes no sense, we'd have to mandate that there's some additional steps in between the two cpuid invocations to reset EAX and ECX. Otherwise all we've done is make it possibly even more confusing when ECX changes after the first invocation, but the code strongly implies it should be 0x0.
Given the stated goals in the article seem to include not requiring the explicit statement of implicit behavior, requiring setting ECX for instruction that will ignore it would seem counter to the goals.
As a side note, I'm curious why (based on your comment about ADD), it would be desirable for a type checker to want to prevent you from doing something like this:
It's perfectly valid to not always care about the starting value in a register even if you're going to use it. Plenty of pseudo-rng type code has read arbitrary registers or addresses as a source for some of their calculation without ever caring what the starting value was. I feel like in some way this gets to the heart of what we're disagreeing about. I read the article as saying "within the bounds of the shape of assembly code as defined by the ISA, odin templates can help enforce types for those shapes and help make wiring normal types to registers for input and output easy. I feel like what you're saying that in addition to that, it's also supposed to help stop you from doing things that are likely to give you non-sensical results.
1 days ago [-]
inkyoto 1 days ago [-]
The correct assembly syntax is a matter of convention and personal preference – not a category error.
Historically, some ISA's have adopted the «src, dst» convention, whilst others have preferred «dst, src». We should be grateful that no engineer, in a moment of excessive creativity, attempted boustrophedon – a conceptual device solely appropriate to the likes of INTERCAL. Then we have 3 operand RISC instructions.
As with so many technical orthodoxies, the first convention one encounters tends thereafter to acquire the status of natural law.
The consistent application of the same convention is useful nevertheless. It reduces the unnecessary cognitive overload once one starts jumping across multiple hardware architectures.
applfanboysbgon 2 days ago [-]
The syntax in the manual is embarrassingly outdated. Like, it's actually a disgrace and shameful for our profession that assembly languages and tooling are stuck in the previous century. There is absolutely zero logical reason we should be constrained to such primitiveness.
It's the statement form, uses Intel syntax, and the compiler keeps track of which registers are modified.
gingerBill 1 days ago [-]
D's approach is great, but it has a few limitations for my use case in Odin. It only supports x86_64/amd64 and uses Intel-style syntax, whereas I needed a solution that universalizes its syntax across multiple ISAs.
D's inline asm is also statement-based rather than a callable template. Though the mixin trick fixes this, it does mean it still uses %0-style parameters making it hard to read and write, something I want to remove completely.
It is great to see that we arrived at similar design compromises, especially regarding `lock` being treated as a separate instruction and thus separated with a `;` (which is automatically inserted by the Odin compiler).
WalterBright 1 days ago [-]
> only supports x86_64/amd64
It supports x86, too, and we're working on Arm64.
> uses Intel-style syntax
Yes, because the instruction set references are in Intel syntax. The backwards gcc asm causes me seizures, like trying to write cursive with my left hand. The asm for Arm64 will also follow Arm's instruction specification.
> D's inline asm is also statement-based
That's so the source code can be tokenized and parsed without needing special behavior inside the asm { ... }.
> it still uses %0-style parameters
Not sure what you mean. RAX means register RAX. %RAX is not accepted.
> especially regarding `lock` being treated as a separate instruction
That just makes it easier to parse!
Anyhow, thank you for the kind words! I am proud of it, the only troubles I have is when Intel adds wacky new instructions that just don't fit in the instruction encoding tables.
gingerBill 12 hours ago [-]
> It supports x86, too
Well I assumed so because amd64 is a superset of x86. But nice to know you're working on arm64 too.
Regarding Intel-syntax, I think there is a little miscommunication here since I try to explain what I mean in the article. Intel-ordering is a good idea, but using nothing but the Intel-syntax wholesale is not universal enough, and needs modifying, especially for AMD64 and other ISAs. Odin's is Intel-like too, but fully Intel by design.
> That's so the source code can be tokenized and parsed without needing special behavior inside the asm { ... }.
This is why Odin's asm templates have their own universalized syntax. Thus the entire article.
> Not sure what you mean. RAX means register RAX. %RAX is not accepted.
It's why I referred to your "trick", which is something I wanted to need in the first place.
> That just makes it easier to parse!
For Odin's asm template syntax, it's not about being easier to parser, it's about having a context free grammar that is the same across ISAs. If I was to allow for prefixes directly in the grammar, either prefixes would have to have their special syntax or you'd need to have a context-sensitive grammar.
WalterBright 5 hours ago [-]
Oh I see what you mean. The "trick" has nothing to do with the inline assembler - it's a way the user can manipulate strings and then feed the result to the parser. String mixins are a very popular feature of D.
A little secret - the D parser does not actually parse the asm syntax. It just snarfs up tokens until it sees the `;`. The semantic phase of the compiler then applies a grammar over it, which is not the D grammar, but the Intel grammar. This enables it to apply custom grammars to each supported instruction set.
mathisfun123 1 days ago [-]
is your fulltime job posting hn comments like "in D...", "this is how D ...", "for D..."?
gingerBill 1 days ago [-]
Why 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 1 days 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).
WalterBright 1 days ago [-]
Google defines "shill" as: "A shill is a person who is paid or secretly works with a trickster, gambler, or salesperson to trick others into buying something or joining a game by pretending to be a normal, happy customer"
I am not paid, nor work secretly (I use my real name), and am not tricking anyone.
WalterBright 1 days ago [-]
The article itself compares its assembler to others, so comparing it to D's is quite on topic.
WalterBright 1 days ago [-]
Why not? Whenever there is an article about D on HN, there are plenty of comments comparing it to other languages, sometimes over half.
bellowsgulch 1 days ago [-]
Honestly, I wish other language developers would chime in, too. It's insightful.
krapp 1 days ago [-]
Walter's proud of his D and he likes to show it off. Don't make it weird.
WalterBright 1 days ago [-]
Yes I am proud of it and like showing it off. It's also fun when other languages copy aspects of D.
19 hours ago [-]
AshamedCaptain 2 days 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).
WalterBright 2 days ago [-]
Zortech had a complete inline assembler in the 80's. It's now in the D compiler!
10000truths 2 days 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
}
gingerBill 1 days ago [-]
> the different operand sizes have a lot of side effects
Which we have massive tables for each form which track those side effects and clobbering information too.
> author/LLM
I am the author, and not an LLM.
> Tomorrow you need to pass a 128 bit int into two registers
Okay? There are no 128-bit integer registers on AMD64, ARM64, nor RISCV-64. So I have no idea what you are on about. And note they are templates, so if you want 128-bit integer support, you can just wrap that template in a procedure and handle the behaviour yourself.
camel-cdr 1 days ago [-]
I really like what you are doing here, the state of inline assembly is a similar travesty to the state of guided codegen/autovec.
On concern I have is how this maps to ARM64 syntax, because ARM64 is massively overloading all mnemonics.
Have extremely different performance characteristics, yet would map to the same code:
ld1d dst, p0/z, [base + idx<<3]
Imo this makes reading the assembly quite bothersome. I'm already not a fan of ARM64 doing the mnemonic overloading, but at least you can figure out the operation by looking at the same line further to the right.
Also, maybe I missed it, but how are you dealing with things like the /z modifier, pre/post-increment load/store and load pair? Or things like TBL/ST4/LD4?
Oh and how are the types going to work for RVV, where the type can't be determined at compile-time in all situations?
gingerBill 10 hours ago [-]
I haven't fully thought out that syntax yet, but it's a problem with AVX-512 in terms of its predicate operands too.
So the parameter is marked as a predicate with zeroing or whatever, and then `pred` is just a normal operand as the binding section specifies everything.
This is not current behaviour yet but it I am considering it when I need to specify this for even AVX-512 and RISC-V behaviour (which has multiple different possibilities).
bananaboy 2 days ago [-]
I love the Watcom C inline assembler. I use it frequently in my retro projects!
winocm 2 days ago [-]
Oh man, #pragma aux.
jcranmer 1 days ago [-]
One 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).
genxy 3 days ago [-]
An avenuge of research worth being sniped on is Typed Assembly Language
TALs 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 3 days ago [-]
The 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 3 days ago [-]
But 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 3 days ago [-]
I 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.
jkhdigital 1 days ago [-]
In other words, the type of an assembly instruction specifies its effects and coeffects. This is an active research area—describing effects and coeffects in the type system, and discharging handler/provider obligations at the compiler level.
I suspect the main reason someone might quibble over the “assembly is typed” assertion is that many programmers have a rather narrow view of type systems, heavily skewed by OOP patterns.
gingerBill 1 days ago [-]
That's pretty much the quibble. Most people's narrow view of type system.
And we already track all of the basic side-effects and clobbering that each form of each mnemonic does. That's kind of the entire point of this being possible: it's all "typed".
questionableans 3 days ago [-]
Yes, and your second paragraph above is the interesting part that I would want the reader to focus on, starting from the title.
woadwarrior01 1 days ago [-]
OT: The 2nd link is the first time I've seen a Microsoft FrontPage website in decades. :)
sxzygz 2 days ago [-]
This 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.
adrian_b 2 days ago [-]
No, 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.
minipci1321 1 days ago [-]
> so they do not allow the efficient use of modern CPUs otherwise than by using inline assembly or compiler intrinsics.
When one provides the full effect of the operation in the source code, a properly ported compiler backend should be able to spot the pattern and emit the instructions with matching non-C semantics. (Typically, saturated operations are a very low hanging fruit.)
That doesn't always work because the optimization passes targeting local optimum break these patterns while "optimizing" them, so they arrive to the instruction emitter unrecognizable. And these passes, living in the generic "good-for-all" area of the compiler core, cannot be made aware of what a particular target does or doesn't support. So ironically, such instructions appear more when the optimizations are disabled.
Intrinsics emit internal representation forms that optimization passes don't dare to touch.
adrian_b 24 hours ago [-]
I agree.
guenthert 2 days ago [-]
Most architectures perform best in combination with a compiler of a statically typed language. SPARC at least still had rudimentary support for tagged data types.
PythagoRascal 2 days 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.
the-smug-one 2 days ago [-]
Good article, but it's so LLM-y, wish it wasn't. Either Bill needs to stop slopping, or he needs to get an editor.
magicalhippo 3 days 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 2 days ago [-]
True, 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.
childintime 3 days ago [-]
I 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.
2 days ago [-]
inkyoto 1 days ago [-]
The 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 1 days ago [-]
Something being a necessity doesn't mean it isn't a monstrosity
appyn 2 days ago [-]
This 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.
gingerBill 11 hours ago [-]
Odin 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.
tialaramex 1 days ago [-]
Does 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.
Krssst 3 days ago [-]
Sorry, 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.
Which is already infinitely more readable and requires no parochial sigils nor the arcane clobbering syntax.
layer8 1 days ago [-]
The 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.
Jblx2 2 days ago [-]
Can you get an assemble-time or run-time type-error with assembly? Might be a fine article otherwise without the click-bait headline.
measurablefunc 2 days 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).
caspper69 1 days ago [-]
I 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.
benj111 1 days 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 1 days ago [-]
Assembly 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 1 days ago [-]
Yes. 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 1 days ago [-]
Generously I assume Bill is thinking of "register classes" as types, so it doesn't care that you're using LEA on an integer, just that you used one of the registers for which LEA is available and not say XMM0
Ultimately the proof is in the pudding. If I screw up some inline assembly in Rust the diagnostics aren't very good because Rust doesn't deeply understand the assembly, whereas obviously for other things they're excellent. If Odin's diagnostics are great because it actually understands these "templates" that's a meaningful benefit to programmers.
pjmlp 1 days ago [-]
I 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.
taeric 2 days ago [-]
This 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.
IshKebab 2 days ago [-]
Everyone 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 2 days ago [-]
Assembly 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.
t-3 1 days ago [-]
If you consider each possible encoding as a different instruction, CPU instructions don't even have operands, the register is part of the instruction. The CPU doesn't care what is in the register because only bits can be in the register and all instructions operate on bits.
IshKebab 1 days ago [-]
I'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 1 days ago [-]
Exactly, 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 1 days ago [-]
Actually 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.
tialaramex 1 days ago [-]
I was aware that some architectures had distinct floating point registers but I didn't know RISC-V is / could be one of them. As RISC-V becomes more popular perhaps I should consider choosing a different example for where typing evaporates to produce zero machine code during compilation, today I'd cite f32::to_bits which takes a 32-bit floating point value and gives us a 32-bit unsigned integer but maybe I should use u32::cast_signed since having distinct registers for signed versus unsigned integers is surely rare.
camel-cdr 1 days ago [-]
> I was aware that some architectures had distinct floating point registers
Most ISAs do.
On x86 and arm scalar and FP registers are separate, it's just that they overlap FP and SIMD registers.
On RISC-V there are three separate register files for scalar, FP and SIMD. Although you can overlap scalar and FP in some minimal embedded configurations.
tialaramex 1 days ago [-]
D'oh. I'm old enough to have written software which needs to care about the i387 FPU and yet here I am acting as though the floating point instructions use the ordinary scalar registers. Worse, I have a window open where I'm writing toy software that doesn't compile because it is asking to do an FPU operation on a GPR and rather than fix that, which would remind me that this can't work, I decided to alt-tab to HN and make the exact same mistake.
adrian_b 23 hours ago [-]
I mean that every instruction has operands that have well defined data types, exactly like any function of a high-level programming language.
Most assemblers do not allow the programmer to declare the data types of the variables, so they do not check whether the types of the operands are valid.
Nonetheless, nothing would stop someone to modify an assembler to require the declaration of the data types and to generate assembly errors whenever there is a type mismatch.
The fact that if you give to an instruction that expects a floating-point number an integer operand then the CPU will compute a bogus value, is irrelevant.
The same will happen if you give operands of the wrong type to any function of a program written in a high-level language.
Any decent modern compiler will prevent you to use the wrong types, but if you use some tricks, you can still avoid this and invoke a function with wrong argument types, in which case it will compute some gibberish.
So the parameters of HLL functions or procedures have well defined data types and the same is true for the operands of CPU instructions.
Enforcing the use of the correct types can be done only at compile-time/assembly time, by the compiler or assembler.
This happens because for efficiency reasons the primitive data types do not contain a tag for identifying their type. Only the programmer-defined union types contain a type tag, so that their type can be identified at run-time. In the languages without static type checking, but with dynamic type checking, any value belongs actually to a kind of tagged union type that includes all the types that can be used in that language, so the type of any argument can be determined at run time, but this is what makes such languages slow.
In conclusion, there is no real difference between an assembly language and a HLL with static type checking, except that most people who have written assemblers were lazy and they did not implement type checking.
The first C compilers never checked the types of the function arguments. Today this would no longer be an acceptable for a C compiler. For the same reasons, it should be no longer acceptable for any assembler to not check the data types.
It is likely that the fact that no well-known assembler does an appropriate type checking is due to the necessity of defining a much more ample type system for an assembly language than for most high-level languages, so this would be a lot of work. Because the assembly programmers were used to lax assemblers anyway, implementing such a feature was not considered as a priority.
fithisux 3 days ago [-]
True. It takes some time to grasp but gingerbill is right.
Razengan 2 days ago [-]
Would 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?
Lisp machines come to mind, like the Symbolics 3600.
PunchyHamster 1 days ago [-]
that'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 11 hours ago [-]
How 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.
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.
Otherwise, all the vendor syntaxes are different and all of them have many ugly quirks, for various historical reasons.
If you ever have to write assembly for at least 2 ISAs, e.g. x86-64 and Aarch64, then it is much more productive to use a unified, and better syntax, like the one described in TFA.
Nothing makes more likely the appearance of bugs than having to alternate frequently between two or more slightly different syntaxes.
Even with only one target ISA, if you frequently intersperse inline assembly within a high-level language source code, it is better to have harmonized syntaxes, as explained in TFA.
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.
Here's Rust implementing this same feature:
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]
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:
The source you presented seems fine - it's explicitly setting the register. The trouble with the cpuid definition in the article is that it just doesn't set ECX at all
It does actually seem as though Odin is intended to notice this problem but maybe is fooled that the register is pinned (because we want its result value) and so the diagnostic doesn't trigger.
Reading this code reminded me that my annual summer leave ends this weekend because it would get so much review feedback if he worked with me. "Commenting out" blocks of code is NOT OK and neither are "if (false)" blocks.
For one, requiring an input parameter when it isn't mandatory would mean that you're spending cycles setting a register with a value that you don't need and is just going to be overwritten anyway. A good number of the CPUID calls never read from ECX, and if you're going to call any of them, then the value in ECX is irrelevant. And sure, it's only an extra instruction or two, but presumably if you're dropping down to inline assembly, you kind of care about every wasted instruction.
Again, this seems to me like it should be a perfectly valid assembly template (based on https://www.felixcloutier.com/x86/cpuid):
I admit I never touch inline assembly or really assembly at all save for the occasional microcontroller project, so maybe I'm missing something that's obvious to people more familiar with this. But to me the article seems to be sayin that the Odin templates will type check and validate that IF you have inputs from Odin types that are being put into registers or used as operands to your assembly, or are mapping registers and outputs from your assembly back to Odin types, that those mappings will be type compatible, and when you get those mappings wrong, you'll get a more useful error. But I didn't read it as saying that it will prevent you from writing assembly that does something completely unrelated to those inputs or outputs.So e.g. maybe the CAD software executes a dozen CPUID instructions during startup, and then based on those it uses AVX512 vectorized operations later on some hardware but uses SSE instead on other hardware.
I haven't read Intel's manual for a modern x86-64 CPU, and I certainly haven't read the community notes about this stuff which would tell you if, despite the documentation you need to behave differently, but my assumption would be that everybody clears ECX if they don't want a non-zero subleaf and that this is either known to be necessary or an obvious way to avoid nasty surprises in code that is never perf critical.
But that seems to be saying that the inline assembly template should have the same shape as the underlying assembly calls that are made. If the underlying call might read 3 registers, then the complaint appears to be that the templating system MUST require a template that calls that assembly to also have 3 inputs, even if it won’t use all 3.
My thinking on this is the templating is a syntax for function declarations where the function is assembly code and not more higher level language code. We don’t require that functions have the same number of inputs and outputs that the code called within the function has in order for it to compile, I don’t see why it would be necessary for the assembly templates to work any differently. I get that `cpuid` specifically isn’t likely to run in the hot path of any code, but the underlying principle is the same. If you’re dropping to assembly, you likely have some performance tuning you’re trying to do. A templating system that requires your template inputs to 1:1 map to all the possible inputs of all the assembly calls you use in the template, regardless of wether you need or use them is adding extra instructions and waste for (to me) no obvious benefits.
But we're not suggesting this ridiculous and arbitrary restriction. What we're suggesting, and in fact I think what Bill actually intended in Odin, is something much more useful.
A templating system that requires all possible inputs of all the assembly calls you use in the template are well defined.
What you seem to want is exactly the thing Bill doesn't like about existing functionality. You can cheerfully ADD two registers together and then use the result but without ever determining what's in those registers. What is the result? -shrug- might be anything.
What Odin seems to intend (but this CPUID example seems to suggest is buggy) is that it will check you've actually written code which means something. You do not need to make those registers inputs of your template, you just need to make sure they're well defined. For example you could set them directly in the template, as with your leaf 7 sub-leaf 1 example.
That (modulo bugs) is a big improvement for this particular corner of the language.
Some instructions like ADD[2] (Vol 2A 3-14) are defined to have multiple forms and those forms each take 2 operands. So the type of the instruction would be `ADD T-OP1, T-OP2` for each of the possible combinations of operand types for the ADD forms. And the templating system and compiler would validate that if you pass a given parameter of a given type to one of those operands, that your types match up.
Some like ANDPD (Vol 2A 3-63) have two forms, one that takes 2 operands and one that takes 3. The 2 operand one says that the first operand is a read/write register, where as the 3 operand forms say the first operand is write only. Presumably if you took an input and bound it to the first operand in a 3 operand form, the compiler would at least emit a warning about this if not an error.
But now if we look at the definition of CPUID (Vol 2A 3-203) the only valid form it has is a form with no operands. So if we were defining a type for the type system to compare against, the only possible correct answer (to me) is `cpuid` with no operands. That is the type system should happily allow inline assembly that calls `cpuid` but not inline assembly that calls `cpuid %eax %ecx` because that's not a valid form of the instruction.
Further the definition explicitly says that for some values of EAX, the value in ECX would be ignored entirely, and for invalid values in either field, the output values are all "Reserved". That tells me that as far as a type checker is concerned, any invocation of `cpuid` as long as there are no operands is a valid invocation. A type checker doesn't check the values of the fields being used, and IMO any argument that since it could read ECX, then the type system should enforce you set a value it to it can be equally countered by an argument that since cpuid will explicitly ignore ECX with certain values, it should never require you to set ECX because that might cause you to set it with a value and get an unexpected result because the value you provided to EAX was one of the values that causes it to be ignored. In either case we're asking the type checker to help us prevent a logic bug, not a type bug.
You might say that the "always require both fields to be set" is at least an easy check that could be applied universally, then the question would become how would it intersect with multiple invocations of CPUID? If you invoke:
that is valid and the outcome of that should be (assuming I'm reading the documentation correctly) that after the first invocation, EAX would contain the maximum valid value for EAX when invoking cpuid, and after the second invocation it should contain the results of invoking cpuid with whatever that value was (and obviously whatever was in ECX after the first invocation). If the purpose of the check is to prevent invocations where ECX might contain an arbitrary value that makes no sense, we'd have to mandate that there's some additional steps in between the two cpuid invocations to reset EAX and ECX. Otherwise all we've done is make it possibly even more confusing when ECX changes after the first invocation, but the code strongly implies it should be 0x0.Given the stated goals in the article seem to include not requiring the explicit statement of implicit behavior, requiring setting ECX for instruction that will ignore it would seem counter to the goals.
[1]: https://www.intel.com/content/www/us/en/developer/articles/t...
[2]:
As a side note, I'm curious why (based on your comment about ADD), it would be desirable for a type checker to want to prevent you from doing something like this:
It's perfectly valid to not always care about the starting value in a register even if you're going to use it. Plenty of pseudo-rng type code has read arbitrary registers or addresses as a source for some of their calculation without ever caring what the starting value was. I feel like in some way this gets to the heart of what we're disagreeing about. I read the article as saying "within the bounds of the shape of assembly code as defined by the ISA, odin templates can help enforce types for those shapes and help make wiring normal types to registers for input and output easy. I feel like what you're saying that in addition to that, it's also supposed to help stop you from doing things that are likely to give you non-sensical results.Historically, some ISA's have adopted the «src, dst» convention, whilst others have preferred «dst, src». We should be grateful that no engineer, in a moment of excessive creativity, attempted boustrophedon – a conceptual device solely appropriate to the likes of INTERCAL. Then we have 3 operand RISC instructions.
As with so many technical orthodoxies, the first convention one encounters tends thereafter to acquire the status of natural law.
The consistent application of the same convention is useful nevertheless. It reduces the unnecessary cognitive overload once one starts jumping across multiple hardware architectures.
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.
D's inline asm is also statement-based rather than a callable template. Though the mixin trick fixes this, it does mean it still uses %0-style parameters making it hard to read and write, something I want to remove completely.
It is great to see that we arrived at similar design compromises, especially regarding `lock` being treated as a separate instruction and thus separated with a `;` (which is automatically inserted by the Odin compiler).
It supports x86, too, and we're working on Arm64.
> uses Intel-style syntax
Yes, because the instruction set references are in Intel syntax. The backwards gcc asm causes me seizures, like trying to write cursive with my left hand. The asm for Arm64 will also follow Arm's instruction specification.
> D's inline asm is also statement-based
That's so the source code can be tokenized and parsed without needing special behavior inside the asm { ... }.
> it still uses %0-style parameters
Not sure what you mean. RAX means register RAX. %RAX is not accepted.
> especially regarding `lock` being treated as a separate instruction
That just makes it easier to parse!
Anyhow, thank you for the kind words! I am proud of it, the only troubles I have is when Intel adds wacky new instructions that just don't fit in the instruction encoding tables.
Well I assumed so because amd64 is a superset of x86. But nice to know you're working on arm64 too.
Regarding Intel-syntax, I think there is a little miscommunication here since I try to explain what I mean in the article. Intel-ordering is a good idea, but using nothing but the Intel-syntax wholesale is not universal enough, and needs modifying, especially for AMD64 and other ISAs. Odin's is Intel-like too, but fully Intel by design.
> That's so the source code can be tokenized and parsed without needing special behavior inside the asm { ... }.
This is why Odin's asm templates have their own universalized syntax. Thus the entire article.
> Not sure what you mean. RAX means register RAX. %RAX is not accepted.
This: https://github.com/dlang/dmd/blob/master/druntime/src/core/i...
It's why I referred to your "trick", which is something I wanted to need in the first place.
> That just makes it easier to parse!
For Odin's asm template syntax, it's not about being easier to parser, it's about having a context free grammar that is the same across ISAs. If I was to allow for prefixes directly in the grammar, either prefixes would have to have their special syntax or you'd need to have a context-sensitive grammar.
A little secret - the D parser does not actually parse the asm syntax. It just snarfs up tokens until it sees the `;`. The semantic phase of the compiler then applies a grammar over it, which is not the D grammar, but the Intel grammar. This enables it to apply custom grammars to each supported instruction set.
I really like it when he does because it allows me to see what he has done for D, and learn from it.
"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).
I am not paid, nor work secretly (I use my real name), and am not tricking anyone.
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).
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:
Which we have massive tables for each form which track those side effects and clobbering information too.
> author/LLM
I am the author, and not an LLM.
> Tomorrow you need to pass a 128 bit int into two registers
Okay? There are no 128-bit integer registers on AMD64, ARM64, nor RISCV-64. So I have no idea what you are on about. And note they are templates, so if you want 128-bit integer support, you can just wrap that template in a procedure and handle the behaviour yourself.
On concern I have is how this maps to ARM64 syntax, because ARM64 is massively overloading all mnemonics.
For example:
Have extremely different performance characteristics, yet would map to the same code: Imo this makes reading the assembly quite bothersome. I'm already not a fan of ARM64 doing the mnemonic overloading, but at least you can figure out the operation by looking at the same line further to the right.Also, maybe I missed it, but how are you dealing with things like the /z modifier, pre/post-increment load/store and load pair? Or things like TBL/ST4/LD4?
Oh and how are the types going to work for RVV, where the type can't be determined at compile-time in all situations?
My hunch would be the following:
So the parameter is marked as a predicate with zeroing or whatever, and then `pred` is just a normal operand as the binding section specifies everything.This is not current behaviour yet but it I am considering it when I need to specify this for even AVX-512 and RISC-V behaviour (which has multiple different possibilities).
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).
https://en.wikipedia.org/wiki/Typed_assembly_language
https://www.cs.cornell.edu/talc/overview.html
TALs are also solving an entirely different problem.
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.
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?
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.
I suspect the main reason someone might quibble over the “assembly is typed” assertion is that many programmers have a rather narrow view of type systems, heavily skewed by OOP patterns.
And we already track all of the basic side-effects and clobbering that each form of each mnemonic does. That's kind of the entire point of this being possible: it's all "typed".
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.
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.
When one provides the full effect of the operation in the source code, a properly ported compiler backend should be able to spot the pattern and emit the instructions with matching non-C semantics. (Typically, saturated operations are a very low hanging fruit.)
That doesn't always work because the optimization passes targeting local optimum break these patterns while "optimizing" them, so they arrive to the instruction emitter unrecognizable. And these passes, living in the generic "good-for-all" area of the compiler core, cannot be made aware of what a particular target does or doesn't support. So ironically, such instructions appear more when the optimizations are disabled.
Intrinsics emit internal representation forms that optimization passes don't dare to touch.
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.
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.
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.
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.
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.
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.
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.
> 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-...
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):);`.
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.
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).
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.
>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.
It’s an effect in the type system, not a data type or behavior.
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.
Ultimately the proof is in the pudding. If I screw up some inline assembly in Rust the diagnostics aren't very good because Rust doesn't deeply understand the assembly, whereas obviously for other things they're excellent. If Odin's diagnostics are great because it actually understands these "templates" that's a meaningful benefit to programmers.
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.
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.
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.
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.
Most ISAs do.
On x86 and arm scalar and FP registers are separate, it's just that they overlap FP and SIMD registers.
On RISC-V there are three separate register files for scalar, FP and SIMD. Although you can overlap scalar and FP in some minimal embedded configurations.
Most assemblers do not allow the programmer to declare the data types of the variables, so they do not check whether the types of the operands are valid.
Nonetheless, nothing would stop someone to modify an assembler to require the declaration of the data types and to generate assembly errors whenever there is a type mismatch.
The fact that if you give to an instruction that expects a floating-point number an integer operand then the CPU will compute a bogus value, is irrelevant.
The same will happen if you give operands of the wrong type to any function of a program written in a high-level language.
Any decent modern compiler will prevent you to use the wrong types, but if you use some tricks, you can still avoid this and invoke a function with wrong argument types, in which case it will compute some gibberish.
So the parameters of HLL functions or procedures have well defined data types and the same is true for the operands of CPU instructions.
Enforcing the use of the correct types can be done only at compile-time/assembly time, by the compiler or assembler.
This happens because for efficiency reasons the primitive data types do not contain a tag for identifying their type. Only the programmer-defined union types contain a type tag, so that their type can be identified at run-time. In the languages without static type checking, but with dynamic type checking, any value belongs actually to a kind of tagged union type that includes all the types that can be used in that language, so the type of any argument can be determined at run time, but this is what makes such languages slow.
In conclusion, there is no real difference between an assembly language and a HLL with static type checking, except that most people who have written assemblers were lazy and they did not implement type checking.
The first C compilers never checked the types of the function arguments. Today this would no longer be an acceptable for a C compiler. For the same reasons, it should be no longer acceptable for any assembler to not check the data types.
It is likely that the fact that no well-known assembler does an appropriate type checking is due to the necessity of defining a much more ample type system for an assembly language than for most high-level languages, so this would be a lot of work. Because the assembly programmers were used to lax assemblers anyway, implementing such a feature was not considered as a priority.
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?
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.