Skip to content

Instantly share code, notes, and snippets.

@PetarKirov
Created September 9, 2017 13:09
Show Gist options
  • Save PetarKirov/15e0299ad4df979f2a9aad0621510295 to your computer and use it in GitHub Desktop.
Save PetarKirov/15e0299ad4df979f2a9aad0621510295 to your computer and use it in GitHub Desktop.
CTFE RNG
// http://forum.dlang.org/post/oxxydengmzzefjxjqgma@forum.dlang.org
/+ CTRNG
Proof-Of-Concept Compile-Time Random Number Generator.
Please never actually use this or anything like it.
While doing some metaprogramming tomfoolery, I stumbled into an
interesting scenario. It occurred to me that with some work, I could
probably turn what I had into a functioning compile-time random
number generator, despite D's restrictions on metaprogramming intended
to keep things deterministic. Some of this may be obvious to you,
some of it may be interesting. Whether or not any of this is a bug, I do
not know. It probably isn't useful unless trying to sabotage a codebase.
Note: This is specifically developed with minimal dependence on other
modules (even standard ones), so some things are implemented in
non-idiomatic (read: strange) ways.
+/
/+
Step 1
We need something useable as a seed. Luckily, D has __TIMESTAMP__, which
reports time of build as a string in roughly the following format:
Wed Jan 20 18:04:41 2016
Specific contents of this function don't matter. All that matters is
timestamp goes in, largish unsigned integers come out. Feel free to skip
the implementation, it is not important. All that matters is that with
this, we will be able to get a psuedo-random seed at compile-time.
We *could* parse the timestamp accurately, but the rest of this is
hackish, so why not make this hackish as well?
+/
ulong timestampToUlong(string stamp)
{
ulong result;
foreach_reverse(c; stamp)
{
result += c;
result *= 10;
}
return result;
}
/+
Step 2
Next, we'll need the ability to track some sort of state. D is designed
with the intention that compile-time constructs cannot have a useful
permanent state, so this'll take some doing.
For starters, we need something we can query that can give different
results different times we call it (as in, impure). For this, we
have the following templated enum. It is manifest constant giving
the number of top-level members in the current module. However,
this number can change as other templates and mixins are resolved.
Technically, each instantiation is different, but because the dirty
details are hidden inside the default parameter value, it can be used
as if it should always be the same value.
+/
enum counter(size_t x = [__traits(allMembers, mixin(__MODULE__))].length)=x;
/+
Step 3
For step 2 to work, we also need the ability to add members in between
uses of `counter`. These need to be uniquely named, so we will
need a way to generate names. Step 2 already gave us a source of
increasing numbers, so we can trivially generate names based on
the value of `counter`. So, we define a method to yeild a string
containing a new unique declaration that we can `mixin()`.
+/
char[] member(ulong x)
{
char[] buf = "void[0] _X0000000000000000;".dup;
enum mapping = "0123456789abcdef";
foreach_reverse(i; buf.length-17 .. buf.length-1)
{
buf[i] = mapping[x & 0xf];
x >>= 4;
}
return buf;
}
/+
Step 4
This is just a simple wrapper combining `member` and `counter` under
a single name, making it slightly easier to increment the counter.
+/
mixin template next()
{
mixin(member(counter!()));
}
/+
Step 5
Finally, we define a simple XorShift64* RNG. We don't really
have arbitrary mutable state (yet?), so this depends on the D
compiler caching previous template instantiations.
The first is a specialization introducing our seed.
The second is how all subsequent xorShift instantiations are
handled, again taking advantage of changing default parameters.
+/
template xorShift(size_t x = counter!())
{
static if(x == 0)
{
enum xorShift = timestampToUlong(__TIMESTAMP__);
}
else
{
// Name previous result to reduce syntax noise.
enum y = xorShift!(x-1);
enum xorShift = 0x2545_f491_4f6c_dd1dUL
* (((y ^ (y >> 12)) ^ ((y ^ (y >> 12)) << 25))
^ (((y ^ (y >> 12)) ^ ((y ^ (y >> 12)) << 25)) >> 27));
}
}
/+
Now, let's use it!
+/
// Two instantiantions of xorShift result in the same value
static assert(xorShift!() == xorShift!());
// However, we can save one value,
enum a = xorShift!();
// ... update the counter,
mixin next;
// ... and save another value,
enum b = xorShift!();
// ... and they will not be the same.
static assert(a != b);
/+ What's more, because we have a time based seed, they will
all be different every time you compile this code. +/
pragma(msg, a);
pragma(msg, b);
mixin next;
mixin template headsOrTails(size_t x)
{
static if(x % 2 == 0)
{
pragma(msg, "Heads!");
}
else
{
pragma(msg, "Tails!");
}
}
mixin headsOrTails!(xorShift!());
/+
Conclusion
This is a really simple proof of concept. A "better" version
could use a recursively scanning counter implementation that
looked for symbols tagged with a certain User-Defined attribute
to allow advancing the RNG state inside structs and such.
More complicated RNGs could be implemented in a similar way.
There may be value in mixing in template specializations.
But then again, all this code is evil and I won't be doing
anything more with it.
+/
void main()
{
// Don't do anything at runtime. Just make dpaste's linker happy.
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment