Comment by tpmoney

1 day 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.

> 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.

  • 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.

    [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:

        really_awful_rng :: asm() -> (r: u64) [
            r = %eax,
        ] {
            add %eax, 0x1
        }
    

    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.

    • > 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.

      No. Never do this. If you don't care and just want random numbers use for example RDRAND, if you do care, design a proper PRNG with your preferred characteristics and seed management. YOLO programming is a bad idea always but it's especially bad when applied to assembly.

      To underscore what this software is for let me quote you a diagnostic it emits:

      {instruction} implicitly reads {register}, but nothing in this template produces a value for it; pin an input parameter to {register}, or write {register} before this instruction

      3 replies →