rudy4 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +114 -0
- data/LICENSE +21 -0
- data/Makefile +137 -0
- data/README.md +400 -0
- data/bin/rudy4 +6 -0
- data/examples/hello_gem/README.md +91 -0
- data/examples/hello_gem/ext/hello_gem/extconf.rb +75 -0
- data/examples/hello_gem/ext/hello_gem/hello_gem.d +122 -0
- data/examples/hello_gem/hello_gem.gemspec +35 -0
- data/examples/hello_gem/lib/hello_gem/version.rb +5 -0
- data/examples/hello_gem/lib/hello_gem.rb +32 -0
- data/examples/hello_gem/test/smoke.sh +81 -0
- data/experiments/exp_compact.d +178 -0
- data/experiments/exp_convert.d +93 -0
- data/experiments/exp_cross.d +144 -0
- data/experiments/exp_defargs.d +301 -0
- data/experiments/exp_exc.d +265 -0
- data/experiments/exp_gcstorm.d +169 -0
- data/experiments/exp_http.d +254 -0
- data/experiments/exp_nonregression.d +209 -0
- data/experiments/exp_objproto.d +123 -0
- data/experiments/exp_perf.d +81 -0
- data/experiments/exp_wrap.d +186 -0
- data/lib/rudy4/cli.rb +204 -0
- data/lib/rudy4/doctor.rb +295 -0
- data/lib/rudy4/error.rb +10 -0
- data/lib/rudy4/generator.rb +172 -0
- data/lib/rudy4/templates.rb +484 -0
- data/lib/rudy4/vendor.rb +36 -0
- data/lib/rudy4/version.rb +5 -0
- data/lib/rudy4.rb +16 -0
- data/scripts/abi_audit.sh +64 -0
- data/scripts/memcheck.sh +77 -0
- data/scripts/vendor_check.sh +33 -0
- data/scripts/vendor_sync.sh +25 -0
- data/src/dexitest.d +278 -0
- data/src/dextest.d +119 -0
- data/src/dtypetest.d +159 -0
- data/src/rudy/args.d +253 -0
- data/src/rudy/bind/value.d +306 -0
- data/src/rudy/convert.d +221 -0
- data/src/rudy/defs.d +440 -0
- data/src/rudy/objproto.d +282 -0
- data/src/rudy/typeddata.d +143 -0
- data/test/dextest_loader.rb +6 -0
- data/test/exitest.rb +113 -0
- data/test/rudy4_test.rb +291 -0
- data/test/run.rb +74 -0
- data/test/typedtest.rb +69 -0
- data/vendor/rudy/args.d +253 -0
- data/vendor/rudy/bind/value.d +306 -0
- data/vendor/rudy/convert.d +221 -0
- data/vendor/rudy/defs.d +440 -0
- data/vendor/rudy/objproto.d +282 -0
- data/vendor/rudy/typeddata.d +143 -0
- metadata +106 -0
data/src/rudy/convert.d
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/// Two-way conversions between native D values and Ruby VALUEs.
|
|
2
|
+
/// D2 rewrite of the original RuDy make_object.d (2010), now calling only
|
|
3
|
+
/// exported Ruby functions — no struct peeking.
|
|
4
|
+
|
|
5
|
+
module rudy.convert;
|
|
6
|
+
|
|
7
|
+
import rudy.bind.value;
|
|
8
|
+
import std.traits;
|
|
9
|
+
import std.string : toStringz;
|
|
10
|
+
import std.exception : enforce;
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------- helpers
|
|
14
|
+
|
|
15
|
+
/// Opt-in raw-VALUE passthrough for def!'d function parameters.
|
|
16
|
+
///
|
|
17
|
+
/// VALUE identity(VALUE v) ... -> DECODED as ulong (VALUE is aliased
|
|
18
|
+
/// to ulong, and d_type now decodes it instead of passing the raw
|
|
19
|
+
/// tagged pointer through).
|
|
20
|
+
///
|
|
21
|
+
/// Raw!VALUE identity(Raw!VALUE v) ... -> raw, untouched VALUE.
|
|
22
|
+
///
|
|
23
|
+
/// Rationale: VALUE is `alias ulong VALUE`, so "parameter is VALUE" can't
|
|
24
|
+
/// be detected as a distinct type — and a plain `ulong` param used to hit
|
|
25
|
+
/// the old raw-passthrough branch, receiving the TAGGED value (Ruby 5
|
|
26
|
+
/// arrived as 11). Passthrough is now explicit: wrap in Raw!(...) to say
|
|
27
|
+
/// "give me the bits untouched".
|
|
28
|
+
struct Raw(T)
|
|
29
|
+
{
|
|
30
|
+
T value;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/// Compile-time check: is T the Raw!(...) passthrough wrapper?
|
|
34
|
+
template isRaw(T)
|
|
35
|
+
{
|
|
36
|
+
static if (is(T == Raw!R, R))
|
|
37
|
+
enum isRaw = true;
|
|
38
|
+
else
|
|
39
|
+
enum isRaw = false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/// Type traits the old rudy/ replaced its meta/ directory for.
|
|
43
|
+
template isDynamicArrayType(T) { enum bool isDynamicArrayType = isDynamicArray!T; }
|
|
44
|
+
template isStaticArrayType(T) { enum bool isStaticArrayType = isStaticArray!T; }
|
|
45
|
+
template isAssocArrayType(T) { enum bool isAssocArrayType = is(T == V[K], V, K); }
|
|
46
|
+
|
|
47
|
+
/// Byte length of a Ruby String, the exported-API way. History: the old
|
|
48
|
+
/// code sliced rb_str_strlen (a CHARACTER count) as a byte count —
|
|
49
|
+
/// truncating every multibyte string (finding #12) — and the natural fix
|
|
50
|
+
/// target rb_str_bytes_len turned out to be inline-macro-only, NOT
|
|
51
|
+
/// exported (caught locally by a symbol-lookup crash; the ABI audit would
|
|
52
|
+
/// have caught it in CI). Ruby's own String#bytesize is the exact answer;
|
|
53
|
+
/// rb_intern lazily, idempotently (VM is alive inside any method call).
|
|
54
|
+
private __gshared ID rudy_id_bytesize;
|
|
55
|
+
|
|
56
|
+
private long rudy_bytesize(VALUE str)
|
|
57
|
+
{
|
|
58
|
+
if (!rudy_id_bytesize)
|
|
59
|
+
rudy_id_bytesize = rb_intern("bytesize");
|
|
60
|
+
return cast(long) rb_num2ll(rb_funcallv(str, rudy_id_bytesize, 0, null));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/// Reject negative values where the D target type is unsigned: the
|
|
64
|
+
/// numeric family (rb_num2ulong / rb_num2uint) would otherwise wrap
|
|
65
|
+
/// them 2's-complement style (-1 arrived as 2^64-1, finding #13 family).
|
|
66
|
+
private void rudy_check_unsigned(VALUE val)
|
|
67
|
+
{
|
|
68
|
+
// Sign via double: rb_num2long would itself raise on values above
|
|
69
|
+
// LONG_MAX (2^64-1 is a legitimate ulong). Sign survives the cast;
|
|
70
|
+
// NaN/inf fail the comparison and get caught by num2ulong below.
|
|
71
|
+
if (rb_num2dbl(val) < 0)
|
|
72
|
+
rb_raise(rb_eRangeError, "negative value not convertible to unsigned D type");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/// Require a Ruby feature (e.g. "complex") before using it from D.
|
|
76
|
+
private void rb_require_c(string name)
|
|
77
|
+
{
|
|
78
|
+
VALUE req = rb_str_new(name.ptr, cast(C_long) name.length);
|
|
79
|
+
VALUE[1] av = [req];
|
|
80
|
+
rb_funcallv(rb_cObject, rb_intern("require"), 1, av.ptr);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ---------------------------------------------------------------- to_ruby_value
|
|
84
|
+
|
|
85
|
+
/// Convert a native D value into a Ruby VALUE.
|
|
86
|
+
VALUE to_ruby_value(T)(T t)
|
|
87
|
+
{
|
|
88
|
+
static if (isRaw!T) // Raw!VALUE -> pass bits through
|
|
89
|
+
return t.value;
|
|
90
|
+
else static if (is(T == bool))
|
|
91
|
+
return t ? Qtrue_ : Qfalse_;
|
|
92
|
+
|
|
93
|
+
else static if (is(T == char))
|
|
94
|
+
return rb_int2inum(t);
|
|
95
|
+
|
|
96
|
+
else static if (is(T == ulong) || is(T == C_ulonglong))
|
|
97
|
+
return rb_ull2inum(cast(C_ulonglong) t); // unsigned encode path —
|
|
98
|
+
// the old signed
|
|
99
|
+
// rb_ll2inum(cast(long))
|
|
100
|
+
// turned 2^64-1 into -1
|
|
101
|
+
// (finding #15)
|
|
102
|
+
|
|
103
|
+
else static if (is(T : C_longlong))
|
|
104
|
+
return rb_ll2inum(cast(long) t);
|
|
105
|
+
|
|
106
|
+
else static if (is(T : C_long))
|
|
107
|
+
return rb_int2inum(cast(int) t);
|
|
108
|
+
|
|
109
|
+
else static if (is(T : double)) // float, real promote
|
|
110
|
+
return rb_float_new(cast(double) t);
|
|
111
|
+
|
|
112
|
+
else static if (is(T == creal) || is(T == cdouble))
|
|
113
|
+
{
|
|
114
|
+
rb_require_c("complex");
|
|
115
|
+
VALUE c = rb_const_get(rb_cObject, rb_intern("Complex"));
|
|
116
|
+
VALUE[2] av = [to_ruby_value(t.re), to_ruby_value(t.im)];
|
|
117
|
+
return rb_funcallv(c, rb_intern("new"), 2, av.ptr);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
else static if (is(T == string))
|
|
121
|
+
return rb_str_new(t.ptr, cast(C_long) t.length);
|
|
122
|
+
|
|
123
|
+
else static if (isDynamicArrayType!T)
|
|
124
|
+
{
|
|
125
|
+
VALUE ary = rb_ary_new_capa(cast(C_long) t.length);
|
|
126
|
+
foreach (ref e; t)
|
|
127
|
+
rb_ary_push(ary, to_ruby_value(e));
|
|
128
|
+
return ary;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
else static if (isStaticArrayType!T)
|
|
132
|
+
{
|
|
133
|
+
VALUE ary = rb_ary_new_capa(cast(C_long) t.length);
|
|
134
|
+
foreach (ref e; t)
|
|
135
|
+
rb_ary_push(ary, to_ruby_value(e));
|
|
136
|
+
return ary;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
else static if (isAssocArrayType!T)
|
|
140
|
+
{
|
|
141
|
+
VALUE hash = rb_hash_new();
|
|
142
|
+
foreach (k, ref v; t)
|
|
143
|
+
rb_hash_aset(hash, to_ruby_value(k), to_ruby_value(v));
|
|
144
|
+
return hash;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
else static assert(0, "to_ruby_value: no conversion for " ~ T.stringof);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
// ---------------------------------------------------------------- d_type
|
|
152
|
+
|
|
153
|
+
/// Convert a Ruby VALUE into a native D type T.
|
|
154
|
+
///
|
|
155
|
+
/// Numeric decode order matters: EXACT types must be tested before the
|
|
156
|
+
/// subtype branches they would otherwise fall into — is(T : C_longlong)
|
|
157
|
+
/// matches int too, and the old chain let `int` decode through the
|
|
158
|
+
/// unchecked-wide path. Each exact type now gets Ruby's own
|
|
159
|
+
/// range-checked converter: rb_num2int/rb_num2uint raise RangeError on
|
|
160
|
+
/// overflow (finding #13: silent wraparound before), fractional floats
|
|
161
|
+
/// truncate toward zero and NaN/inf raise FloatDomainError — exactly
|
|
162
|
+
/// NUM2INT/NUM2UINT/Integer() semantics (finding #14).
|
|
163
|
+
T d_type(T)(VALUE val)
|
|
164
|
+
{
|
|
165
|
+
static if (is(T == bool))
|
|
166
|
+
{
|
|
167
|
+
// RUBY_TEST (RTEST) semantics: nil/false are false, everything
|
|
168
|
+
// else — 0, 0.0, "" — is true, matching Ruby's own truthiness.
|
|
169
|
+
// (The old `val == Qtrue_` made Ruby truthy 0 decode as D false.)
|
|
170
|
+
return val != Qnil_ && val != Qfalse_;
|
|
171
|
+
}
|
|
172
|
+
else static if (is(T == string))
|
|
173
|
+
{
|
|
174
|
+
VALUE str = rb_str_to_str(val);
|
|
175
|
+
auto ptr = rb_string_value_ptr(&str); // may swap str in-place
|
|
176
|
+
return ptr[0 .. rudy_bytesize(str)].idup; // BYTE length, not chars
|
|
177
|
+
// (finding #12)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// exact unsigned machine words first (VALUE itself is a ulong alias)
|
|
181
|
+
else static if (is(T == ulong) || is(T == C_ulonglong))
|
|
182
|
+
{
|
|
183
|
+
rudy_check_unsigned(val); // -1 must raise, not wrap
|
|
184
|
+
return cast(T) rb_num2ulong(val);
|
|
185
|
+
} // a ulong param still means
|
|
186
|
+
// "Ruby Integer -> machine word",
|
|
187
|
+
// NOT raw passthrough
|
|
188
|
+
else static if (is(T == int))
|
|
189
|
+
return cast(T) rb_num2int(val); // range-checked (finding #13)
|
|
190
|
+
else static if (is(T == uint))
|
|
191
|
+
{
|
|
192
|
+
rudy_check_unsigned(val);
|
|
193
|
+
return cast(T) rb_num2uint(val); // range-checked (finding #13)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// remaining signed subtypes (long, char, short...): wide is fine
|
|
197
|
+
else static if (is(T : C_longlong))
|
|
198
|
+
return cast(T) rb_num2ll(val);
|
|
199
|
+
|
|
200
|
+
// remaining unsigned subtypes (ubyte, ushort...): range-checked
|
|
201
|
+
else static if (is(T : uint))
|
|
202
|
+
{
|
|
203
|
+
rudy_check_unsigned(val);
|
|
204
|
+
return cast(T) rb_num2uint(val);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
else static if (is(T : double))
|
|
208
|
+
return cast(T) rb_num2dbl(val);
|
|
209
|
+
|
|
210
|
+
else static assert(0, "d_type: no conversion to " ~ T.stringof);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/// NB: D's uninitialized `double x;` is NaN (float.init), NOT 0.0 as in C.
|
|
214
|
+
/// Always explicitly initialize double accumulators: `double x = 0.0;` —
|
|
215
|
+
/// otherwise every sum silently becomes NaN (issue #3).
|
|
216
|
+
|
|
217
|
+
/// Raise a Ruby exception from D.
|
|
218
|
+
void rudy_raise(VALUE exc, string msg)
|
|
219
|
+
{
|
|
220
|
+
rb_raise(exc, "%s", toStringz(msg));
|
|
221
|
+
}
|
data/src/rudy/defs.d
ADDED
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
/// `def!` — register a D function as a Ruby method, with automatic
|
|
2
|
+
/// argument unwrapping (VALUE -> native D) and result rewrapping
|
|
3
|
+
/// (native D -> VALUE). The 2010 original passed raw VALUEs through;
|
|
4
|
+
/// this one finally does the wrapping PyD-style.
|
|
5
|
+
///
|
|
6
|
+
/// def!(myFunc); // global (Kernel) function
|
|
7
|
+
/// def!("MyClass", myFunc); // instance method of MyClass
|
|
8
|
+
/// def!("MyModule", myFunc); // module function
|
|
9
|
+
///
|
|
10
|
+
/// The D function may take native D types (int, double, string, ...) as
|
|
11
|
+
/// parameters; each argument is converted from the incoming VALUE with
|
|
12
|
+
/// d_type!T, and the return value goes back through to_ruby_value.
|
|
13
|
+
///
|
|
14
|
+
/// ---
|
|
15
|
+
///
|
|
16
|
+
/// Special parameter types (rudy/args.d) extend the model beyond fixed
|
|
17
|
+
/// positional arguments:
|
|
18
|
+
///
|
|
19
|
+
/// RubyBlock (last param) — receives the caller's block as a Proc;
|
|
20
|
+
/// blk.call(...) invokes it; blk.given says
|
|
21
|
+
/// whether a block was passed at all.
|
|
22
|
+
/// Rest — captures *rest into a Ruby Array.
|
|
23
|
+
/// KwArgs — captures **kwargs into a Ruby Hash
|
|
24
|
+
/// (given()==false when none passed).
|
|
25
|
+
/// Self (first param) — the receiver as raw VALUE.
|
|
26
|
+
/// Raw!T — opt-in raw VALUE passthrough.
|
|
27
|
+
///
|
|
28
|
+
/// Any def!'d function using a special parameter (or D default args) is
|
|
29
|
+
/// registered with arity -1 (argc/argv style); rb_scan_args splits the
|
|
30
|
+
/// arguments per a compile-time-generated format string and raises the
|
|
31
|
+
/// standard ArgumentError on wrong arity. Plain fixed-arity functions
|
|
32
|
+
/// go through the same uniform trampoline (also registered -1): one
|
|
33
|
+
/// code path for every def!'d method.
|
|
34
|
+
module rudy.defs;
|
|
35
|
+
|
|
36
|
+
import rudy.bind.value;
|
|
37
|
+
import rudy.convert;
|
|
38
|
+
import rudy.args;
|
|
39
|
+
import std.traits;
|
|
40
|
+
import std.string : toStringz;
|
|
41
|
+
import std.conv : to;
|
|
42
|
+
import std.meta : AliasSeq, anySatisfy;
|
|
43
|
+
import std.algorithm.iteration : splitter;
|
|
44
|
+
|
|
45
|
+
private:
|
|
46
|
+
|
|
47
|
+
// ------------------------------------------------------------------
|
|
48
|
+
// Parameter classification
|
|
49
|
+
// ------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
/// True for parameter types the trampoline fills in ITSELF (never
|
|
52
|
+
/// decoded from a positional VALUE): the rudy/args.d wrappers, the
|
|
53
|
+
/// Self receiver, and the opt-in Raw!T passthrough.
|
|
54
|
+
template isValueParam(T)
|
|
55
|
+
{
|
|
56
|
+
static if (is(T == RubyBlock) || is(T == Rest) || is(T == KwArgs)
|
|
57
|
+
|| is(T == Self) || isRaw!T)
|
|
58
|
+
enum bool isValueParam = true;
|
|
59
|
+
else
|
|
60
|
+
enum bool isValueParam = false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/// Decode one positional argument VALUE into parameter type T.
|
|
64
|
+
/// Raw!T passthrough wraps the bits; everything else goes through
|
|
65
|
+
/// d_type!T. (There is deliberately no plain-VALUE branch: VALUE is
|
|
66
|
+
/// aliased to ulong, and a ulong parameter means "Ruby Integer ->
|
|
67
|
+
/// machine word", NOT raw passthrough — see issue #1.)
|
|
68
|
+
T argFrom(T)(VALUE v)
|
|
69
|
+
{
|
|
70
|
+
static if (isRaw!T)
|
|
71
|
+
return T(v);
|
|
72
|
+
else
|
|
73
|
+
{
|
|
74
|
+
static assert(!isValueParam!T, "def!: " ~ T.stringof
|
|
75
|
+
~ " is filled by the trampoline, not decoded from argv");
|
|
76
|
+
return d_type!T(v);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private template isRestParam(T) { enum bool isRestParam = is(T == Rest); }
|
|
81
|
+
private template isKwParam(T) { enum bool isKwParam = is(T == KwArgs); }
|
|
82
|
+
private template isBlockParam(T) { enum bool isBlockParam = is(T == RubyBlock); }
|
|
83
|
+
private template isSelfParam(T) { enum bool isSelfParam = is(T == Self); }
|
|
84
|
+
|
|
85
|
+
/// Index of the first parameter satisfying pred, or -1.
|
|
86
|
+
template findIdx(alias pred, P...)
|
|
87
|
+
{
|
|
88
|
+
static if (P.length == 0)
|
|
89
|
+
enum int findIdx = -1;
|
|
90
|
+
else static if (pred!(P[0]))
|
|
91
|
+
enum int findIdx = 0;
|
|
92
|
+
else
|
|
93
|
+
enum int findIdx = findIdx!(pred, P[1 .. $]) == -1
|
|
94
|
+
? -1 : 1 + findIdx!(pred, P[1 .. $]);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ------------------------------------------------------------------
|
|
98
|
+
// ArgInfo: compile-time layout of a def!'d function's parameters.
|
|
99
|
+
//
|
|
100
|
+
// nLead / nOpt / nTrail / hasAsterisk — pieces of the rb_scan_args
|
|
101
|
+
// format string. At most one of { Rest, KwArgs, RubyBlock } per
|
|
102
|
+
// function (static assert'ed).
|
|
103
|
+
// posSlots — decodable positional slots in scan order (mandatory
|
|
104
|
+
// first, then optional): each element is the parameter index it
|
|
105
|
+
// maps to.
|
|
106
|
+
// restIdx / kwIdx / blkIdx / selfIdx — parameter indices, -1 when
|
|
107
|
+
// absent; filled by the trampoline (self from the self argument).
|
|
108
|
+
// ------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
struct ArgInfo(alias fn)
|
|
111
|
+
{
|
|
112
|
+
alias P = Parameters!(typeof(&fn));
|
|
113
|
+
alias DEFAULTS = ParameterDefaults!(fn);
|
|
114
|
+
|
|
115
|
+
enum bool hasRest = anySatisfy!(isRestParam, P);
|
|
116
|
+
enum bool hasKw = anySatisfy!(isKwParam, P);
|
|
117
|
+
enum bool hasBlock = anySatisfy!(isBlockParam, P);
|
|
118
|
+
enum bool hasSelf = anySatisfy!(isSelfParam, P);
|
|
119
|
+
|
|
120
|
+
enum int restIdx = findIdx!(isRestParam, P);
|
|
121
|
+
enum int kwIdx = findIdx!(isKwParam, P);
|
|
122
|
+
enum int blkIdx = findIdx!(isBlockParam, P);
|
|
123
|
+
enum int selfIdx = findIdx!(isSelfParam, P);
|
|
124
|
+
|
|
125
|
+
// positional classification of one parameter: is it a decodable
|
|
126
|
+
// argv slot, and must the caller supply it? Raw!T params are
|
|
127
|
+
// ordinary positional slots (raw passthrough instead of d_type).
|
|
128
|
+
template slotKind(size_t i)
|
|
129
|
+
{
|
|
130
|
+
static if (is(P[i] == RubyBlock) || is(P[i] == Rest)
|
|
131
|
+
|| is(P[i] == KwArgs) || is(P[i] == Self))
|
|
132
|
+
enum string slotKind = "none"; // filled by trampoline
|
|
133
|
+
else static if (!is(DEFAULTS[i] == void))
|
|
134
|
+
enum string slotKind = "opt"; // optional positional
|
|
135
|
+
else
|
|
136
|
+
enum string slotKind = "req"; // mandatory positional
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
enum nLead = countLead;
|
|
140
|
+
enum nOpt = countOpt;
|
|
141
|
+
enum nTrail = 0; // trailing mandatory after splat:
|
|
142
|
+
// not supported yet
|
|
143
|
+
enum bool hasAsterisk = hasRest;
|
|
144
|
+
|
|
145
|
+
// positional slots in scan order: mandatory first, then optional
|
|
146
|
+
template posSlotsOf(size_t i = 0)
|
|
147
|
+
{
|
|
148
|
+
static if (i >= P.length)
|
|
149
|
+
alias posSlotsOf = AliasSeq!();
|
|
150
|
+
else static if (slotKind!i == "none")
|
|
151
|
+
alias posSlotsOf = posSlotsOf!(i + 1);
|
|
152
|
+
else static if (slotKind!i == "req")
|
|
153
|
+
alias posSlotsOf = AliasSeq!(i, posSlotsOf!(i + 1));
|
|
154
|
+
else
|
|
155
|
+
alias posSlotsOf = AliasSeq!(i, posSlotsOf!(i + 1));
|
|
156
|
+
}
|
|
157
|
+
alias posSlots = posSlotsOf!();
|
|
158
|
+
|
|
159
|
+
// mandatory = leading run of "req" slots in scan order
|
|
160
|
+
private template countLeadImpl(size_t i, int acc)
|
|
161
|
+
{
|
|
162
|
+
static if (i >= P.length || slotKind!i != "req")
|
|
163
|
+
enum int countLeadImpl = acc;
|
|
164
|
+
else
|
|
165
|
+
enum int countLeadImpl = countLeadImpl!(i + 1, acc + 1);
|
|
166
|
+
}
|
|
167
|
+
enum int countLead = countLeadImpl!(0, 0);
|
|
168
|
+
|
|
169
|
+
enum int countOpt = posSlots.length - countLead;
|
|
170
|
+
|
|
171
|
+
// DEBUG
|
|
172
|
+
|
|
173
|
+
static assert(!(hasRest && hasKw), "def!: Rest + KwArgs in one function not supported yet");
|
|
174
|
+
static assert(blkIdx == -1 || blkIdx == P.length - 1,
|
|
175
|
+
"def!: RubyBlock must be the LAST parameter");
|
|
176
|
+
static assert(countLead + countOpt + (hasRest ? 1 : 0)
|
|
177
|
+
+ (hasKw || hasBlock ? 1 : 0) + (hasSelf ? 1 : 0)
|
|
178
|
+
== P.length,
|
|
179
|
+
"def!: unsupported parameter layout — special params (Rest/KwArgs/RubyBlock/Self) must not carry D default values");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ------------------------------------------------------------------
|
|
183
|
+
// rb_scan_args format string, generated at compile time:
|
|
184
|
+
// digits: nLead, then nOpt, then '*' when hasRest, then nTrail, then
|
|
185
|
+
// ':' when hasKw, then '&' when hasBlock.
|
|
186
|
+
// Examples: "0" (no args), "1", "12&" (1 req + up to 2 opt + block),
|
|
187
|
+
// "1*", "2*:", "0&", "1*&" (1 req + *rest + block).
|
|
188
|
+
// ------------------------------------------------------------------
|
|
189
|
+
|
|
190
|
+
private string scanFmt(alias AI)()
|
|
191
|
+
{
|
|
192
|
+
string s;
|
|
193
|
+
static if (AI.nLead > 0)
|
|
194
|
+
s ~= AI.nLead.to!string;
|
|
195
|
+
static if (AI.nOpt > 0)
|
|
196
|
+
s ~= AI.nOpt.to!string;
|
|
197
|
+
static if (AI.hasAsterisk)
|
|
198
|
+
s ~= "*";
|
|
199
|
+
static if (AI.nTrail > 0)
|
|
200
|
+
s ~= AI.nTrail.to!string;
|
|
201
|
+
static if (AI.hasKw)
|
|
202
|
+
s ~= ":";
|
|
203
|
+
static if (AI.hasBlock)
|
|
204
|
+
s ~= "&";
|
|
205
|
+
if (s.length == 0)
|
|
206
|
+
s = "0"; // zero-arg functions still get the
|
|
207
|
+
// argc == 0 arity check
|
|
208
|
+
return s;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ------------------------------------------------------------------
|
|
212
|
+
// The one uniform trampoline. Registered with arity -1 for every
|
|
213
|
+
// def!'d function; Ruby calls it as
|
|
214
|
+
// VALUE f(int argc, const(VALUE)* argv, VALUE self)
|
|
215
|
+
// (the classic variadic C-method ABI). rb_scan_args splits argv per
|
|
216
|
+
// the generated format (raising the standard ArgumentError on wrong
|
|
217
|
+
// arity), omitted optional slots become Qnil, and wrappers
|
|
218
|
+
// (Rest/KwArgs/RubyBlock) + Self/Raw slots are filled directly.
|
|
219
|
+
// ------------------------------------------------------------------
|
|
220
|
+
|
|
221
|
+
extern(C)
|
|
222
|
+
VALUE rudy_trampoline_var(alias fn)(int argc, const(VALUE)* argv, VALUE self)
|
|
223
|
+
{
|
|
224
|
+
alias AI = ArgInfo!(fn);
|
|
225
|
+
alias P = Parameters!(typeof(&fn));
|
|
226
|
+
alias DEFAULTS = ParameterDefaults!(fn);
|
|
227
|
+
alias SLOTS = AI.posSlots;
|
|
228
|
+
|
|
229
|
+
P args; // default-initialized
|
|
230
|
+
|
|
231
|
+
// rb_scan_args targets (positional slots in scan order); optional
|
|
232
|
+
// slots omitted by the caller are filled with Qnil by Ruby itself.
|
|
233
|
+
static if (SLOTS.length > 0)
|
|
234
|
+
VALUE[SLOTS.length] slotVals = Qnil_;
|
|
235
|
+
|
|
236
|
+
// NB: rb_scan_args' vararg order is FORMAT ORDER — '*' slot first,
|
|
237
|
+
// then ':' tail, then '&' tail. Rest+block is therefore two targets:
|
|
238
|
+
// restAry AND tail. KwArgs + RubyBlock can't coexist (: then & would
|
|
239
|
+
// be fine, but a function taking both kwargs and a block with the
|
|
240
|
+
// same Hash/Proc split is untestable through get!T semantics — see
|
|
241
|
+
// the static assert below).
|
|
242
|
+
static if (AI.hasRest)
|
|
243
|
+
{
|
|
244
|
+
VALUE restAry = Qnil_;
|
|
245
|
+
static if (AI.hasBlock)
|
|
246
|
+
{
|
|
247
|
+
VALUE tail = Qnil_; // '&' after '*'
|
|
248
|
+
static if (SLOTS.length == 0)
|
|
249
|
+
rb_scan_args(argc, argv, toStringz(scanFmt!(AI)), &restAry, &tail);
|
|
250
|
+
else static if (SLOTS.length == 1)
|
|
251
|
+
rb_scan_args(argc, argv, toStringz(scanFmt!(AI)),
|
|
252
|
+
&slotVals[0], &restAry, &tail);
|
|
253
|
+
else static if (SLOTS.length == 2)
|
|
254
|
+
rb_scan_args(argc, argv, toStringz(scanFmt!(AI)),
|
|
255
|
+
&slotVals[0], &slotVals[1], &restAry, &tail);
|
|
256
|
+
else static if (SLOTS.length == 3)
|
|
257
|
+
rb_scan_args(argc, argv, toStringz(scanFmt!(AI)),
|
|
258
|
+
&slotVals[0], &slotVals[1], &slotVals[2], &restAry, &tail);
|
|
259
|
+
else
|
|
260
|
+
static assert(0, "def!: max 3 positional slots + special params");
|
|
261
|
+
}
|
|
262
|
+
else static if (SLOTS.length == 0)
|
|
263
|
+
rb_scan_args(argc, argv, toStringz(scanFmt!(AI)), &restAry);
|
|
264
|
+
else static if (SLOTS.length == 1)
|
|
265
|
+
rb_scan_args(argc, argv, toStringz(scanFmt!(AI)),
|
|
266
|
+
&slotVals[0], &restAry);
|
|
267
|
+
else static if (SLOTS.length == 2)
|
|
268
|
+
rb_scan_args(argc, argv, toStringz(scanFmt!(AI)),
|
|
269
|
+
&slotVals[0], &slotVals[1], &restAry);
|
|
270
|
+
else static if (SLOTS.length == 3)
|
|
271
|
+
rb_scan_args(argc, argv, toStringz(scanFmt!(AI)),
|
|
272
|
+
&slotVals[0], &slotVals[1], &slotVals[2], &restAry);
|
|
273
|
+
else
|
|
274
|
+
static assert(0, "def!: max 3 positional slots + special params");
|
|
275
|
+
}
|
|
276
|
+
else static if (AI.hasKw || AI.hasBlock)
|
|
277
|
+
{
|
|
278
|
+
VALUE tail = Qnil_; // kwargs hash / block proc
|
|
279
|
+
static if (SLOTS.length == 0)
|
|
280
|
+
rb_scan_args(argc, argv, toStringz(scanFmt!(AI)), &tail);
|
|
281
|
+
else static if (SLOTS.length == 1)
|
|
282
|
+
rb_scan_args(argc, argv, toStringz(scanFmt!(AI)),
|
|
283
|
+
&slotVals[0], &tail);
|
|
284
|
+
else static if (SLOTS.length == 2)
|
|
285
|
+
rb_scan_args(argc, argv, toStringz(scanFmt!(AI)),
|
|
286
|
+
&slotVals[0], &slotVals[1], &tail);
|
|
287
|
+
else static if (SLOTS.length == 3)
|
|
288
|
+
rb_scan_args(argc, argv, toStringz(scanFmt!(AI)),
|
|
289
|
+
&slotVals[0], &slotVals[1], &slotVals[2], &tail);
|
|
290
|
+
else
|
|
291
|
+
static assert(0, "def!: max 3 positional slots + special params");
|
|
292
|
+
}
|
|
293
|
+
else static if (SLOTS.length > 0)
|
|
294
|
+
{
|
|
295
|
+
static if (SLOTS.length == 1)
|
|
296
|
+
rb_scan_args(argc, argv, toStringz(scanFmt!(AI)), &slotVals[0]);
|
|
297
|
+
else static if (SLOTS.length == 2)
|
|
298
|
+
rb_scan_args(argc, argv, toStringz(scanFmt!(AI)),
|
|
299
|
+
&slotVals[0], &slotVals[1]);
|
|
300
|
+
else static if (SLOTS.length == 3)
|
|
301
|
+
rb_scan_args(argc, argv, toStringz(scanFmt!(AI)),
|
|
302
|
+
&slotVals[0], &slotVals[1], &slotVals[2]);
|
|
303
|
+
else
|
|
304
|
+
static assert(0, "def!: max 3 positional slots (extend the ladder for more)");
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// fill wrapper params from the scan results
|
|
308
|
+
static if (AI.hasRest)
|
|
309
|
+
args[AI.restIdx] = Rest(restAry);
|
|
310
|
+
static if (AI.hasKw)
|
|
311
|
+
args[AI.kwIdx] = KwArgs(tail);
|
|
312
|
+
static if (AI.hasBlock)
|
|
313
|
+
args[AI.blkIdx] = RubyBlock(tail);
|
|
314
|
+
static if (AI.hasSelf)
|
|
315
|
+
args[AI.selfIdx] = Self(self);
|
|
316
|
+
|
|
317
|
+
// decode positional slots; D default values substitute for omitted
|
|
318
|
+
// optional arguments (Qnil means "not supplied"). Two-var static
|
|
319
|
+
// foreach: si = slot position (argv order), i = parameter index.
|
|
320
|
+
// NB: an AliasSeq passed as an alias template arg EXPANDS into
|
|
321
|
+
// separate args (a slotPos!(SLOTS, i) helper died exactly that
|
|
322
|
+
// way) - iterating it in foreach is the safe pattern.
|
|
323
|
+
static foreach (si, i; SLOTS)
|
|
324
|
+
{{
|
|
325
|
+
static if (is(DEFAULTS[i] == void))
|
|
326
|
+
args[i] = argFrom!(P[i])(slotVals[si]);
|
|
327
|
+
else
|
|
328
|
+
{
|
|
329
|
+
if (slotVals[si] == Qnil_)
|
|
330
|
+
{
|
|
331
|
+
static if (is(P[i] == VALUE))
|
|
332
|
+
args[i] = cast(VALUE) DEFAULTS[i];
|
|
333
|
+
else static if (isRaw!(P[i]))
|
|
334
|
+
args[i] = P[i](cast(VALUE) DEFAULTS[i]);
|
|
335
|
+
else
|
|
336
|
+
args[i] = DEFAULTS[i];
|
|
337
|
+
}
|
|
338
|
+
else
|
|
339
|
+
args[i] = argFrom!(P[i])(slotVals[si]);
|
|
340
|
+
}
|
|
341
|
+
}}
|
|
342
|
+
|
|
343
|
+
return to_ruby_value(fn(args));
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// ------------------------------------------------------------------
|
|
347
|
+
// Trampoline selection. Every def!'d function — fixed arity, defaults,
|
|
348
|
+
// or wrapper params — uses the uniform argc/argv trampoline, registered
|
|
349
|
+
// with arity -1. (The old per-arity extern(C) shims rudy_trampoline_0..
|
|
350
|
+
// _3 are gone: one code path, and blocks/rest/kwargs would not fit the
|
|
351
|
+
// fixed-arity ABI anyway.)
|
|
352
|
+
// ------------------------------------------------------------------
|
|
353
|
+
|
|
354
|
+
template rudy_trampoline(alias fn)
|
|
355
|
+
{
|
|
356
|
+
alias rudy_trampoline = rudy_trampoline_var!(fn);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/// Registered arity for Ruby: always -1 (argc/argv style).
|
|
360
|
+
enum int rudy_arity(alias fn) = -1;
|
|
361
|
+
|
|
362
|
+
// ------------------------------------------------------------------
|
|
363
|
+
// def! — the public interface
|
|
364
|
+
// ------------------------------------------------------------------
|
|
365
|
+
|
|
366
|
+
public:
|
|
367
|
+
|
|
368
|
+
/// Define fn as a global function (on Object).
|
|
369
|
+
void def(alias fn, string name = __traits(identifier, fn))()
|
|
370
|
+
{
|
|
371
|
+
// NB: alias template params must be forwarded in the bang-list;
|
|
372
|
+
// def!"Object"(fn, name) misparses under GDC-14 as a runtime call.
|
|
373
|
+
def!("Object", fn, name)();
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/// Define fn in the scope of `modulename` (class -> method, module ->
|
|
377
|
+
/// module function, anything else -> global on Object).
|
|
378
|
+
void def(string modulename, alias fn, string name = __traits(identifier, fn))()
|
|
379
|
+
{
|
|
380
|
+
VALUE parent = rb_const_get(rb_cObject, rb_intern(toStringz(modulename)));
|
|
381
|
+
alias tramp = rudy_trampoline!(fn);
|
|
382
|
+
|
|
383
|
+
// T_MODULE == 0x03 per value.d; rb_type is macro-only, classify
|
|
384
|
+
// via is_a?(Module)
|
|
385
|
+
VALUE[1] av = [rb_const_get(rb_cObject, rb_intern("Module"))];
|
|
386
|
+
VALUE res = rb_funcallv_public(parent, rb_intern("is_a?"), 1, av.ptr);
|
|
387
|
+
|
|
388
|
+
if (res == Qtrue_)
|
|
389
|
+
rb_define_module_function(parent, toStringz(name),
|
|
390
|
+
cast(AnyMethod) &tramp, rudy_arity!(fn));
|
|
391
|
+
else
|
|
392
|
+
rb_define_method(parent, toStringz(name),
|
|
393
|
+
cast(AnyMethod) &tramp, rudy_arity!(fn));
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/// Define fn as a PRIVATE instance method (e.g. DSL-style callbacks).
|
|
397
|
+
void def_private(string modulename, alias fn,
|
|
398
|
+
string name = __traits(identifier, fn))()
|
|
399
|
+
{
|
|
400
|
+
VALUE parent = rb_const_get(rb_cObject, rb_intern(toStringz(modulename)));
|
|
401
|
+
alias tramp = rudy_trampoline!(fn);
|
|
402
|
+
rb_define_private_method(parent, toStringz(name),
|
|
403
|
+
cast(AnyMethod) &tramp, rudy_arity!(fn));
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/// Define fn as a PROTECTED instance method.
|
|
407
|
+
void def_protected(string modulename, alias fn,
|
|
408
|
+
string name = __traits(identifier, fn))()
|
|
409
|
+
{
|
|
410
|
+
VALUE parent = rb_const_get(rb_cObject, rb_intern(toStringz(modulename)));
|
|
411
|
+
alias tramp = rudy_trampoline!(fn);
|
|
412
|
+
rb_define_protected_method(parent, toStringz(name),
|
|
413
|
+
cast(AnyMethod) &tramp, rudy_arity!(fn));
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/// Define fn as a SINGLETON method of `modulename`'s class object
|
|
417
|
+
/// (i.e. a class-level method: MyClass.def!'d_thing).
|
|
418
|
+
void def_singleton(string modulename, alias fn,
|
|
419
|
+
string name = __traits(identifier, fn))()
|
|
420
|
+
{
|
|
421
|
+
VALUE klass = rb_const_get(rb_cObject, rb_intern(toStringz(modulename)));
|
|
422
|
+
alias tramp = rudy_trampoline!(fn);
|
|
423
|
+
rb_define_singleton_method(klass, toStringz(name),
|
|
424
|
+
cast(AnyMethod) &tramp, rudy_arity!(fn));
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/// Define fn as a singleton method of a specific OBJECT (per-object
|
|
428
|
+
/// method; the object must be reachable by name at def! time).
|
|
429
|
+
void def_singleton_on(string objectname, alias fn,
|
|
430
|
+
string name = __traits(identifier, fn))()
|
|
431
|
+
{
|
|
432
|
+
// "A::B" paths walk the scope chain (rb_const_get resolves one
|
|
433
|
+
// constant per call, not a path)
|
|
434
|
+
VALUE obj = rb_cObject;
|
|
435
|
+
foreach (part; objectname.splitter("::"))
|
|
436
|
+
obj = rb_const_get(obj, rb_intern(toStringz(part)));
|
|
437
|
+
alias tramp = rudy_trampoline!(fn);
|
|
438
|
+
rb_define_singleton_method(obj, toStringz(name),
|
|
439
|
+
cast(AnyMethod) &tramp, rudy_arity!(fn));
|
|
440
|
+
}
|