More mixin tips to avoid stringof

Posted 2022-12-26

I wrote at some length in a chatroom about mixin and since I'm behind schedule I'm gonna be lazy and link it here.

Core D Development Statistics

In the community

Community announcements

See more at the announce forum.

Random thought:

What if we had a "must override" method: https://forum.dlang.org/post/pskvoctnusqkpmjxjvfn@forum.dlang.org which reminds people to fill it in for child classes, even if there is an implementation in the base class already? Could prompt them toward mixin templates with reflection-based implementations.

More mixin techniques

I did two case studies to remove .stringof and reduce some string concatenation.

I wrote most of this on the discord chatroom: https://discord.com/channels/242094594181955585/242122752436338688/1059196133580214362

But here's some code comparisons. This is from DCompute:

http://arsdnet.net/dcode/stringof.d

http://arsdnet.net/dcode/nostringof.d

1 module dcompute.driver.ocl.util;
2 
3 import std.range;
4 import std.meta;
5 import std.traits;
6 
7 //deal with arrays seperately, in part to avoid any
8 //narrow-string idiocy
9 @property auto memSize(R)(R r)
10 if (is(R : T[], T))
11 {
12     static if (is(R : T[], T))
13         return r.length * T.sizeof;
14     else
15         static assert(false);
16 }
17 
18 @property auto memSize(R)(R r)
19 if(isInputRange!R && hasLength!R && !is(R : T[], T))
20 {
21     return r.length * (ElementType!R).sizeof;
22 }
23 
24 T[Args.length + 1] propertyList(T,Args...)(Args args)
25 {
26     T[Args.length + 1] props;
27     foreach(i, arg; args)
28         props[i] = *cast(T*)(&arg);
29     props[$-1] = cast(T)0;
30     return props;
31 }
32 
33 struct ArrayAccesssor(alias ptr, alias len) {}
34 
35 struct StringzAccessor(alias ptr) {}
36 
37 struct ZeroTerminatedArrayAccessor(alias ptr) {}
38 
39 struct ArrayAccesssor2D(alias ptr, alias lens, alias len) {}
40 
41 // Returned by ArrayAccesssor2D
42 struct RangeOfArray(T)
43 {
44     T**     ptr;
45     size_t* lengths;
46     size_t  length;
47     size_t  index;
48 
49     bool empty()
50     {
51         return index == length;
52     }
53 
54     @property T[] front()
55     {
56         return ptr[index][0 .. lengths[index]];
57     }
58 
59     T[] opIndex(size_t i)
60     {
61         return ptr[i][0 .. lengths[i]];
62     }
63     void popFront()
64     {
65         ++index;
66     }
67     
68     @property size_t opDollar() { return length; }
69 }
70 
71 string generateGetInfo(Info,alias func,string args = "raw")()
72 {
73     import std.string;
74     return helper!(Info.tupleof).format(func.stringof,args);
75 }
76 
77 // A substitute for fullyQualifiedName to speed up compile time
78 private template isModule(alias a) {
79     static if (is(a) || is(typeof(a)) || a.stringof.length < 7) {
80         enum isModule = false;
81     } else {
82         enum isModule = a.stringof[0..7] == "module ";
83     }
84 }
85 
86 private template partiallyQualifiedName(alias a) {
87     static if (isModule!a) {
88         enum partiallyQualifiedName = "";
89     } else {
90         static if (!isModule!(__traits(parent, a))) {
91             enum prefix = partiallyQualifiedName!(__traits(parent, a)) ~ ".";
92         } else {
93             enum prefix = "";
94         }
95         enum partiallyQualifiedName = prefix ~ __traits(identifier, a);
96     }
97 }
98 
99 private template helper(Fields...)
100 {
101     static if (Fields.length == 0)
102         enum helper = "";
103 
104     else static if (is(typeof(Fields[0]) : ArrayAccesssor!(ptr,len),alias ptr,alias len))
105     {
106         enum helper = "@property " ~ typeof(*ptr).stringof ~ "[] " ~ Fields[0].stringof ~ "()\n" ~
107             "{\n" ~
108             "    return " ~ ptr.stringof ~ "[0 .. " ~ len.stringof ~"];"~
109             "}\n" ~ helper!(Fields[1 .. $]);
110     }
111     else static if (is(typeof(Fields[0]) : StringzAccessor!ptr,alias ptr))
112     {
113         enum helper = "@property char[] " ~ Fields[0].stringof ~ "()\n" ~
114             "{\n" ~
115             "    import std.typecons; char[] ret;" ~
116             "    size_t len;" ~
117             "    %1$s(%2$s," ~ __traits(getAttributes, ptr).stringof ~ "[0], 0, null, &len);" ~
118             "    ret.length = len;" ~
119             "    %1$s(%2$s," ~ __traits(getAttributes, ptr).stringof ~ "[0], memSize(ret), ret.ptr, null);" ~
120             "    return ret;" ~
121             "}\n" ~ helper!(Fields[1 .. $]);
122     }
123     else static if (is(typeof(Fields[0]) : ArrayAccesssor2D!(ptr,lens,len) , alias ptr, alias lens, alias len))
124     {
125         enum helper = "@property RangeOfArray!(" ~ typeof(**ptr).stringof ~ ") " ~ Fields[0].stringof ~ "()\n" ~
126             "{\n" ~
127             "   import std.typecons; size_t length; size_t* lengths; " ~ typeof(ptr).stringof ~ " ptr;" ~
128             "   %1$s(%2$s," ~ __traits(getAttributes, len).stringof ~ "[0],length.sizeof, &length,null);" ~
129             "   lengths = (new size_t[length]).ptr; ptr = (new " ~ typeof(*ptr).stringof ~ "[length]).ptr;" ~
130             "   %1$s(%2$s," ~ __traits(getAttributes, lens).stringof ~ "[0],lengths.sizeof, lengths,null);" ~
131             "   if (lengths[length - 1] == 0) length--;" ~
132             "   foreach(i; 0 .. length) \n{" ~
133             "       ptr[i] = (new " ~ typeof(**ptr).stringof ~ "[lengths[i]]).ptr;" ~
134             "   }\n" ~
135             "   %1$s(%2$s," ~ __traits(getAttributes, ptr).stringof ~ "[0], ptr.sizeof, ptr, null);" ~
136             "   return typeof(return)(ptr,lengths,length,0);" ~
137             "}\n" ~ helper!(Fields[1 .. $]);
138     }
139     else
140     {
141         static if (is(typeof(Fields[0]) == enum))
142         {
143             enum helper = "@property " ~ partiallyQualifiedName!(typeof(Fields[0])) ~ " " ~ Fields[0].stringof ~ "()\n" ~
144                 "{\n" ~
145                 "    import std.typecons; typeof(return) ret;" ~
146                 "%1$s(%2$s,"~ __traits(getAttributes, Fields[0]).stringof ~ "[0], ret.sizeof, &ret, null);" ~
147                 "return ret; " ~ 
148                 "}\n" ~ helper!(Fields[1 .. $]);
149     
150         }
151         else 
152         {
153             enum helper = "@property " ~ typeof(Fields[0]).stringof ~ " " ~ Fields[0].stringof ~ "()\n" ~
154                 "{\n" ~
155                 "    import std.typecons; typeof(return) ret;" ~
156                 "%1$s(%2$s,"~ __traits(getAttributes, Fields[0]).stringof ~ "[0], ret.sizeof, &ret, null);" ~
157                 "return ret; " ~ 
158                 "}\n" ~ helper!(Fields[1 .. $]);
159         }
160     }
161 }
1 module dcompute.driver.ocl.util;
2 
3 import std.range;
4 import std.meta;
5 import std.traits;
6 
7 //deal with arrays seperately, in part to avoid any
8 //narrow-string idiocy
9 @property auto memSize(R)(R r)
10 if (is(R : T[], T))
11 {
12     static if (is(R : T[], T))
13         return r.length * T.sizeof;
14     else
15         static assert(false);
16 }
17 
18 @property auto memSize(R)(R r)
19 if(isInputRange!R && hasLength!R && !is(R : T[], T))
20 {
21     return r.length * (ElementType!R).sizeof;
22 }
23 
24 T[Args.length + 1] propertyList(T,Args...)(Args args)
25 {
26     T[Args.length + 1] props;
27     foreach(i, arg; args)
28         props[i] = *cast(T*)(&arg);
29     props[$-1] = cast(T)0;
30     return props;
31 }
32 
33 struct ArrayAccesssor(alias ptr, alias len) {}
34 
35 struct StringzAccessor(alias ptr) {}
36 
37 struct ZeroTerminatedArrayAccessor(alias ptr) {}
38 
39 struct ArrayAccesssor2D(alias ptr, alias lens, alias len) {}
40 
41 // Returned by ArrayAccesssor2D
42 struct RangeOfArray(T)
43 {
44     T**     ptr;
45     size_t* lengths;
46     size_t  length;
47     size_t  index;
48 
49     bool empty()
50     {
51         return index == length;
52     }
53 
54     @property T[] front()
55     {
56         return ptr[index][0 .. lengths[index]];
57     }
58 
59     T[] opIndex(size_t i)
60     {
61         return ptr[i][0 .. lengths[i]];
62     }
63     void popFront()
64     {
65         ++index;
66     }
67     
68     @property size_t opDollar() { return length; }
69 }
70 
71 mixin template generateGetInfo(Info,alias func, args...)
72 {
73     static foreach(Field; Info.tupleof) {
74         mixin helper!(Field, Info, func, args);
75     }
76 }
77 
78 mixin template helper(alias Field, Info, alias func, args...)
79 {
80     static if (is(typeof(Field) : ArrayAccesssor!(ptr,len),alias ptr,alias len))
81     {
82         mixin(q{@property typeof(*ptr)[] } ~ __traits(identifier, Field) ~ q{()
83             {
84                 return ptr[0 .. len];
85             }
86         });
87     }
88     else static if (is(typeof(Field) : StringzAccessor!ptr,alias ptr))
89     {
90         mixin(q{@property char[] } ~ __traits(identifier, Field) ~ q{()
91             {
92                 char[] ret;
93                 size_t len;
94                 func(args, __traits(getAttributes, ptr)[0], 0, null, &len);
95                 ret.length = len;
96                 func(args, __traits(getAttributes, ptr)[0], memSize(ret), ret.ptr, null);
97                 return ret;
98             }
99         });
100     }
101     else static if (is(typeof(Field) : ArrayAccesssor2D!(ptr,lens,len) , alias ptr, alias lens, alias len))
102     {
103         mixin(q{@property RangeOfArray!(typeof(**ptr)) } ~ __traits(identifier, Field) ~ q{()
104             {
105                size_t length; size_t* lengths; typeof(ptr) ptr;
106                func(args, __traits(getAttributes, len)[0],length.sizeof, &length,null);
107                lengths = (new size_t[length]).ptr; ptr = (new typeof(*ptr)[length]).ptr;
108                func(args, __traits(getAttributes, lens)[0],lengths.sizeof, lengths,null);
109                if (lengths[length - 1] == 0) length--;
110                foreach(i; 0 .. length)
111                {
112                    ptr[i] = (new typeof(**ptr)[lengths[i]]).ptr;
113                }
114                func(args, __traits(getAttributes, ptr)[0], ptr.sizeof, ptr, null);
115                return typeof(return)(ptr,lengths,length,0);
116             }
117         });
118     }
119     else
120     {
121         static if (is(typeof(Field) == enum))
122         {
123             mixin(q{@property typeof(Field) } ~ __traits(identifier, Field) ~ q{()
124                 {
125                 typeof(return) ret;
126                 func(args, __traits(getAttributes, Field)[0], ret.sizeof, &ret, null);
127                 return ret;
128                 }
129             });
130         }
131         else 
132         {
133             mixin(q{@property typeof(Field) } ~ __traits(identifier, Field) ~ q{()
134                 {
135                 typeof(return) ret;
136                 func(args, __traits(getAttributes, Field)[0], ret.sizeof, &ret, null);
137                 return ret;
138                 }
139             });
140         }
141     }
142 }

