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/test/rudy4_test.rb
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Gem-level tests for the `rudy4` command: the scaffolder, the doctor, and the
|
|
4
|
+
# vendor refresh. Plain Ruby — no test framework, run it with:
|
|
5
|
+
#
|
|
6
|
+
# ruby -Ilib test/rudy4_test.rb # scaffolding + CLI (fast)
|
|
7
|
+
# RUDY4_FULL=1 ruby -Ilib test/rudy4_test.rb # + real GDC build (slow)
|
|
8
|
+
#
|
|
9
|
+
# The full build test is what CI runs: it scaffolds a project from the gem and
|
|
10
|
+
# drives `rake test` inside it, which is exactly the path a new user takes.
|
|
11
|
+
|
|
12
|
+
require "fileutils"
|
|
13
|
+
require "open3"
|
|
14
|
+
require "rbconfig"
|
|
15
|
+
require "stringio"
|
|
16
|
+
require "tmpdir"
|
|
17
|
+
|
|
18
|
+
require "rudy4"
|
|
19
|
+
|
|
20
|
+
$checks = 0
|
|
21
|
+
$failures = []
|
|
22
|
+
|
|
23
|
+
def assert(cond, msg)
|
|
24
|
+
$checks += 1
|
|
25
|
+
return if cond
|
|
26
|
+
|
|
27
|
+
$failures << msg
|
|
28
|
+
puts " FAIL: #{msg}"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def assert_equal(expected, actual, msg)
|
|
32
|
+
assert(expected == actual, "#{msg} (expected #{expected.inspect}, got #{actual.inspect})")
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def section(name)
|
|
36
|
+
puts "\n== #{name}"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def run_cli(*argv)
|
|
40
|
+
out = StringIO.new
|
|
41
|
+
err = StringIO.new
|
|
42
|
+
status = Rudy4::CLI.new(argv, out: out, err: err).run
|
|
43
|
+
[status, out.string, err.string]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
RUBY = RbConfig.ruby
|
|
47
|
+
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
section "CLI: version and help"
|
|
50
|
+
|
|
51
|
+
status, out, = run_cli("--version")
|
|
52
|
+
assert_equal 0, status, "--version exit status"
|
|
53
|
+
assert_equal Rudy4::VERSION, out.strip, "--version prints the version"
|
|
54
|
+
|
|
55
|
+
status, out, = run_cli("help")
|
|
56
|
+
assert_equal 0, status, "help exit status"
|
|
57
|
+
assert(out.include?("rudy4 init NAME"), "help documents init")
|
|
58
|
+
assert(out.include?("rudy4 doctor"), "help documents doctor")
|
|
59
|
+
assert(out.include?("rake compile"), "help shows the getting-started loop")
|
|
60
|
+
|
|
61
|
+
status, out, = run_cli(nil)
|
|
62
|
+
assert_equal 0, status, "bare invocation prints usage and exits 0"
|
|
63
|
+
assert(out.include?("Usage:"), "bare invocation prints usage")
|
|
64
|
+
|
|
65
|
+
status, _out, err = run_cli("frobnicate")
|
|
66
|
+
assert_equal Rudy4::CLI::EXIT_USAGE, status, "unknown command is a usage error"
|
|
67
|
+
assert(err.include?("unknown command"), "unknown command is reported")
|
|
68
|
+
|
|
69
|
+
status, _out, err = run_cli("init", "whatever", "--quick")
|
|
70
|
+
assert_equal Rudy4::CLI::EXIT_USAGE, status, "--quick is rejected for init"
|
|
71
|
+
assert(err.include?("not valid"), "invalid option is reported")
|
|
72
|
+
|
|
73
|
+
# ---------------------------------------------------------------------------
|
|
74
|
+
section "init: scaffold contents"
|
|
75
|
+
|
|
76
|
+
Dir.mktmpdir("rudy4-gem-test-") do |tmp|
|
|
77
|
+
status, out, err = run_cli("init", "fast_math", "-h")
|
|
78
|
+
assert_equal 0, status, "init -h prints usage"
|
|
79
|
+
|
|
80
|
+
status, out, err = run_cli("init", "fast_math")
|
|
81
|
+
assert_equal 0, status, "init exits 0 (err: #{err})"
|
|
82
|
+
|
|
83
|
+
root = File.join(tmp, "fast_math")
|
|
84
|
+
|
|
85
|
+
# The generator ran in Dir.pwd, so move it into the tmpdir to keep $PWD clean.
|
|
86
|
+
# (CLI uses Dir.pwd by design, so we exercise it there and clean up after.)
|
|
87
|
+
expected = [
|
|
88
|
+
"fast_math.gemspec",
|
|
89
|
+
"Rakefile",
|
|
90
|
+
"README.md",
|
|
91
|
+
".gitignore",
|
|
92
|
+
"lib/fast_math.rb",
|
|
93
|
+
"lib/fast_math/version.rb",
|
|
94
|
+
"ext/fast_math/fast_math.d",
|
|
95
|
+
"ext/fast_math/extconf.rb",
|
|
96
|
+
"ext/fast_math/rudy/defs.d",
|
|
97
|
+
"ext/fast_math/rudy/bind/value.d",
|
|
98
|
+
"ext/fast_math/rudy/convert.d",
|
|
99
|
+
"ext/fast_math/rudy/args.d",
|
|
100
|
+
"ext/fast_math/rudy/objproto.d",
|
|
101
|
+
"ext/fast_math/rudy/typeddata.d",
|
|
102
|
+
"test/fast_math_test.rb",
|
|
103
|
+
]
|
|
104
|
+
expected.each do |rel|
|
|
105
|
+
assert(File.file?(File.join("fast_math", rel)), "generated #{rel}")
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
assert(out.include?("Created fast_math/"), "init reports what it created")
|
|
109
|
+
assert(out.include?("rake compile"), "init prints the next steps")
|
|
110
|
+
|
|
111
|
+
# No leftover template placeholders anywhere.
|
|
112
|
+
Dir.glob("fast_math/**/*").select { |f| File.file?(f) }.each do |file|
|
|
113
|
+
body = File.read(file)
|
|
114
|
+
%w[PROJECT_NAME PROJECT_CONST RUBY_REQ AUTHOR EMAIL].each do |token|
|
|
115
|
+
assert(!body.include?(token), "#{file} has no unresolved #{token} placeholder")
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# The extension name must be spliced consistently: Init_<name>_ext, module,
|
|
120
|
+
# gem name, and the artifact name are all the same project.
|
|
121
|
+
d_source = File.read("fast_math/ext/fast_math/fast_math.d")
|
|
122
|
+
assert(d_source.include?('pragma(mangle, "Init_fast_math_ext")'), "Init_ entry point named after the project")
|
|
123
|
+
assert(d_source.include?('module fast_math;'), "D module named after the project")
|
|
124
|
+
assert(d_source.include?('rb_define_module("FastMath")'), "Ruby module is the camelized name")
|
|
125
|
+
assert(d_source.include?('def!("FastMath", add)'), "def! targets the Ruby module")
|
|
126
|
+
|
|
127
|
+
extconf = File.read("fast_math/ext/fast_math/extconf.rb")
|
|
128
|
+
assert(extconf.include?("TARGET = fast_math_ext."), "artifact is <name>_ext, not <name>")
|
|
129
|
+
assert(extconf.include?('RUDY_SRC = File.join(HERE, "rudy")'),
|
|
130
|
+
"extconf uses the vendored rudy sources, not a checkout path")
|
|
131
|
+
assert(!extconf.include?("RUDY4_ROOT"), "extconf does not need RUDY4_ROOT")
|
|
132
|
+
|
|
133
|
+
gemspec = File.read("fast_math/fast_math.gemspec")
|
|
134
|
+
assert(gemspec.include?('spec.extensions = ["ext/fast_math/extconf.rb"]'),
|
|
135
|
+
"gemspec declares the D extension")
|
|
136
|
+
|
|
137
|
+
lib_rb = File.read("fast_math/lib/fast_math.rb")
|
|
138
|
+
assert(lib_rb.include?('require "fast_math_ext"'), "lib requires the _ext artifact")
|
|
139
|
+
|
|
140
|
+
FileUtils.rm_rf("fast_math")
|
|
141
|
+
|
|
142
|
+
# -------------------------------------------------------------------------
|
|
143
|
+
section "init: argument validation"
|
|
144
|
+
|
|
145
|
+
status, _out, err = run_cli("init")
|
|
146
|
+
assert_equal Rudy4::CLI::EXIT_USAGE, status, "init without a name is a usage error"
|
|
147
|
+
assert(err.include?("usage: rudy4 init NAME"), "missing name shows usage")
|
|
148
|
+
|
|
149
|
+
status, _out, err = run_cli("init", "fast-math")
|
|
150
|
+
assert_equal Rudy4::CLI::EXIT_ERROR, status, "hyphenated name is rejected"
|
|
151
|
+
assert(err.include?("snake_case"), "hyphenated name explains the rule")
|
|
152
|
+
|
|
153
|
+
status, _out, err = run_cli("init", "FastMath")
|
|
154
|
+
assert_equal Rudy4::CLI::EXIT_ERROR, status, "capitalized name is rejected"
|
|
155
|
+
|
|
156
|
+
status, _out, err = run_cli("init", "ruby")
|
|
157
|
+
assert_equal Rudy4::CLI::EXIT_ERROR, status, "reserved name is rejected"
|
|
158
|
+
assert(err.include?("reserved"), "reserved name explains why")
|
|
159
|
+
|
|
160
|
+
status, _out, err = run_cli("init", "fast_math", "extra")
|
|
161
|
+
assert_equal Rudy4::CLI::EXIT_USAGE, status, "extra arguments are rejected"
|
|
162
|
+
|
|
163
|
+
# -------------------------------------------------------------------------
|
|
164
|
+
section "init: existing directory handling"
|
|
165
|
+
|
|
166
|
+
Dir.mktmpdir("rudy4-existing-") do |dir|
|
|
167
|
+
FileUtils.mkdir_p(File.join(dir, "taken"))
|
|
168
|
+
File.write(File.join(dir, "taken", "precious.txt"), "do not clobber")
|
|
169
|
+
|
|
170
|
+
generator = Rudy4::Generator.new("taken", parent_dir: dir, out: StringIO.new)
|
|
171
|
+
begin
|
|
172
|
+
generator.call
|
|
173
|
+
assert(false, "refuses to overwrite a non-empty directory")
|
|
174
|
+
rescue Rudy4::Error => e
|
|
175
|
+
assert(e.message.include?("already exists"), "existing directory is reported")
|
|
176
|
+
assert(e.message.include?("--force"), "the error suggests --force")
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
assert_equal "do not clobber", File.read(File.join(dir, "taken", "precious.txt")),
|
|
180
|
+
"existing files are untouched"
|
|
181
|
+
|
|
182
|
+
forced = Rudy4::Generator.new("taken", parent_dir: dir, force: true, out: StringIO.new)
|
|
183
|
+
forced.call
|
|
184
|
+
assert(File.file?(File.join(dir, "taken", "taken.gemspec")), "--force writes into the directory")
|
|
185
|
+
assert(File.file?(File.join(dir, "taken", "precious.txt")), "--force does not delete existing files")
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# -------------------------------------------------------------------------
|
|
189
|
+
section "vendor: refresh an existing project"
|
|
190
|
+
|
|
191
|
+
status, _out, err = run_cli("init", "vendored_app")
|
|
192
|
+
assert_equal 0, status, "scaffold a project for the vendor test (err: #{err})"
|
|
193
|
+
|
|
194
|
+
ext_dir = File.join("vendored_app", "ext", "vendored_app")
|
|
195
|
+
sentinel = File.join(ext_dir, "rudy", "defs.d")
|
|
196
|
+
File.write(sentinel, "// stale, hand-mangled copy")
|
|
197
|
+
File.write(File.join(ext_dir, "vendored_app.d"), "// user code — must not be touched")
|
|
198
|
+
|
|
199
|
+
Dir.chdir(ext_dir) do
|
|
200
|
+
status, out, err = run_cli("vendor")
|
|
201
|
+
assert_equal 0, status, "vendor exits 0 (err: #{err})"
|
|
202
|
+
assert(out.include?("Refreshed"), "vendor reports what it refreshed")
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
assert(!File.read(sentinel).include?("stale"), "vendor overwrites the stale vendored source")
|
|
206
|
+
assert_equal Rudy4::Vendor.d_sources.length,
|
|
207
|
+
Dir.glob(File.join(ext_dir, "rudy", "**", "*.d")).length,
|
|
208
|
+
"vendor restores every vendored source"
|
|
209
|
+
assert_equal "// user code — must not be touched",
|
|
210
|
+
File.read(File.join(ext_dir, "vendored_app.d")),
|
|
211
|
+
"vendor leaves user code alone"
|
|
212
|
+
|
|
213
|
+
FileUtils.rm_rf("vendored_app")
|
|
214
|
+
|
|
215
|
+
# -------------------------------------------------------------------------
|
|
216
|
+
section "doctor: quick checks"
|
|
217
|
+
|
|
218
|
+
Dir.mktmpdir("rudy4-doctor-") do |dir|
|
|
219
|
+
Dir.chdir(dir) do
|
|
220
|
+
out = StringIO.new
|
|
221
|
+
status = Rudy4::Doctor.new(quick: true, out: out).call
|
|
222
|
+
text = out.string
|
|
223
|
+
|
|
224
|
+
assert(text.include?("rudy4 doctor"), "doctor prints a header")
|
|
225
|
+
assert(text.include?("ruby headers"), "doctor checks ruby headers")
|
|
226
|
+
assert(text.include?("shared libruby"), "doctor checks the shared library")
|
|
227
|
+
assert(text.include?("gdc (D compiler)"), "doctor checks GDC")
|
|
228
|
+
assert(text.include?("vendored rudy4 sources"), "doctor checks the vendored sources")
|
|
229
|
+
assert(!text.include?("build smoke test"), "--quick skips the build probe")
|
|
230
|
+
|
|
231
|
+
gdc_ok = !text.include?("[fail] gdc")
|
|
232
|
+
headers_ok = !text.include?("[fail] ruby headers")
|
|
233
|
+
if gdc_ok && headers_ok
|
|
234
|
+
assert_equal 0, status, "doctor exits 0 on a healthy toolchain"
|
|
235
|
+
else
|
|
236
|
+
assert_equal 1, status, "doctor exits 1 when the toolchain is missing"
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# ---------------------------------------------------------------------------
|
|
243
|
+
if ENV["RUDY4_FULL"] == "1"
|
|
244
|
+
section "end-to-end: scaffold, GDC build, generated test suite (slow)"
|
|
245
|
+
|
|
246
|
+
Dir.mktmpdir("rudy4-e2e-") do |dir|
|
|
247
|
+
Dir.chdir(dir) do
|
|
248
|
+
status, _out, err = run_cli("init", "fast_math")
|
|
249
|
+
assert_equal 0, status, "init for the e2e build (err: #{err})"
|
|
250
|
+
|
|
251
|
+
# The exact path a new user runs.
|
|
252
|
+
out, build_status = Open3.capture2e({ "GDC" => ENV["GDC"] || "gdc-14" },
|
|
253
|
+
"rake", "test",
|
|
254
|
+
chdir: File.join(dir, "fast_math"))
|
|
255
|
+
puts out
|
|
256
|
+
assert(build_status.success?, "rake test succeeds in the generated project")
|
|
257
|
+
|
|
258
|
+
lib = File.join(dir, "fast_math", "lib", "fast_math_ext.#{RbConfig::CONFIG['DLEXT']}")
|
|
259
|
+
assert(File.file?(lib), "rake compile installs the artifact into lib/")
|
|
260
|
+
|
|
261
|
+
probe, probe_status = Open3.capture2e(
|
|
262
|
+
RUBY, "-I#{File.dirname(lib)}", "-e",
|
|
263
|
+
'require "fast_math"; print FastMath.squares(3).inspect',
|
|
264
|
+
)
|
|
265
|
+
assert_equal "[0.0, 1.0, 4.0]", probe.strip, "the built extension is loadable and correct"\
|
|
266
|
+
if probe_status.success?
|
|
267
|
+
assert(probe_status.success?, "loading the built extension succeeds")
|
|
268
|
+
|
|
269
|
+
# `rake` + no build residue tracked by the gem's own .gitignore.
|
|
270
|
+
gitignore = File.read(File.join(dir, "fast_math", ".gitignore"))
|
|
271
|
+
assert(gitignore.include?("/lib/*.so"), ".gitignore covers built artifacts")
|
|
272
|
+
|
|
273
|
+
# And the doctor, end to end, inside the generated project.
|
|
274
|
+
out = StringIO.new
|
|
275
|
+
status = Rudy4::Doctor.new(out: out).call
|
|
276
|
+
puts out.string
|
|
277
|
+
assert_equal 0, status, "doctor reports a healthy setup inside a generated project"
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# ---------------------------------------------------------------------------
|
|
283
|
+
puts
|
|
284
|
+
if $failures.empty?
|
|
285
|
+
puts "rudy4 gem tests: #{$checks} assertions OK"
|
|
286
|
+
exit 0
|
|
287
|
+
else
|
|
288
|
+
puts "rudy4 gem tests: #{$checks} assertions, #{$failures.length} FAILED"
|
|
289
|
+
$failures.each { |f| puts " - #{f}" }
|
|
290
|
+
exit 1
|
|
291
|
+
end
|
data/test/run.rb
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# Acceptance suite for rudy-d's dextest extension (dexter revival, 2010 -> 2026).
|
|
3
|
+
|
|
4
|
+
require_relative "dextest_loader"
|
|
5
|
+
|
|
6
|
+
$failures = 0
|
|
7
|
+
$count = 0
|
|
8
|
+
|
|
9
|
+
def assert(cond, msg)
|
|
10
|
+
$count += 1
|
|
11
|
+
if cond
|
|
12
|
+
puts "ok #{$count} - #{msg}"
|
|
13
|
+
else
|
|
14
|
+
$failures += 1
|
|
15
|
+
puts "FAIL #{$count} - #{msg}"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def assert_equal(expected, actual, msg = nil)
|
|
20
|
+
assert(expected == actual, "#{msg || 'equal'}: expected #{expected.inspect}, got #{actual.inspect}")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def assert_raises(exp)
|
|
24
|
+
yield
|
|
25
|
+
assert(false, "expected #{exp} to be raised")
|
|
26
|
+
rescue exp
|
|
27
|
+
assert(true, "raised #{exp}")
|
|
28
|
+
rescue => e
|
|
29
|
+
assert(false, "expected #{exp}, got #{e.class}")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# ---------------------------------------------------------------- raw C API class
|
|
33
|
+
d = DexterClass.new
|
|
34
|
+
assert_equal(Class, DexterClass.class, "DexterClass is a class")
|
|
35
|
+
assert_equal(Object, DexterClass.superclass, "DexterClass < Object")
|
|
36
|
+
assert_equal(10, d.return_ten, "return_ten")
|
|
37
|
+
assert_equal([], d.arr, "arr starts empty")
|
|
38
|
+
d.add(1); d.add(2)
|
|
39
|
+
assert_equal([1, 2], d.arr, "add pushes raw VALUEs")
|
|
40
|
+
d.native_add(22)
|
|
41
|
+
assert_equal([1, 2, 23], d.arr, "native_add converts via num2long/ll2inum")
|
|
42
|
+
d.add_strings_first_letter("showtime!")
|
|
43
|
+
assert_equal([1, 2, 23, "s"], d.arr, "add_strings_first_letter")
|
|
44
|
+
|
|
45
|
+
# ---------------------------------------------------------------- def! globals
|
|
46
|
+
assert_equal(15, the_new_fifteen, "def! global function")
|
|
47
|
+
assert_equal(33, convert_integer_two_ways(33), "def! integer round trip")
|
|
48
|
+
assert_equal(45.67, convert_float_two_ways(45.67), "def! float round trip")
|
|
49
|
+
assert_equal("somestring", convert_string_two_ways("somestring"), "def! string round trip")
|
|
50
|
+
assert_equal(true, convert_bool_two_ways(true), "def! bool true round trip")
|
|
51
|
+
assert_equal(false, convert_bool_two_ways(false), "def! bool false round trip")
|
|
52
|
+
assert_equal(17, add_five(12), "def! computed int")
|
|
53
|
+
assert_equal("hi tomash 42", DexterModule.greet("tomash", 42), "def! module function, 2 args, mixed types")
|
|
54
|
+
|
|
55
|
+
# ---------------------------------------------------------------- issue #1 regression: ulong params decode, Raw!VALUE passes bits
|
|
56
|
+
assert_equal(10, identity_ulong(10), "def! ulong param decodes (not tagged)")
|
|
57
|
+
assert_equal(0, identity_ulong(0), "def! ulong zero decodes")
|
|
58
|
+
assert_equal(20, double_it_ulong(10), "def! ulong param arithmetic")
|
|
59
|
+
assert_equal(10, raw_value_id(10), "def! Raw!VALUE passthrough identity")
|
|
60
|
+
assert_equal("abc", raw_value_id("abc"), "def! Raw!VALUE passthrough string object")
|
|
61
|
+
|
|
62
|
+
# ---------------------------------------------------------------- arity / errors
|
|
63
|
+
assert_raises(ArgumentError) { convert_integer_two_ways } # wrong arity
|
|
64
|
+
assert_raises(TypeError) { convert_integer_two_ways("nope") } # bad conversion
|
|
65
|
+
|
|
66
|
+
# ---------------------------------------------------------------- results
|
|
67
|
+
puts
|
|
68
|
+
if $failures.zero?
|
|
69
|
+
puts "PASS: all #{$count} assertions green"
|
|
70
|
+
exit 0
|
|
71
|
+
else
|
|
72
|
+
puts "FAIL: #{$failures}/#{$count} assertions failed"
|
|
73
|
+
exit 1
|
|
74
|
+
end
|
data/test/typedtest.rb
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# Acceptance suite for the typed-data extension (rudy/typeddata.d).
|
|
3
|
+
# Exercises: wrap/unwrap via TypedData API (rb_data_typed_object_wrap /
|
|
4
|
+
# rb_check_typeddata), alloc func, det, GC stress, TypeError on bad unwrap.
|
|
5
|
+
|
|
6
|
+
require "pathname"
|
|
7
|
+
EXT = Pathname.new(__dir__).parent + ".build" + "dtypetest.so"
|
|
8
|
+
raise "extension not built: run `make` first" unless EXT.exist?
|
|
9
|
+
require EXT.to_s
|
|
10
|
+
|
|
11
|
+
$failures = 0
|
|
12
|
+
$count = 0
|
|
13
|
+
|
|
14
|
+
def assert(cond, msg)
|
|
15
|
+
$count += 1
|
|
16
|
+
if cond then puts "ok #{$count} - #{msg}"
|
|
17
|
+
else $failures += 1; puts "FAIL #{$count} - #{msg}" end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def assert_equal(expected, actual, msg = nil)
|
|
21
|
+
assert(expected == actual, "#{msg || 'equal'}: expected #{expected.inspect}, got #{actual.inspect}")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def assert_raises(exp)
|
|
25
|
+
yield
|
|
26
|
+
assert(false, "expected #{exp} to be raised")
|
|
27
|
+
rescue exp
|
|
28
|
+
assert(true, "raised #{exp}")
|
|
29
|
+
rescue => e
|
|
30
|
+
assert(false, "expected #{exp}, got #{e.class}: #{e.message}")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# ---------------------------------------------------------------- basics
|
|
34
|
+
m = DTypeMatrix.new(3, 3)
|
|
35
|
+
assert_equal(9, DTypeMatrix.allocate.unwrap_other(m), "typed wrap/unwrap round trip")
|
|
36
|
+
[[2, 0, 1], [0, 3, 0], [1, 0, 2]].each_with_index do |row, r|
|
|
37
|
+
row.each_with_index { |v, c| m[r, c] = v }
|
|
38
|
+
end
|
|
39
|
+
assert_equal([[2, 0, 1], [0, 3, 0], [1, 0, 2]], m.to_a, "to_a returns matrix contents")
|
|
40
|
+
assert((m.det - 9.0).abs < 1e-9, "3x3 det == 9.0 (Laplace, recursion)")
|
|
41
|
+
assert_equal(24, DTypeTest::SIZEOF, "sizeof Matrix struct exposed (2*8 + 8 == 24 on LP64)")
|
|
42
|
+
|
|
43
|
+
# 10x10 identity perturbation — deep det recursion
|
|
44
|
+
big = DTypeMatrix.new(10, 10)
|
|
45
|
+
10.times { |r| 10.times { |c| big[r, c] = (r == c) ? 1.0 : 0.0 } }
|
|
46
|
+
big[3, 3] = 3.5
|
|
47
|
+
big[7, 0] = 2.0
|
|
48
|
+
assert((big.det - 3.5).abs < 1e-9, "10x10 identity det with [3,3]=3.5")
|
|
49
|
+
|
|
50
|
+
# ---------------------------------------------------------------- TypeError
|
|
51
|
+
assert_raises(TypeError) { DTypeMatrix.allocate.unwrap_other("not a matrix") }
|
|
52
|
+
assert_raises(TypeError) { DTypeMatrix.allocate.unwrap_other(42) }
|
|
53
|
+
assert_raises(TypeError) { DTypeMatrix.allocate.unwrap_other(DTypeMatrix) }
|
|
54
|
+
|
|
55
|
+
# ---------------------------------------------------------------- GC stress
|
|
56
|
+
before = `ps -o rss= -p #{Process.pid}`.to_i
|
|
57
|
+
m2 = DTypeMatrix.new(4, 4)
|
|
58
|
+
m2.stress(100_000)
|
|
59
|
+
after = `ps -o rss= -p #{Process.pid}`.to_i
|
|
60
|
+
assert(after - before < 50_000, "100k typed wrap/free cycles: RSS delta #{after - before} KB < 50 MB")
|
|
61
|
+
|
|
62
|
+
puts
|
|
63
|
+
if $failures.zero?
|
|
64
|
+
puts "PASS: all #{$count} assertions green"
|
|
65
|
+
exit 0
|
|
66
|
+
else
|
|
67
|
+
puts "FAIL: #{$failures}/#{$count} assertions failed"
|
|
68
|
+
exit 1
|
|
69
|
+
end
|
data/vendor/rudy/args.d
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/// Parameter wrappers for `def!` (rudy/defs.d) that reach past the
|
|
2
|
+
/// plain positional-argument model. The 2010 RuDy had none of these;
|
|
3
|
+
/// a `def!`'d function could neither receive the caller's block nor
|
|
4
|
+
/// take rest/keyword arguments.
|
|
5
|
+
///
|
|
6
|
+
/// void with_block(RubyBlock blk) { blk.call(42); } // receives the
|
|
7
|
+
/// // caller's block
|
|
8
|
+
/// long rest_sum(long first, Rest rest) { ... } // 1 + *rest
|
|
9
|
+
/// D str_kw(KwArgs kw) { kw.get!string("sep", "-") } // **kwargs
|
|
10
|
+
///
|
|
11
|
+
/// All wrappers are VALUE-sized (passable as fixed-arity C args); the
|
|
12
|
+
/// interesting VALUE lives inside (block proc, rest array, kwargs
|
|
13
|
+
/// hash). See defs.d for how the trampolines populate them.
|
|
14
|
+
|
|
15
|
+
module rudy.args;
|
|
16
|
+
|
|
17
|
+
import rudy.bind.value;
|
|
18
|
+
import rudy.convert;
|
|
19
|
+
import std.string : toStringz;
|
|
20
|
+
|
|
21
|
+
// lazily-interned method names (rb_intern on every call is a symbol-table
|
|
22
|
+
// hash probe; the VM is alive inside any method call, so lazy init is safe)
|
|
23
|
+
private __gshared ID rudy_id_call;
|
|
24
|
+
private __gshared ID rudy_id_arity;
|
|
25
|
+
private __gshared ID rudy_id_length;
|
|
26
|
+
private __gshared ID rudy_id_to_a;
|
|
27
|
+
private __gshared ID rudy_id_to_s;
|
|
28
|
+
|
|
29
|
+
private ID id_call() { if (!rudy_id_call) rudy_id_call = rb_intern("call"); return rudy_id_call; }
|
|
30
|
+
private ID id_arity() { if (!rudy_id_arity) rudy_id_arity = rb_intern("arity"); return rudy_id_arity; }
|
|
31
|
+
private ID id_length() { if (!rudy_id_length) rudy_id_length = rb_intern("length"); return rudy_id_length; }
|
|
32
|
+
private ID id_to_a() { if (!rudy_id_to_a) rudy_id_to_a = rb_intern("to_a"); return rudy_id_to_a; }
|
|
33
|
+
private ID id_to_s() { if (!rudy_id_to_s) rudy_id_to_s = rb_intern("to_s"); return rudy_id_to_s; }
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------- RubyBlock
|
|
36
|
+
|
|
37
|
+
/// A caller's block, materialized as a Proc. `def!` fills it in via
|
|
38
|
+
/// rb_scan_args' "&" slot inside the trampoline (Qnil when no block was
|
|
39
|
+
/// passed), before the D function runs.
|
|
40
|
+
///
|
|
41
|
+
/// blk.call(args...) — invoke the block
|
|
42
|
+
/// blk.arity — the Proc's arity
|
|
43
|
+
/// blk.given — was a block actually passed?
|
|
44
|
+
///
|
|
45
|
+
/// A block-less method raises LocalJumpError from Ruby the moment
|
|
46
|
+
/// call() runs — same failure mode as bare `yield` in Ruby. Check
|
|
47
|
+
/// `given` first when that must not happen.
|
|
48
|
+
struct RubyBlock
|
|
49
|
+
{
|
|
50
|
+
VALUE proc = Qnil_;
|
|
51
|
+
|
|
52
|
+
/// Was a block actually passed to this method?
|
|
53
|
+
bool given() const @safe pure nothrow @nogc { return proc != Qnil_; }
|
|
54
|
+
|
|
55
|
+
/// The wrapped Proc, as raw VALUE (for API-passing scenarios).
|
|
56
|
+
Raw!VALUE raw() const @safe pure nothrow @nogc { return Raw!VALUE(proc); }
|
|
57
|
+
|
|
58
|
+
/// Number of arguments the block expects (Proc#arity).
|
|
59
|
+
long arity() { return d_type!long(rb_funcallv(proc, id_arity(), 0, null)); }
|
|
60
|
+
|
|
61
|
+
/// Call the block with args; returns whatever the block returned.
|
|
62
|
+
/// LocalJumpError (from Ruby) if called with no block given; any
|
|
63
|
+
/// exception the block raises propagates straight through the D frame.
|
|
64
|
+
VALUE call() { return lje_or_call(proc, 0, null); }
|
|
65
|
+
|
|
66
|
+
VALUE call(VALUE a1)
|
|
67
|
+
{
|
|
68
|
+
VALUE[1] av = [a1];
|
|
69
|
+
return lje_or_call(proc, 1, av.ptr);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
VALUE call(VALUE a1, VALUE a2)
|
|
73
|
+
{
|
|
74
|
+
VALUE[2] av = [a1, a2];
|
|
75
|
+
return lje_or_call(proc, 2, av.ptr);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
VALUE call(VALUE a1, VALUE a2, VALUE a3)
|
|
79
|
+
{
|
|
80
|
+
VALUE[3] av = [a1, a2, a3];
|
|
81
|
+
return lje_or_call(proc, 3, av.ptr);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/// Call with a pre-built argv (n args).
|
|
85
|
+
VALUE callv(int n, const(VALUE)* argv)
|
|
86
|
+
{
|
|
87
|
+
return lje_or_call(proc, n, cast(VALUE*) argv);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/// RubyBlock#call contract helper: a block-less call is the same error
|
|
92
|
+
/// a bare `yield` with no block would give (rb_funcallv on Qnil would
|
|
93
|
+
/// give a misleading NoMethodError instead).
|
|
94
|
+
private VALUE lje_or_call(VALUE proc, int n, VALUE* argv)
|
|
95
|
+
{
|
|
96
|
+
if (proc == Qnil_)
|
|
97
|
+
rb_raise(rb_eLocalJumpError, "no block given (yield)");
|
|
98
|
+
return rb_funcallv(proc, id_call(), n, argv);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ---------------------------------------------------------------- Self
|
|
102
|
+
|
|
103
|
+
/// The method's receiver (self), passed through raw. First-parameter
|
|
104
|
+
/// convention; lets def!'d functions inspect the object they were
|
|
105
|
+
/// called on (class, ivars, singleton-ness) without dropping to a
|
|
106
|
+
/// hand-written trampoline.
|
|
107
|
+
///
|
|
108
|
+
/// long klass_name(Self self) { ... rb_obj_class(self.v) ... }
|
|
109
|
+
struct Self
|
|
110
|
+
{
|
|
111
|
+
VALUE v = Qnil_;
|
|
112
|
+
|
|
113
|
+
/// The receiver as raw VALUE.
|
|
114
|
+
Raw!VALUE raw() const @safe pure nothrow @nogc { return Raw!VALUE(v); }
|
|
115
|
+
|
|
116
|
+
/// implicit conversion so `self.v` and passing Self around stays terse
|
|
117
|
+
VALUE value() const @safe pure nothrow @nogc { return v; }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ---------------------------------------------------------------- Rest
|
|
121
|
+
|
|
122
|
+
/// Captured `*rest` arguments, as a Ruby Array. The trampoline owns the
|
|
123
|
+
/// VALUE (GC-rooted for the call's duration); the D function sees this
|
|
124
|
+
/// slice-style wrapper.
|
|
125
|
+
struct Rest
|
|
126
|
+
{
|
|
127
|
+
VALUE ary = Qnil_;
|
|
128
|
+
|
|
129
|
+
/// Number of captured rest arguments (0 for an unused slot).
|
|
130
|
+
@property size_t length() const
|
|
131
|
+
{
|
|
132
|
+
if (ary == Qnil_)
|
|
133
|
+
return 0;
|
|
134
|
+
return cast(size_t) d_type!long(rb_funcallv(ary, id_length(), 0, null));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/// True when nothing was captured. NB: rb_scan_args' "*" slot fills
|
|
138
|
+
/// an EMPTY ARRAY (not Qnil) when the caller passes zero rest args —
|
|
139
|
+
/// this must agree with Ruby-side `rest.empty?`, so it checks the
|
|
140
|
+
/// length, not just the slot's Qnil-ness.
|
|
141
|
+
bool empty() { return length == 0; }
|
|
142
|
+
|
|
143
|
+
/// i-th captured argument (0-based).
|
|
144
|
+
VALUE opIndex(size_t i)
|
|
145
|
+
{
|
|
146
|
+
return rb_ary_entry(ary, cast(long) i);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/// Iterate the captured arguments (foreach over a Rest works).
|
|
150
|
+
int opApply(scope int delegate(ref VALUE) dg)
|
|
151
|
+
{
|
|
152
|
+
long n = cast(long) length;
|
|
153
|
+
foreach (i; 0 .. n)
|
|
154
|
+
{
|
|
155
|
+
VALUE v = rb_ary_entry(ary, i);
|
|
156
|
+
int r = dg(v);
|
|
157
|
+
if (r)
|
|
158
|
+
return r;
|
|
159
|
+
}
|
|
160
|
+
return 0;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/// The raw Ruby Array, as raw VALUE.
|
|
164
|
+
Raw!VALUE raw() const @safe pure nothrow @nogc { return Raw!VALUE(ary); }
|
|
165
|
+
|
|
166
|
+
/// Convert every captured arg to D type T (allocates a D array).
|
|
167
|
+
T[] to(T)()
|
|
168
|
+
{
|
|
169
|
+
long n = cast(long) length;
|
|
170
|
+
T[] outv;
|
|
171
|
+
outv.length = cast(size_t) n;
|
|
172
|
+
foreach (i; 0 .. n)
|
|
173
|
+
outv[cast(size_t) i] = d_type!T(rb_ary_entry(ary, i));
|
|
174
|
+
return outv;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ---------------------------------------------------------------- KwArgs
|
|
179
|
+
|
|
180
|
+
/// Captured keyword arguments, as a Ruby Hash. `def!` fills it via
|
|
181
|
+
/// rb_scan_args' `:` slot (Qnil_ when no keywords were passed — see
|
|
182
|
+
/// `given`).
|
|
183
|
+
///
|
|
184
|
+
/// kw.get!long("sep", 1) — decoded with a D default when absent
|
|
185
|
+
/// kw["sep"] — raw VALUE (Qnil_ when absent)
|
|
186
|
+
/// kw.to!(long, string)() — whole hash as a D AA
|
|
187
|
+
struct KwArgs
|
|
188
|
+
{
|
|
189
|
+
VALUE hash = Qnil_;
|
|
190
|
+
|
|
191
|
+
/// Were any keywords passed?
|
|
192
|
+
bool given() const @safe pure nothrow @nogc { return hash != Qnil_; }
|
|
193
|
+
|
|
194
|
+
/// Raw value of keyword `key` (Qnil_ when absent). Ruby keyword keys
|
|
195
|
+
/// are Symbols (`m(sep: x)`), but string keys are legal too
|
|
196
|
+
/// (`m("sep": x)` / string-keyed **hash) — probe both.
|
|
197
|
+
VALUE opIndex(string key)
|
|
198
|
+
{
|
|
199
|
+
if (!given)
|
|
200
|
+
return Qnil_;
|
|
201
|
+
ID id = rb_intern(toStringz(key));
|
|
202
|
+
VALUE v = rb_hash_lookup2(hash, rb_id2sym(id), Qnil_);
|
|
203
|
+
if (v != Qnil_)
|
|
204
|
+
return v;
|
|
205
|
+
VALUE[1] av = [rb_str_new(key.ptr, cast(C_long) key.length)];
|
|
206
|
+
return rb_hash_lookup2(hash, av[0], Qnil_);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/// Keyword decoded to D type T; `dflt` when the keyword was absent.
|
|
210
|
+
T get(T)(string key, T dflt)
|
|
211
|
+
{
|
|
212
|
+
VALUE v = opIndex(key);
|
|
213
|
+
if (v == Qnil_ || v == Qundef_)
|
|
214
|
+
return dflt;
|
|
215
|
+
return d_type!T(v);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/// All keywords as a D associative array (empty when none passed).
|
|
219
|
+
V[K] to(V, K)()
|
|
220
|
+
{
|
|
221
|
+
V[K] outv;
|
|
222
|
+
if (!given)
|
|
223
|
+
return outv;
|
|
224
|
+
// Hash#to_a -> [[k, v], ...]: plain exported-API walking, no
|
|
225
|
+
// callbacks (rb_block_call's return value can't accumulate).
|
|
226
|
+
VALUE pairs = rb_funcallv(hash, id_to_a(), 0, null);
|
|
227
|
+
long n = d_type!long(rb_funcallv(pairs, id_length(), 0, null));
|
|
228
|
+
foreach (i; 0 .. n)
|
|
229
|
+
{
|
|
230
|
+
VALUE pair = rb_ary_entry(pairs, i);
|
|
231
|
+
VALUE k = rb_ary_entry(pair, 0);
|
|
232
|
+
static if (is(K == string))
|
|
233
|
+
{
|
|
234
|
+
// kwargs keys are normally Symbols; d_type!string of a
|
|
235
|
+
// Symbol raises TypeError. Normalize through to_s
|
|
236
|
+
// (Symbol#to_s -> name String, String#to_s -> self) so
|
|
237
|
+
// string-keyed D AAs see every key. Non-string K keys
|
|
238
|
+
// go through d_type directly.
|
|
239
|
+
k = rb_funcallv(k, id_to_s(), 0, null);
|
|
240
|
+
}
|
|
241
|
+
static if (is(V == Raw!VALUE))
|
|
242
|
+
{
|
|
243
|
+
// raw VALUE escape hatch: bits unchanged, no decode
|
|
244
|
+
outv[d_type!K(k)] = Raw!VALUE(rb_ary_entry(pair, 1));
|
|
245
|
+
}
|
|
246
|
+
else
|
|
247
|
+
{
|
|
248
|
+
outv[d_type!K(k)] = d_type!V(rb_ary_entry(pair, 1));
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return outv;
|
|
252
|
+
}
|
|
253
|
+
}
|