dinline-assemblygdc

D - friendlier asm syntactic sugar for GDC (Dlang)


I had an idea to ease the process of creating D plus asm code using GDC's extended asm syntax. I'd like to get rid of the need to insert \n\t markers all over the place by, say, having separate strings and getting the D compiler to concatenate them. But I'm open to other suggestions. My attempt failed, because concatenating D strings unfortunately doesn't work in GDC at compile-time and I need CTFE. It's a requirement, as you'd expect, that this piece of sugar has zero cost.

I need to do something with mixin, I suspect. Any tips on where to go and how to stay within CTFE?


Solution

  • GDC is buggy in that the AssemblerTemplate in extended inline ASM is supposed to be a compile-time generate string, but actually isn't. What you can do is generate the string, put all the ASM stuff around it, and mix it in. I've been using something similar for a custom syscall implementation (which still inlines).

    module test;
    
    template joinASMStatements(T...) {
        static if (T.length == 0) enum joinASMStatements = "";
        else static if (T.length == 1) enum joinASMStatements = T[0];
        else enum joinASMStatements = joinASMStatements!(T[0..$/2]) ~ "\\n\\t" ~  joinASMStatements!(T[$/2..$]);
    }
    
    void main() {
        ulong dst, src = 20;
        enum statements = joinASMStatements!("mov %[dst], %[src]");
        mixin("asm {" ~ statements ~ " : [dst] \"rm,r\" dst : [src] \"ri,rmi\" src }");
    }
    

    But frankly, that looks horrifying. It would be easier and more aesthetically pleasing to create a template to handle all of this for you, which takes an array of strings. You could implement extra stuffs in the template for handling certain opcodes and adding constraints automatically based on them. That would enable the code to work on DMD and LDC too, if you wanted it to. And you can use a bit of compile-time magic to work this all out. (EDIT) This actually works.

    module test2;
    
    import std.traits: AliasSeq;
    
    // Input operand type
    struct IOp(string _name) {
        string constraints; // A set of constraints. This is the whole thing.
        string asmName; // A label to be given to the operand (the "[<name>]" thing)
        enum name = _name; // Inner usage, to ease accessing `_name`.
    }
    
    // Output operand type
    struct OOp(string _name) {
        // For variable details, see IOp comments.
        string constraints;
        string asmName;
        enum name = _name;
    }
    
    // type for register (and "cc" and "memory") clobbers
    struct Clobber(string _reg) {enum reg = _reg;}
    
    // type for goto labels
    struct Goto(string _goto) {enum name = _goto;} // note that `goto` is a D keyword.
    
    // filters out types as S!(string blah)
    template filterOp(alias S, T...) {
        static if (T.length == 0) alias filterOp = AliasSeq!();
        else static if (T.length == 1) {
            static if (is(typeof(T[0]) : S!(N), string N))
                alias filterOp = AliasSeq!(T[0]);
            else
                alias filterOp = AliasSeq!();
        } else
            alias filterOp = AliasSeq!(filterOp!(S, T[0..$/2]), filterOp!(S, T[$/2..$]));
    }
    
    // joiner function for input and output operands.
    template joinOps(T...) {
        static if (T.length == 0) enum joinOps = "";
        else static if (T.length == 1) enum joinOps = ((T[0].asmName != "")?"[" ~ T[0].asmName ~ "] ":"") ~ "\"" ~ T[0].constraints ~ "\" " ~ T[0].name; // The .name unescapes the name
        else enum joinOps = joinOps!(T[0..$/2]) ~ ", " ~ joinOps!(T[$/2..$]);
    }
    
    // joiner function for clobbers
    template joinClobbers(T...) {
        static if (T.length == 0) enum joinClobbers = "";
        else static if (T.length == 1) enum joinClobbers = "\"" ~ T[0].reg ~ "\"";
        else enum joinClobbers = joinClobbers!(T[0..$/2]) ~ ", " ~ joinClobbers!(T[$/2..$]);
    }
    
    // joiner function for goto labels
    template joinGotos(T...) {
        static if (T.length == 0) enum joinGotos = "";
        else static if (T.length == 1) enum joinGotos = T[0].name; // Here the label is unescaped
        else enum joinGotos = joinGotos!(T[0..$/2]) ~ ", " ~ joinGotos!(T[$/2..$]); // Recursively executes itself on halves of the input. Eventually, the halves become lengths of `1` or `0`, and they are actually evaluated.
    }
    
    // joiner function for instructions.
    template joinInstrs(string[] instrs) {
        static if (instrs.length == 0) enum joinInstrs = "";
        else static if (instrs.length == 1) enum joinInstrs = instrs[0];
        else enum joinInstrs = joinInstrs!(instrs[0..$/2]) ~ "\\n\\t" ~ joinInstrs!(instrs[$/2..$]);
    }
    
    // complete assembly generator function. Output is to be mixed in.
    template ASM(string[] ops, T...) {
        enum iops = joinOps!(filterOp!(IOp, T));
        enum oops = joinOps!(filterOp!(OOp, T));
        enum clobbers = joinClobbers!(filterOp!(Clobber, T));
        enum gotos = joinGotos!(filterOp!(Goto, T));
        enum instrs = "\"" ~ joinInstrs!(ops) ~ "\"";
        enum ASM = "asm { " ~ instrs ~ " : " ~ oops ~ " : " ~ iops ~ " : " ~ clobbers ~ " : " ~ gotos ~ "; }";
    }
    
    void main() {
        ulong src = 24, dst;
        mixin(ASM!(["mov %[dst], %[src]"], IOp!"src"("ri,rmi", "src"), OOp!"dst"("=rm,r", "dst")));
    }
    

    NOTES:

    BTW, thanks for making me think of this! I need to implement it for my own stuff now.