Before on the left, after on the right. Call sites changed to use mixin thing!(args) instead of mixin(thing(args));. Compile time in initial test cut by over 70%.

Separate discord chat (linked above) also had one about interface forwarding where the user had a bunch of ugly string interpolating code. Here's the self-contained after code:

1 interface ITest {
2 	void foo() @(4) @nogc;
3 	string bar(int a, int b) const;
4 	string bar(string s) nothrow;
5 }
6 
7 class Impl :ITest {
8 	void foo() {import core.stdc.stdio; printf("foo\n");}
9 	string bar(int a, int b) const { return "lol"; }
10 	string bar(string s) { return "rofl"; }
11 }
12 
13 class A : ITest {
14 	this() { member = new Impl;}
15 
16 	ITest member;
17 
18 	mixin ForwardInterfaceTo!(ITest, member);
19 }
20 
21 mixin template ForwardInterfaceTo(IFace, alias member) {
22 	static foreach(memberName; __traits(allMembers, IFace))
23 	static foreach(idx, overload; __traits(getOverloads, IFace, memberName)) {
24 		// the EXTREMELY RARE case where stringof works, though technically you're
25 		// still better off using to!string(idx) or similar
26 		mixin("mixin helper!(overload, member) " ~ memberName ~ idx.stringof ~ ";");
27 		// this is to merge the overload sets from across the helper things
28 		// if you didn't care about overloads, no need for mixin strings up here, but alas
29 		// and these need mixin conats because it is basically all name
30 		mixin("alias " ~ memberName ~ " = " ~ memberName ~ idx.stringof ~ "." ~ memberName ~ ";");
31 
32 		// pity we don't have Paul's `mixin foo this;` thing. that's not always the right thing to do but it would cover this beautifully. alas.
33 	}
34 }
35 
36 string constness(alias member)() {
37 	string ret;
38 	foreach(attr; __traits(getFunctionAttributes, member))
39 		if(attr == "const" || attr == "immutable")
40 			ret = attr;
41 	return ret;
42 }
43 
44 mixin template helper(alias member, alias forwardTo) {
45 	static if(is(typeof(member) ReturnType == return))
46 	static if(is(typeof(member) Params == __parameters))
47 	mixin(q{
48 		ReturnType
49 		// you must concat the name...
50 		} ~ __traits(identifier, member) ~ q{
51 		// and the constness
52 		(Params params) } ~ constness!member ~ q{
53 		@(__traits(getAttributes, member))
54 		{
55 			// but not much of anything else
56 			return __traits(child, __traits(child, this, forwardTo), member)(params);
57 		}
58 	});
59 }
60 
61 void main() {
62 	import std.stdio;
63 	auto a = new A;
64 	a.foo();
65 	writeln(a.bar("foo"));
66 	writeln(a.bar(4, 3));
67 	pragma(msg, __traits(getAttributes, A.foo));
68 }

This is a bit uglier to support const and overloads in the interface, and I don't have the "before" code to compare against, but the general idea in both studies is:

  • Always use local names. Don't try to reconstruct an outside name - don't use .stringof or fullyQualifiedName or anything similar (except in rare exceptions). More often than not, when you write " ~ T.stringof ~ " you can simply replace that with T.
  • Use mixin templates to help isolate namespaces. Reflection info might need to be contained inside them to avoid redefinition errors in static foreaches.
  • Use inline string mixins - that is, a string that starts on the same line as the mixin() code, instead of having it returned from a function - to get easier to read error messages when you need them to match names.
  • Use inference whenever possible to avoid having to query data to spit back out in the info.

And yes, I know, I'm being lazy this week but that's all for now.